HHack 2024. 8. 30. 18:31
반응형

6.1 abex' crack me #1

디버깅을 시작하기 전에 먼저 파일을 실행시켜 어떤 프로그램인지 살펴보겠습니다. abexcm1-voiees.exe를 실행해봅니다.

그림 6.1 프로그램 실행
그림 6.2 메시지 박스 출력

 

 

 

6.1.1 Start Debugging

x64dbg를 실행시켜 파일의 디스어셈 코드를 살펴봅니다.

그림 6.3 EP 코드

디버거로 열어보니 EP코드가 매우 짧습니다. abexcm1-voiees.exe 파일은 어셈블리 언어로 만들어져 이전 HelloWorld.exe 파일과 달리 Stub Code가 없기 때문입니다.

6.1.2 코드 분석

기본적인 x32dbg로 열면 Win32 API의 주석이 상세하게 나오지 않습니다. xAnalyzer 플러그인을 사용하면 좀 더 상세한 분석을 해줘 Win32 API 코드를 이해하는데 도움이 됩니다. xAnalyzer 플러그인을 사용해 Win32 API 코드를 살펴보겠습니다.

그림 6.4 xAnalyzer플러그인 사용

MessageBox("Make me think your HD is a CD-Rom.")
GetDriveType("C:\\")
...
MessageBox("Nah... This is not a CD-ROM Drive!")
MessageBox("OK, I really think that your HD is a CD-ROM! :p")
ExitProcess()

GetDriveType() API로 C드라이브 타입을 얻어오는데 이걸 조작해 "OK, I really think that your HD is a CD-ROM! :p" 메시지 박스가 출력되게 하는 문제였습니다.

 

라인별로 상세하게 분석해보도록 하겠습니다.

[MessageBoxA() 호출]
00401000    PUSH 0                        ; UINT uType = MB_OK
00401002    PUSH 00204000                 ; LPCSTR lpCaption = "abex' 1st crackme"
00401007    PUSH 12204000                 ; LPCSTR lpText = "Make me think your HD is a CD-Rom."
0040100C    PUSH 0                        ; HWND hWnd = NULL
0040100E    CALL <JMP.&MessageBoxA>       ; Call MessageBoxA function
                                          ; 함수 내부에서 ESI = FFFFFFFF로 세팅됩니다.
										  
                                          
[GetDriveTypeA() 호출]
00401013    PUSH 00402094                 ; LPCSTR lpRootPathName = "c:\\"
00401018    CALL <JMP.&GetDriveTypeA>     ; Call GetDriveTypeA function
                                          ; 리턴 값(EAX)은 3(DRIVE_FIXED) 입니다.
                                          
0040101D    INC ESI                       ; Increment ESI (ESI = 0)
0040101E    DEC EAX                       ; Decrement EAX (EAX = 2)
0040101F    JMP SHORT 00401021            ; Short jump to address 00401021 (의미 없는 JMP 명령, garbaged code)
00401021    INC ESI                       ; Increment ESI (ESI = 1)
00401022    INC ESI                       ; Increment ESI (ESI = 2)
00401023    DEC EAX                       ; Decrement EAX (EAX = 1)

[조건 분기]
00401024    CMP EAX, ESI                  ; Compare EAX with ESI (EAX(1) 과 ESI(2)를 비교)
00401026    JE abexcm1-voiees.0040103D    ; JE(Jump if equal) 조건분기 명령
                                          ; 두 값이 같으면 40103D로 점프
                                          ; 다르면 그냥 밑(401028)으로 진행
                                          ; 40103D 주소는 제작자가 원하는 메세지 박스 출력 코드
                                          
[실패 MessageBoxA() 호출]
00401028    PUSH 0                        ; UINT uType = MB_OK
0040102A    PUSH 35204000                 ; LPCSTR lpCaption = "Error"
0040102F    PUSH 3B204000                 ; LPCSTR lpText = "Nah... This is not a CD-ROM Drive!"
00401034    PUSH 0                        ; HWND hWnd = NULL
00401036    CALL <JMP.&MessageBoxA>       ; Call MessageBoxA function
0040103B    JMP abexcm1-voiees.00401050   ; Unconditional jump to address 00401050

[성공 MessageBoxA() 호출]
00401040    PUSH 0                        ; UINT uType = MB_OK
00401042    PUSH 5E204000                 ; LPCSTR lpCaption = "YEAH!"
00401047    PUSH 64204000                 ; LPCSTR lpText = "Ok, I really think that your HD is a 
                                          ; CD-ROM! :p"
0040104C    PUSH 0                        ; HWND hWnd = NULL
0040104E    CALL <JMP.&MessageBoxA>       ; Call MessageBoxA function

