이번에는 3번째 예제를 분석해보겠습니다.
프로그램 실행 시 다음과 같이 알림이 뜨고 확인을 누르면
다음 창이 뜨고 프로그램이 종료됩니다.
함수 호출 규약이란 함수를 호출할 때 인자를 전달하는 방식이나 함수 실행이 끝나고 스택을 정리하는 방식에 대한 약속입니다.
크게 3가지의 방식이 있습니다.
=> 인자가 오른쪽에서 왼쪽으로 순서대로 스택으로 전달되고 함수 종료 시 호출자가 피호출자의 스택 프레임을 정리합니다.
=> 인자가 오른쪽에서 왼쪽으로 순서대로 스택으로 전달되고 함수 종료 시 피호출자가 스스로 자신의 스택 프레임을 정리합니다.
=> 인자가 오른쪽에서 왼쪽으로 순서대로 레지스터를 사용해서 전달되고 레지스터를 사용하므로 별도로 스택을 정리할 필요가 없습니다.
예시를 들어 설명해보겠습니다.
고급 언어
void caller() {
callee(1,2);
return 0;
}
고급 언어가 다음과 같이 작성되있다고 해보면
cdecl 방식의 어셈블리어는 다음과 같습니다.
PUSH 2
PUSH 1
CALL callee
ADD ESP 8
즉, 스택은 후입선출의 자료구조이므로 오른쪽인자부터 넣어 꺼낼때는 왼쪽인자(1) 부터 나오게 됩니다.
또한 마지막에 사용한 스택 프레임 정리과정에서 인자 2개, 총 8바이트를 사용하였으므로 현재 ESP값에 8바이트를 더해주어 스택을 정리합니다.
stdcall의 경우
PUSH 2
PUSH 1
CALL callee
한 후 호출된 callee 루틴에서
PUSH EBP
MOV EBP, ESP
.............
POP EBP
RETN 8
이렇게 해줍니다.
분석해보면 마찬가지로 인자의 오른쪽부터 스택에 값을 넣어준 후 callee 서브루틴을 호출합니다.
그 후 서브루틴의 스택 영역을 할당하고 마지막에 RETN 8로 사용한 바이트 수만큼을 호출자가 아닌 피호출자가 직접 스택을 정리합니다.
마지막으로 fastcall의 경우
MOV EDX 2
MOV ECX 1
CALL callee
한 후 호출된 callee 루틴에서
PUSH EBP
MOV EBP, ESP
............
MOV DWORD PTR SS:[EBP-8], EDX
MOV DWORD PTR SS:[EBP-4], ECX
............
POP EBP
RETN
이렇게 해줍니다.
분석해보면 레지스터에 인자를 할당할 때는 왼쪽에 있는 1부터 순서대로 할당되고 callee함수 안에서 레지스터에 들어가 있는 값을 사용할 때는 스택에 먼저 복사한 다음에 스택에 있는 값을 사용합니다.
윈도우에서 음수를 표현할 때 2의 보수 방식을 사용합니다. 운영체제 수업 때 배웠던 기억이 있네요.
숫자의 2진수에서 NOT연산을 한 후에 1을 더해주는 방식입니다.
예시를 들어보자면,
-4를 표현하려면 4의 이진수 값 0000 0100에서 NOT연산을 해줍니다.
그러면, 1111 1011이 되고 여기에 1을 더해주면 1111 1100이 됩니다.
이 값은 16진수로 표현하면 FC가 됩니다.
올리디버거로 파일을 열자 생각보다 단순한 파일이었습니다.
엔트리 포인트 아래 필요한 문자열들이 보여서 문자열을 찾을 필요가 없었습니다.
일단 분석해보자면
MessageBoxA함수 호출로 알림창을 띄우고
CreateFileA함수로 abex.12c라는 이름의 파일이 있는지 확인합니다.
그리고 여기 분기점에서 EAX와 -1을 묵시점 빼기를 하여 결과 값이 0이면 ZF가 1이되어 다음 JE 분기점에서 지정된 주소로 점프를 합니다. 00401075에는 실패 메세지가 나옵니다.
만약 파일이 있다면, GetFilesize함수로 파일의 사이즈를 확인합니다.
여기서도 CMP로 EAX와 12(10진수로는 18) 값을 비교합니다.
그 후, JNZ 분기문으로 ZF가 1이 아닐경우 분기하지 않고 최종 성공 메세지가 나오고 ZF가 1일 경우 분기하여 실패 메세지가 나옵니다.
본격적으로 문제 해결을 해보겠습니다.
일단 CreateFileA() 함수에 대해 알아보면
HANDLE CreateFileA(
[in] LPCSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);
상단에 파일 이름을 지정하는 부분이 있습니다.
그러므로 코드에서 abex.l2c라는 이름의 파일이 있는지 확인을 하므로 abex crakcme3파일이 있는곳에 abex.l2c라는 파일을 하나 만들어 주겠습니다.
이제 아마 CreateFileA 부분은 통가하겠지만 GetFilesize 함수에서 실패로 분기될 것입니다.
그러면 CreateFileA 함수 이전에 브레이크를 걸어 확인해 보겠습니다.
역시 JE부분에서 분기되지 않고 무사히 건너왔습니다.
하지만, GetFileSize에서 역시 분기되어 실패 메세지 호출 함수로 이동하는군요.
그러면, 해당 분기점에서도 그대로 건너가게 하기위해 해당 파일 사이즈를 바꿔 주어야 합니다.
파일에 1234567890을 입력하고 실행 후 CMP이전에 EAX값을 보면
제가 입력한 10바이트 만큼인것을 알 수 있습니다.
즉, CMP에서 비교하는 것은 파일의 크기와 12(10진수 18) 인 것을 알 수 있습니다.
그리고 JNZ에서 분기되지 않으려면 ZF가 1이어야 하므로 CMP에서 EAX와 12는 같은 값이어야합니다.
그러므로 파일의 크기를 총 18바이트로 맞추겠습니다.
이제 다시 디버깅해보면
분기점을 지나서 최종 성공 메세지 함수로 왔습니다.
성공 메세지
함수 호출 시 호출 전에 인자들을 스택이나 레지스터에 집어 넣은 후에 call을 수행
cdecl 방식
=> 인자를 오른쪽에서 부터 왼쪽 순으로 스택에 넣고 스택 프레임 정리 시 호출자가 피호출자의 스택 프레임 정리 (ADD ESP, 8)
stdcall 방식
=> 인자를 오른쪽에서부터 왼쪽 순으로 스택에 넣고 스택 프레임 정리 시 피호출자가 스스로 자신의 스택 프레임 정리 (RETN)
fastcall 방식
=> 인자를 레지스터에 넣어 사용하고 함수 호출하고 레지스터에 값 사용 시 스택에 먼저 복사한 후에 사용
CreateFileA() : 파일 또는 I/O 디바이스를 만들거나 열어줍니다.
HANDLE CreateFileA(
[in] LPCSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);
GetFileSize() : 지정된 파일의 크기를 검색합니다.
DWORD GetFileSize(
[in] HANDLE hFile,
[out, optional] LPDWORD lpFileSizeHigh
);
CMP 인자1, 인자2
인자1 > 인자2인 경우 : ZF = 0, CF = 0
인자1 = 인자2인 경우 : ZF = 1, CF = 0
인자1 < 인자2인 경우 : ZF = 0, CF = 1
JE (JUMP IF EQUAL) : ZF == 0 인 경우 분기
JNZ (JUMP IF NOT EQUAL) : 같지 않으면 (즉, ZF가 0이라면) 분기
리버싱을 하면서 SS와 DS의 차이가 계속 궁금해서 찾아보았는데
SS는 스택영역을 나타내는 스택 세그먼트이고 DS는 데이터영역을 나타내는 데이터 세그먼트이다.
스택 세그먼트는 스택 메모리 영역의 시작 주소를 가리킵니다.
데이터 세그먼트는 전역 변수와 정적 데이터를 저장하는 메모리 영역의 시작 주소를 가리킵니다.
스택은 일시적인 연산과 저장을 위한것이고 데이터 세그먼트는 프로그램 내의 전체적인 데이터를 저장하는것으로 보면 될 것 같습니다.