티스토리 뷰
02. C 문법과 디스어셈블링
함수의 기본 구조
리버싱 엔지니어링에서 가장 큰 골칫거리는 개발자가 직접 코딩한 부분이 아니라 빌드 시에 컴파일러가 자동으로 생성해내는 코드를 필터링하는 것이다. 이런 필요없는 부분을 넘어갈 수 있어야 한다. 간단하게 예시들을 살펴보자.
int sum(int a, int b){
int c =a+b;
return c;
}
2줄짜리 함수도 디스어셈해보니 10줄이 된다. 여기서 필요없는 부분을 걸러내는게 중요하다.
ebp는 Extended Base Pointer로 스택 베이스 포인터로, 스택에서 사용될 함수의 시작 기준점을 잡는 레지스터이다.
먼저 시작의 push ebp, mov ebp, esp가 보인다. 이 부분을 프롤로그, 시작 부분이다. 함수를 호출해 스택에서 사용하겠다는 ebp를 push하고 ebp와 esp의 위치를 동일하게 해 다음에 push될 변수들을 쌓을 수 있게 정리하는 과정이다.
마지막 mov esp, ebp와 pop ebp은 에필로그 부분으로 ebp를 esp(스택 상단)까지 가져와 함수의 스택 사용을 멈추겠다는 의미이고, pop ebp로 다시 이전 함수로 돌아갈 준비를 하는 과정이다.
함수의 호출 규약
리버서가 바이너리를 분석하면서 먼저 해야할 일은 눈에 보이는 함수의 역할은 무엇이고, 파라미터는 이렇게 넘어가는 구나를 이해해야 한다. 그의 선행 작업으로 함수가 어떻게 생겼고 인자가 몇 개인지 등에 대한 정보를 알아야 한다.
함수 호출 규약에는 4가지 방법이 있다.
1. __cdecl
2. __stdcall
3. __fastcall
4. thiscall
여기서 우리가 확인할 것은 디스어셈블된 코드를 보고 어떤 콜링 컨벤션에 해당되는지 파악해야 한다. 이를 확인하는 목적은 리버싱을 할 때 call 문을 보고 함수의 인자가 몇개이고 어떤 용도로 쓰였는지를 분석하기 위함이다. 일단 __cdecl을 확인해 보자.
int __cdecl sum(int a, int b){
int c = a + b;
return c;
}
int main(int argc, char* argv[]){
sum(1,2);
return 0;
}
가장 먼저 살펴봐야할 것은 call 문의 다음 줄로 스택을 정리하는 곳이 있는지이다.
이 코드처럼 add esp, 8과 같이 스택을 보정하는코드가 등장하면 이것이 __cdecl 방식이다.(__cdecl 방식은 call한 함수 밖에서 스택을 보정한다.) 이 명령어로 파라미터 개수까지 파악이 가능하다. 인자는 4바이트씩 계산되므로 스택을 8바이트까지 끌어올린다는 점은 파라미터가 2개라는 의미이다.
add 명령어가 스택을 보정하는 명령어인 이유는 call할 함수가 사용할 인자 2개(push 2, push 1)이 사용한 자리만큼을 다시 올라가기 위함이다.
여기까지 알아낸 정보는
1.__cdecl 방식
- call 명령어 밑에 add 명령어로 스택을 정리하는 모습으로 함수를 호출한 곳에서 스택을 정리하는 __cdecl 방식임을 알 수 있다.
2. 파라미터는 2개
- add esp,8 그리고 push 명령어가 2개인 것으로 4바이트 파라미터 2개임을 확인.
3.리턴 값이 숫자
- sum함수의 맨 마지막 부분에 eax에 들어가는 값이 숫자(mov 명령어를 사용했기 때문에 알 수 있음)라는 것을 봐서 리턴 값은 주소 같은 값이 아닌 숫자.
__stdcall 방식으로 다시 디스어셈 해보자.
sum 함수를 선언할 때 __stdcall을 붙였더니 main문이 이전과는 다르게 디스어셈 됐다.
차이점은 sum 함수의 디스어셈에서 그냥 retn이 아닌 retn 8을 했다는 점이다.
즉, 함수 안에서 스택을 처리한다는 의미이다. 그래서 8바이트의 스택 정리와 파라미터의 개수를 함수 내에서 판단해야 한다.
__fastcall의 경우엔
sum 함수 내에서 sub esp, 0C로 스택의 자리를 확보한다. __fastcall 컨벤션은 함수의 파라미터가 2개 이하일 경우, 인자를 push로 넣지 않고 ecx, edx 레지스터를 이용한다. 메모리를 이용하는 것보다 레지스터를 이용하는 편이 더빠르기 때문이다.
__thiscall 방식은
C++에서 주로 사용하는 방식이다. 현재 객체의 포인터를 ecx에 전달하는 특징이 있다. C++에서 많이 사용하는 this의 개념을 사용하는데 this포인터를 ecx에 전달하는 방식이다. 그리고 해당 클래스에서 사용하는 멤버 변수나 각종 값은 다음과 같이 ecx 포인터에서 오프셋 값으로 주소를 사용한다.
if문
다음의 간단한 코드를 디스어셈해 봤다.
temp 디스어셈
main 디스어셈
일단 main문을 먼저 살펴보자.
push ebp
mov ebp, esp
이전의 함수로의 ret을 push하고 main함수의 시작지점인 ebp를 push한 후, ebp와 esp를 같은 위치로 옮긴다.
push ~~~
start 값이 들어있는 위치인 0x403020을 push해 다음에 호출될 함수의 인자로 전달한다.
call printf
printf함수를 호출한다.
add esp, 4
위에서 배웠던 __cdecl 방식을 사용했다는 것을 알 수 있고, 인자는 4바이트 인자 1개인걸 알 수 있다.
push 1
다음에 호출될 함수에서 사용될 인자 1을 push한다.
call temp
temp 함수를 호출한다.
add esp, 4
__cdecl 방식이고 인자 1개를 사용했다.
xor eax, eax
같은 값을 xor 연산해 eax를 0으로 만들었다.
pop ebp
ebp를 pop해 main 이전의 함수로 리턴한다.
temp 함수
그럼 temp 함수 안으로 들어가 보자.
temp 함수의 주소는 0x401080이다.
먼저 프롤로그로 스택을 시작한 다음, push ecx를 통해 fastcall 방식을 사용했다는걸 알 수 있다.
mov dword ptr ss:[ebp-0x4], 0x1
mov 명령어로 주소에 값을 넣는다는 것을 알 수 있고, dword 단위로 0x1 값을 ebp-0x4 주소의 값으로 설정한다는 의미이다. C 코드에선 b의 값을 1로 설정하는 부분이다.
cmp dword ptr ss:[ebp+0x8], 0x1
cmp 명령어로 값을 비교한다. 0x1과 ebp+0x8값을 비교하는데 ebp가 main함수의 가장 높은 부분(시작 부분, 스택은 높은 곳에서 낮은 곳으로 자란다는 걸 기억.)인데 이보다 높은 부분은 어디일까? 이전 포스트의 그림을 참고해보면 ebp+4는 이전 함수로의 ret고 ebp+8은 인자라는 것을 알 수 있다. main함수에서 인자로 전달한 1과 0x1을 비교한다는 의미이다. C 코드에선 if문에 해당되는 부분이다.
jne 0x0040109C
jne이라는 명령어는 처음엔 어려울 수 있다. jump not equal이라는 명령어로 조건문에서 많이 사용하는 jump에 관한 명령어이다. 이외에도 엄청 많은 명령어들이 있으니 나올때 마다 찾아보도록 하자.
점프 명령은 주로 이전 명령(여기선 cmp)의 결과를 가지고 결정된다. 이번의 경우엔 cmp의 결과가 equal일 경우엔 계속 진행하고 not equal일 경우엔 0x40109c로 점프하라는 명령이다.
cmp의 결과가 equal이므로 계속 진행한다. C 코드로 봤을 땐 if의 조건으로 들어갈지 else의 조건으로 들어갈지가 결정되는 부분이다.
mov eax, dword ptr ss:[ebp+0x8]
인자 값1을 eax에 옮기는 명령어이다.
add eax, 0x1
eax(1)에 1을 더한다.
mov dword ptr ss:[ebp+0x8], eax
eax 값을 ebp+8, 인자 값으로 옮긴다. 결국 a++가 된다.
jmp 0x004010A5
별 다른 조건 없이 0x4010a5로 점프한다.
mov eax, dword ptr ss:[ebp-0x4]
ebp-0x4 값을 eax에 옮긴다. 즉 b의 값 1을 eax에 담는다.
반복문
구조체와 API call
프로세스를 생성하는 CreateProcess() 함수를 살펴보자. 이는 STARTUPINFO와 RPOCESS_INFORMATION 구조체를 사용한다.
다음 코드를 어셈블리어로 확인해보자.
#include <windows.h>
#include <stdio.h>
void RunProcess()
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
// Start the child process.
if (!CreateProcess(NULL,
"MyChildProcess",
NULL,
NULL,
FALSE,
0,
NULL,
NULL,
&si,
&pi)
)
{
printf("CreateProcess failed.\n");
return;
}
// Wait until child process exits.
WaitForSingleObject(pi.hProcess, INFINITE);
// Close process and thread handles.
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
void main(int argc, char* argv[])
{
RunProcess();
}
꽤 길지만 이제 눈에 보이는 부분이 많다.
프롤로그 부분을 지나서
sub esp, 54로 94바이트만큼을 확장한다. 왜 0x54만큼 확장하는지 코드를 보면 이해가 된다.
RunProcess를 시작한 후 2개의 구조체를 선언한다.
첫번째 STARTUPINFO 구조체는 0x44바이트이고 두번째 PROCESS_INFOMATION은 0x10바이트이기 때문에 0x54바이트만큼 확장이 되는 것이다. 앞으로 선언될 모든 변수들의 크기를 알고 스택의 확장을 예측할 수는 없는 노릇이다. 그러니 확장된 크기로 대충은 감을 잡을 수 있다.
코드 상에서 다음은 ZeroMemory함수를 사용해야 하는데 어셈블리에는 memset이라는 함수를 call한다. 더구나 인자도 원래는 2개지만 3개(44,0,eax)를 전달한다. 그 이유는 ZeroMemory함수가 사실은 memset함수를 이용하기 때문이다.
따라서 Destination으로 ebp-54에 위치해있던 주소값을, Length로 44를 전달한다.
이후 진행하는 명령어 add로 __cdecl 규약임을 확인할 수 있다. memset의 인자 3개만큼 0xC크기를 정리한다.
다음 mov명령어로 si구조체의 cb값으로 44를 전달한다.
xor ecx로 다음에 사용할 레지스터의 값을 0으로 초기화 해준 후
PROCESS_INFORMATION을 초기화 해준다.
PROCESS_INFOMATION의 변수들을 0으로 초기화해준 후 ebp-10의 값을 edx에 넣는다.
이 후 CreateProcess가 실행되었는지를 확인하는 if문으로 진입한다.
이때 CreateProcess의 인자로 10개를 전달한다.
pi의 주소값이 담긴 edx, si의 주소값이 담긴 eax를 확인할 수 있고 그 뒤로는 0이다.
CreateProcess를 호출한 뒤로 조건에 따라 끝낼 것인지 WaitForSingleObject 함수를 호출할지 정한다.
WaitforSingleObject를 호출한다고 가정하고 어셈블리어를 보자.
함수의 인자로 전달될 2개의 변수 FFFFFFFF와 ecx이다.
FFFFFFFFF은 INFINITE이고 ecx는 위에서 사용했던 pi의 주소값이 들어있다.
마지막은 CloseHandle과 에필로그 부분인데 전달되는 인자들을 코드와 비교하며 살펴보니 ebp-10이 hProcess이고 ebp-C가 hThread라는 것을 확인할 수 있다.
'리버싱' 카테고리의 다른 글
PinTool Opcode(XOR,AND,SFT) 출력 구현 (0) | 2022.04.10 |
---|---|
리버스 엔지니어링 바이블-0320 (0) | 2022.03.21 |
리버싱 엔지니어링 바이블 - 0307 (0) | 2022.03.07 |
PinTool BBL 개수 출력 (0) | 2022.02.15 |
Taint Analysis란 (0) | 2022.02.14 |
- Total
- Today
- Yesterday
- 딥러닝
- CAN-FD
- automotive ethernet
- AE
- porks
- PCA
- 이상탐지
- json2html
- Ethernet
- Python
- 회귀
- 크로스 엔트로피
- many-to-one
- one-to-many
- HTML
- cuckoo
- 차량용 이더넷
- 머신러닝
- 차량 네트워크
- many-to-many
- SVM
- automotive
- 단순선형회귀
- AVB
- SOME/IP
- 로지스틱회귀
- 케라스
- 논문 잘 쓰는법
- AVTP
- problem statement
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |