해당 포스팅은 강병탁 교수님의 수업과 리버스 엔지니어링 바이블을 참고해 작성했습니다.
F-Secure level 1 문제 풀어보기
- 이 프로그램을 리버싱해서 시리얼 넘버를 찾아보자.
Step 1. 파일 스캐너로 스캔
- 파일 스캐너로 해당 파일의 패킹 여부를 확인하자.
- IAT(Import Address Table)이 제대로 보이면 패킹이 안되었다는 것이다.
Step 2. API 확인
- IAT를 통해 어떤 API가 사용되었는지 확인하자.
- 해당 dll 파일에서 어떤 함수를 불러 왔는지 알 수 있고, 이를 통해 대략적인 행위를 예상하자.
MSVCR71.dll
의_stricmp (IAT: 0x2230)
을 확인할 수 있는데, 결정적으로 쓰이는 함수인지 의심해볼 필요가 있다.
Step 3. 파일의 이미지 베이스 확인
- 실행 파일의 이미지 베이스를 확인해 디버깅 할 번지가 어디인지 확인하자.
- PE의 Optional Header의 ImageBase를 보면
0x69000000
이라고 적혀있다. (대부분 400000 번지지만)
Step 4. 디버깅 해보기
-
OllyBDG로 디버깅을 해보면 아까 찾은 이미지 베이스부터 시작하는 걸 볼 수 있다.
-
프로그램에서 쓰인 string을 메모리에서 찾아서 트레이스를 하거나,
-
_strcmp()
가 쓰인 부분을 찾아 함수가 시작하는 부분에 BP를 걸고 실행시켜보자. -
여기에서는 네 가지 방법으로 같은 결과를 보여줄 수 있다.
1) Jmp 문 패치
jnz
는 (레지스터의) zero flag(Z)가 켜져있지 않을 때 해당 주소로 점프하는 코드인데test eax, eax
에서 eax가 0일 때만 zero가 된다 (Z가 1로 바뀐다)_stricmp
는 비교하는 문자가 같을 때 0을 리턴하므로 위와 같이 다른 문자를 입력했을 때는 Z=0이다.- 근데 여기서
jnz
를jz
로 변경하면 Z가 꺼져있을 때만 점프하게 된다. - 이는 코드를 변경하는 방식이므로 무결성 검사가 이루어졌을 때는 사용할 수 없다.
2) 플래그 패치
- 코드를 변경하지 말고 레지스터의 zero flag를 1로 변경시켜도 점프문을 통과할 수 있다.
3) Nop 패치
- 점프문을
nop
명령어로 대체하여 Sorry 부분으로 점프하지 못하도록 변경한다. - 여기에서 주의할 점은
nop
는 1바이트이므로 2바이트(jnz
는 2바이트)를 맞춰서 두 번 넣어줘야한다.
!
4) API 패치
- 연결된 dll 부분으로 들어가 리턴값을 바꿔준다.
- 즉,
MSVCR71.dll
의_stricmp()
함수의 리턴값을 무조건 0을 리턴하도록 만들어준다. _stricmp()
의 최종 리턴 부분인RETN
부분을XOR EAX, EAX
로 변경하고RETN
을 추가하자.- 그럼 값에 상관 없이 EAX 에는 0이 들어가고 0을 리턴하게 될 것이다.
String 조작하기
- 한편 출력하려는 string을 변경할 수도 있는데, PUSH 하는 부분에서 ascii 값의 주소를 알아내고 아래와 같이 드래그를 한 뒤 Binary edit(Ctrl + E)를 선택하면
- 이와 같이 출력 문자를 변경할 수 있다.
- 이 주제랑은 그렇게 상관있는 내용은 아닌데, 주소를 리틀 엔디안 방식으로 입력한다는 것을 주의해야한다.
0x690030E8
→E8300069
로 표현
Call Stack
-
리버스 엔지니어링을 위해 스택에 관해 알아야 할 지식은 다음과 같다.
- 함수 호출 시 파라미터가 들어가는 방향
- 리턴 주소
- 지역 변수 사용
-
함수 안에서 스택을 사용하게 되면 보통 다음과 같은 코드가 함수의 시작 부분에 생성된다.(fastcall)
push ebp -> 1. ebp 레지스터를 스택에 넣는다. mov ebp, esp -> 2. 현재 esp의 값을 ebp에 넣는다. sub esp, 0C -> 3. esp에서 0C만큼 뺀다.
-
여기서 esp는 현재 스택의 가장 위에 있는 데이터를 가리키고 있는 포인터이고
-
ebp는 현재 스택의 가장 바닥을 가리키는 포인터이다.
-
두 번째 줄까지 실행시키면 ebp와 esp 값이 같아지면서 이 함수에서 지역변수는 ebp 에서부터 얼마든지 계산할 수 있다.
- ebp를 기준으로 오프셋을 더하고 빼는 작업으로 스택을 처리할 수 있게 된다는 뜻이다.
-
스택은 LIFO 특성으로 아래로 자라는데, 특정 값만큼 빼면 그만큼 스택을 사용하겠다는 의미이다.
- 따라서 세 번째 줄을 실행시키면 스택을 0C만큼 지역변수를 사용하겠다고 해석할 수 있다.
-
그러면 ebp는 현재 함수에서 스택의 맨 위가 되었고, 첫 번째 번지가 되었다.
-
또한 사이즈를 빼가면서 자리를 확보하고 있으므로, 결국 지역 변수는 “-“ 마이너스 형태로 계산이 가능하다.
- 4바이트 단위로 움직이는 변수라고 생각했을 때 ebp-4는 첫 번째 지역 변수가 될 것이고, ebp-8은 두 번째 지역 변수가 될 것이다.
-
예제로 다시 한 번 살펴보자.
-
여기서 ebp: 0019FF28 / esp: 0019FF14이다.
- 첫 번째 줄인 push ebp를 실행시키면 이전 스택의 base를 스택에 올린다.
- 두 번째 줄인 mov ebp, esp를 실행시키면 현재 스택의 꼭대기 주소를 새로운 스택의 base를 설정한다.(새로운 스택이 시작됨)
- 여기서 ebp: 0019FF10 / esp: 0019FF10이다.
- 세 번째 줄인 sub esp, 0C를 실행시키면 스택을 12 바이트 내려서 지역변수로 그 만큼 사용할 수 있게 된다.
- 여기서 ebp: 0019FF10 / esp: 0019FF04이다.
- 함수의 마지막 부분에는 스택을 원상복구 시 마지막에 있는 ebp를 pop 시켜주고 ebp+4로 리턴하면 된다.
- 또한 전역 변수는 ebp+8부터 사용할 수 있다.
함수의 호출 규약
int sum(int a, int b) {
int c = a + b;
return c;
}
int main(int argc, char* argv[]) {
sum(1, 2);
return 0;
}
-
여기의 sum 함수를 어셈블리로 나타내면 다음과 같다.(호출 규약에 따라 변경 될 수도, 이건 cdecl)
⭐️ push ebp ⭐️ mov ebp, esp push ecx mov eax, [ebp+arg_0] add eax, [ebp+arg_4] mov [ebp+var_4], eax mov eax, [ebp+var_4] ⭐️ mov esp, ebp ⭐️ pop ebp ⭐️ retn
-
⭐️ 부분은 모든 함수에서 공통적으로 볼 수 있다.
-
이런 함수를 세 가지 방법으로 호출해보자.
1. cdecl (declaration)
-
함수 선언 부분을
int __cdecl sum(int a, int b)
로 변경해주면 된다. -
그럼 sum과 main 함수는 다음과 같이 만들어진다.
<sum 함수> push ebp mov ebp, esp push ecx mov eax, [ebp+arg_0] add eax, [ebp+arg_4] mov [ebp+var_4], eax mov eax, [ebp+var_4] mov esp, ebp pop ebp retn <main 함수> push 2 push 1 call calling.00401000 ⭐️ add esp, 8
-
⭐️ 부분은 스택을 사용한 만큼 다시 원래대로 당겨주는 의미
-
즉 cdecl 은 함수를 호출한 쪽에서 스택을 보정해준다.
-
add에 들어간 operand 값으로 파라미터의 데이터 사이즈를 알 수 있다.
-
여기에서는 eax에 숫자가 들어가는 것으로 보아 리턴 값은 주소 값이 아닌 숫자임을 확인 할 수 있다.
2. stdcall
-
함수 선언 부분을
int __stdcall sum(int a, int b)
로 변경해주면 된다.<sum 함수> push ebp mov ebp, esp push ecx mov eax, [ebp+arg_0] add eax, [ebp+arg_4] mov [ebp+var_4], eax mov eax, [ebp+var_4] mov esp, ebp pop ebp ⭐️ retn 8 <main 함수> push 2 push 1 call calling.00401000
-
cdecl과 다르게 함수 안에서 스택을 보정한다.
-
main의
add esp, 8
이 없어진 대신 retn에 8이 생겼다. -
Win32 API는 __stdcall 방식을 이용한다.
3. fastcall
-
함수 선언 부분을
int __fastcall sum(int a, int b)
로 변경해주면 된다.<sum 함수> push ebp mov ebp, esp ⭐️ sub esp, 0Ch mov [ebp+var_C], edx mov [ebp+var_8], ecx mov eax, [ebp+var_8] add eax, [ebp+var_C] mov [ebp+var_4], eax mov eax, [ebp+var_4] mov esp, ebp pop ebp retn <main 함수> push ebp mov ebp, esp ⭐️ mov edx, 2 ⭐️ mov ecx, 1 call sub_401000 xor eax, eax pop ebp retn
-
인자가 2개 이하일 때, stack에 값을 push하지 않고 ecx, edx 레지스터로 바로 계산하는 방식이다.
-
sub esp, 0Ch
로 스택 공간을 확보하고 edx 레지스터를 사용했다.