[프로세스 종료]
00401053    JMP <JMP.&ExitProcess>        ; Jump to ExitProcess
00401055    JMP <JMP.&GetDriveTypeA>      ; Jump to GetDriveTypeA
0040105A    JMP <JMP.&MessageBoxA>        ; Jump to MessageBoxA
0040105F    ADD BYTE PTR DS:[EAX],AL      ; Add AL to the byte at memory location [EAX]
00401061    ADD BYTE PTR DS:[EAX],AL      ; Add AL to the byte at memory location [EAX]

 

※ 참고 - MessageBoxA()와 GetDriveTypeA() API 원형 살펴보기

1. MessageBoxA()

int MessageBoxA(
  [in, optional] HWND   hWnd,
  [in, optional] LPCSTR lpText,
  [in, optional] LPCSTR lpCaption,
  [in]           UINT   uType
);

(1) [in, optional] hWnd

  • 형식: HWND
  • 만들 메시지 상자의 소유자 창에 대한 핸들입니다. 이 매개 변수가 NULL이면 메시지 상자에 소유자 창이 없습니다.

(2) [in, optional] lpText

  • 형식: LPCTSTR
  • 표시할 메시지입니다. 문자열이 둘 이상의 줄로 구성된 경우 각 줄 사이에 캐리지 리턴 및/또는 줄 바꿈 문자를 사용하여 줄을 구분할 수 있습니다.

(3) [in, optional] lpCaption

  • 형식: LPCTSTR
  • 대화 상자 제목입니다. 이 매개 변수가 NULL이면 기본 제목은 Error입니다.

(4) [in] uType

  • 형식: UINT
  • 대화 상자의 내용 및 동작입니다. 이 매개 변수는 다음 플래그 그룹의 플래그 조합일 수 있습니다.
  • 메시지 상자에 표시되는 단추를 나타내려면 지정된 값 중 하나를 지정합니다.

2. GetDriveTypeA()

UINT GetDriveTypeA(
  [in, optional] LPCSTR lpRootPathName
);

(1) [in, optional] lpRootPathName

드라이브의 루트 디렉터리입니다. 후행 백슬래시가 필요합니다. 이 매개 변수가 NULL이면 함수는 현재 디렉터리의 루트를 사용합니다.

 

3. [in], [optional]

 

  • [in]:
    • 이 키워드는 매개변수가 입력으로 사용된다는 것을 나타냅니다. 즉, 이 매개변수는 함수에 값을 전달하기 위해 사용되며, 함수 내부에서 이 값을 수정하지 않습니다.
    • 예를 들어, MessageBoxA 함수에서 hWnd, lpText, lpCaption, uType는 모두 함수가 호출될 때 외부에서 제공하는 값을 의미합니다.
  • [optional]:
    • 이 키워드는 매개변수가 선택 사항임을 나타냅니다. 즉, 매개변수를 제공하지 않아도 된다는 의미입니다.
    • C/C++ 함수에서 선택적 매개변수는 기본적으로 NULL 또는 함수에 정의된 기본값을 사용할 수 있습니다.
    • MessageBoxA 함수에서 hWnd는 선택적 매개변수입니다. hWnd가 NULL이면, 메세지 박스는 소유자가 없는 것으로 간주됩니다.

6.2 크랙

이제 코드를 패치하여 프로그램을 크랙해보도록 하겠습니다. 401026 주소 명령어를 'Assemble' 기능[Space]을 이용하여 JMP 0040103D 명령어로 변경해보도록 하겠습니다. 즉, 조건 분기 명령어를 무조건 성공 메세지 출력하는 부분으로 점프하는 방법입니다.

그림 6.5 Assemble 명령

 

6.3 스택에 파라미터를 전달하는 방법

이번 챕터에서 가장 중요한 개념을 설명하면 함수 호출 시 스택에 파라미터를 전달하는 방법입니다.

아래의 00401000 ~ 0040100E 주소 사이의 명령어를 보면 MessageBoxA() 함수를 호출하기 전에 4번의 PUSH 명령어를 사용하여 필요한 파라미터를 역순으로 입력하고 있습니다.

00401000    PUSH 0                        ; UINT uType = MB_OK
00401002    PUSH 00204000                 ; LPCSTR lpCaption = "abex' 1st crackme"
00401007    PUSH 12204000                 ; LPCSTR lpText = "Make me think your HD is a CD-Rom."
0040100C    PUSH 0                        ; HWND hWnd = NULL
0040100E    CALL <JMP.&MessageBoxA>       ; Call MessageBoxA function

위 코드를 C언어로 번역하면 아래와 같습니다.

MessageBoxA(NULL, "Make me think your HD is a CD-Rom.", "abex' 1st crackme", MB_OK|MB_APPLMODAL);

실제 C언어 소스코드에서 함수에 넘기는 파라미터의 순서가 어셈블리에서는 역순으로 넘어간다는 것을 알아두면 됩니다.

더보기

즉, 스택은 FILO 구조이기 때문에 파라미터를 역순으로 넣어주면 받는 쪽 (MessageBoxA() 함수 내부)에서는 올바른 순서로 꺼낼 수 있습니다.

디버거를 통해 Stack을 확인해보겠습니다. 두 개의 디버거는 서로 다른 스택창을 보여주니 참고 바랍니다.

그림 6.5 ollydbg의 스택창

 

그림 6.6 x32dbg의 스택창

x86 환경에서는 스택은 아래로 자라기 때문에(주소가 높은곳에서 낮은곳으로 할당) 디버거에서 스택을 보면 MessageBoxA() 함수의 첫 번째 파라미터가 스택의 제일 위에 보이고 마지막 파라미터가 아래에 쌓인다.

0019FF64    00000000                         ; HWND hWnd = NULL
0019FF68    00402012                         ; LPCSTR lpText = "Make me think your HD is a CD-Rom."
0019FF6C    00402000                         ; LPCSTR lpCaption = "abex' 1st crackme"
0019FF64    00000000                         ; UINT uType = MB_OK

따라서 MessageBoxA() 함수 내부에서는 스택에서 파라미터를 하나씩 꺼낼 때 스택의 FILO 구조에 따라 첫 번째 파라미터부터 꺼낼 수 있게 됩니다.

※ 참고 - Stack에 높은 주소부터 할당하는 이유

1. 메모리 효율성

스택은 보통 함수 호출 시 매개변수, 리턴 주소, 로컬 변수 등을 저장하는 데 사용됩니다. 프로그램의 코드와 전역 변수, 정적 데이터는 일반적으로 낮은 주소 영역에 위치합니다. 스택이 높은 주소에서 낮은 주소로 확장되면, 힙 메모리 영역과 겹치지 않고 두 영역이 각각 독립적으로 확장될 수 있는 구조가 됩니다. 힙은 동적으로 할당되는 메모리로, 낮은 주소에서 높은 주소로 확장되며, 이 구조를 통해 메모리 효율성을 극대화할 수 있습니다. 따라서, 스택과 힙이 서로 반대 방향으로 확장되어 두 메모리 영역이 최대한 활용될 수 있습니다.

2. 함수 호출과 리턴 주소 관리

스택이 높은 주소에서 낮은 주소로 확장되면 함수 호출이 이루어질 때마다 새로운 스택 프레임이 생성되며, 함수 호출 직후의 리턴 주소가 스택에 저장됩니다. 이 방식은 함수 호출과 리턴 시, 이전 스택 프레임으로 쉽게 돌아갈 수 있게 해줍니다. 만약 스택이 반대로 낮은 주소에서 높은 주소로 확장되었다면, 이전 스택 프레임의 리턴 주소를 관리하는 것이 더 복잡해질 수 있습니다.

3. CPU 아키텍처 설계

많은 CPU 아키텍처는 스택을 위한 특별한 명령어들을 가지고 있으며, 이러한 명령어들은 스택이 높은 주소에서 낮은 주소로 확장되는 것을 전제로 설계되었습니다. 예를 들어, x86 아키텍처에서는 PUSH 명령어가 데이터를 스택에 푸시할 때 스택 포인터를 감소시키고, POP 명령어가 데이터를 팝할 때 스택 포인터를 증가시킵니다. 이 명령어들은 스택이 높은 주소에서 낮은 주소로 확장될 것을 예상하고 설계되었기 때문에, 이러한 메모리 관리 방식이 자연스럽게 표준이 되었습니다.

4. 메모리 보호와 오류 감지

스택이 높은 주소에서 낮은 주소로 확장될 때, 스택 오버플로우가 발생하면 스택 영역의 하단에 있는 데이터 영역(전역 변수나 프로그램 코드 등)에 접근하려고 시도할 수 있습니다. 이로 인해 메모리 보호 기법을 활용하여 프로그램 오류를 쉽게 감지하고, 프로그램의 안정성을 유지할 수 있습니다.

 

이러한 이유들 때문에 스택이 높은 메모리 주소에서 낮은 메모리 주소 방향으로 확장되도록 설계되었습니다. 이러한 설계는 CPU 아키텍처, 운영 체제, 컴파일러의 동작 방식과 맞물려 효율적인 메모리 관리와 오류 방지를 가능하게 합니다.

스택과 힙에 데이터가 쌓이는 과정

 

반응형