[리버스 엔지니어링 스터디]

[이 글은 저자인 이승원씨가 작성한 '리버싱 핵심원리: 악성 코드 분석가의 리버싱 이야기'를 참고하여 쓰여졌습니다.]


코드 케이브를 삽입 후 프로그램을 패치한다!

인라인 코드 패치(Inline Code Patch)




오늘은 인라인 코드 패치(Inline Code Patch)에 대해서 공부해보도록 하겠습니다. 인라인 코드 패치란 원하는 위치에 있는 코드를 직접 패치하기 어려울 때 코드 케이브(Code Cave)라고 하는 패치 코드를 삽입한 후 실행하여 프로그램을 패치시키는 기법이라 합니다. 주로 패킹 혹은 암호화된 프로그램은 EP(Entry Point)에서 OEP(Original Entry Point) 코드를 복호화 시킨 뒤, 복호화 된 OEP 부분으로 이동합니다. 만약, 우리가 패치하려는 코드가 암호화된 OEP 영역에 존재한다면 직접적인 패치가 곤란할 것입니다. 복호화 과정을 거치면서 직접 패치한 코드를 전혀 다른 코드로 복호화 하기 때문입니다.

여기서 앞서 배우게될 코드 케이브란 것을 설치하게 된다면 어떨까요? EP 코드에 있는 복호화 과정을 거친 뒤 JMP 명령을 수정하여 코드 케이브로 이동하게 만듭니다. 그 후에, OEP는 이미 복호화 과정을 거친 상태이므로 코드 케이브 내의 패치 코드를 통해 간접적인 수정이 가능합니다. 코드 케이브의 패치 코드가 끝나면 복호화된 OEP 코드 부분으로 다시 이동하게 됩니다. 위 그림처럼, 코드 케이브는 사용되지 않는 메모리 영역을 통해 임의의 코드를 삽입할 수 있는 곳을 말합니다. 간단하게, 코드 케이브란 함수를 호출하여 간접적인 수정을 하는 것이라고 이해를 하셔도 됩니다. 오히려, 그러는 편이 이해가 더 쉬울지도 모릅니다. 위 그림과 같이 코드 케이브를 설치하면, 프로그램이 실행될 때마다 프로세스 메모리의 코드를 패치하여 그때그때 처리하기 때문에 이 기법을 인라인 코드 패치라고 부릅니다. 이 글에서는 위에서 설명한 패치 기법을 통해 이중 암호화된 프로그램을 패치할 것입니다. 우선 아래의 파일을 다운로드 받아주세요.


unpackme#1.aC.exe


위 파일을 다운로드 받았으면 먼저 실행시켜 보도록 하겠습니다.



프로그램을 실행시키니, 잔소리를 패치하라고 알림창이 뜨는 것을 보실 수 있습니다. 그리고 확인을 누르면 아래와 같은 창이 등장합니다.



위 창을 살펴보니, 텍스트 박스 내에 자신을 언팩(unpack) 해달라는 문자열을 보실 수 있습니다. 우선은 올리 디버거를 통해 이 파일을 살펴보도록 하겠습니다. 먼저 EP 코드 부분을 살펴보도록 합시다.

00401000 unpackme.<mo>/$  PUSHAD
00401001                  CALL 004010E9
00401006                  RETN

상당히 간단하죠? PUSHAD를 통해 모든 레지스터의 값을 스택에 올립니다. 그리고 4010E9 함수를 호출하고 있습니다. 우리가 원하는건 알림창 내에 있는 잔소리(NAG)를 패치하는 것이기 때문에 원래대로라면 문자열의 위치를 통해 손쉽게 패치할 수 있을것 같으나 위에서 말씀드린대로 모든 문자열이 암호화 되어 찾기도, 변경하기도 힘들기 때문에 그냥 4010E9 함수 내부로 따라가도록 하겠습니다. 

004010E9              MOV EAX,004010F5
004010EE              PUSH EAX               ;  kernel32.BaseThreadInitThunk
004010EF              CALL 0040109B

위에선 4010F5를 EAX에 저장하고, EAX를 스택에 올린 뒤에 함수 40109B를 호출하고 있습니다. 40109B로 F7(Step Into)를 통해 계속 진행해보도록 하겠습니다.

0040109B              PUSH EAX                          ;  unpackme.004010F5
0040109C              MOV EBX,EAX                       ;  unpackme.004010F5
0040109E              MOV ECX,154
004010A3              XOR BYTE PTR DS:[EBX],44
004010A6              SUB ECX,1
004010A9              INC EBX                           ;  unpackme.004010FA
004010AA              CMP ECX,0
004010AD              JNZ SHORT 004010A3

여기서 유심히 보아야 할 부분이 4010A3~4010AD 부분인데, 40109C에서 EBX에다 4010F5를 저장하고, ECX에다 154를 넣는 것을 보실 수 있습니다. 4010A3~4010AD 복호화 루프에서 ECX가 0이 될때까지 1씩 감소시키며, EBX의 값을 1씩 증가시키는 것을 보실 수 있습니다. 여기서 EBX가 1씩 증가되며, 1바이트 정도 읽어온 뒤에 이 값과 44에다 XOR 연산을 취합니다. 즉, XOR 명령으로 복호화가 진행된다는 것을 보실 수 있습니다. ECX가 0이 되는 순간, 루프를 벗어나 4010AF로 계속 진행합니다.

004010AF              PUSH EAX                       ;  unpackme.004010F5
004010B0              CALL 004010BD

보시는 바와 같이 EAX의 값인 4010F5를 스택에 올리고, 4010BD 함수를 호출합니다. 4010BD 쪽을 보도록 합시다.

004010BD              PUSH EAX                          ; unpackme.004010F5
004010BE              MOV EBX,00401007
004010C3              MOV ECX,7F
004010C8              XOR BYTE PTR DS:[EBX],7
004010CB              SUB ECX,1
004010CE              INC EBX                           ; unpackme.00401249
004010CF              CMP ECX,0
004010D2              JNZ SHORT 004010C8
004010D4              MOV EBX,EAX                       ; unpackme.004010F5
004010D6              MOV ECX,154
004010DB              XOR BYTE PTR DS:[EBX],11
004010DE              SUB ECX,1
004010E1              INC EBX                           ; unpackme.00401249
004010E2              CMP ECX,0
004010E5              JNZ SHORT 004010DB
004010E7              POP EAX                           ; unpackme.004010F5
004010E8              RETN

위 어셈블리 코드에서 두개의 복호화 루프를 보실 수 있는데, 하나는 4010C8~4010D2, 또 하나는 4010DB~4010E5 부분을 주목해주세요. 아까 보았던 복호화 루프와 같은 구조입니다. 4010C8 부터 시작하는 복호화 루프는 401007 부터 401085 영역까지 복호화를 진행하고, 4010DB 부터 시작하는 복호화 루프는 4010F5~401248 까지를 복호화 시키게 됩니다. 이중으로 암호화 되어있다는 사실을 알 수 있으며, 4010BD 함수의 호출이 완료되면 4010B5로 돌아가게 됩니다.

004010B5              PUSH EAX                          ;  unpackme.004010F5
004010B6              CALL 00401039

다시 스택에 EAX의 값인 4010F5를 올려두는 부분이구요, 함수 401039를 호출합니다. 다시 내부로 진입합시다.

401039 내부를 돌아다니시다 보면 크게 두 파트로 어셈블리 코드를 나눌 수 있습니다. 먼저 401039~40104F 부분을 보도록 하겠습니다.

00401039              PUSH EAX                          ;  unpackme.004010F5
0040103A              MOV EBX,EAX                       ;  unpackme.004010F5
0040103C              MOV ECX,154
00401041              MOV EDX,0
00401046              ADD EDX,DWORD PTR DS:[EBX]
00401048              SUB ECX,1
0040104B              INC EBX                           ;  unpackme.00401249
0040104C              CMP ECX,0
0040104F              JNZ SHORT 00401046

먼저 이 부분은 EBX가 4010F5로, EDX를 먼저 0으로 값을 덮어씌우고 4010F5~401248에서 순차적으로 4바이트 단위로 값을 읽어온 뒤 ADD(덧셈) 연산을 통해 누적시킵니다. 이어서 401062~401083 부분을 봐봅시다.

00401062         CMP EDX,31EB8DB0
00401068         JE SHORT 00401083
0040106A         PUSH 30
0040106C         PUSH 00401032   ;  ASCII "Error:"
00401071         PUSH 00401009   ;  ASCII "CrC of this file has been modified !!!"
00401076         PUSH 0
00401078         CALL 00401262
0040107D         PUSH EAX        ;  unpackme.004010F5
0040107E         CALL 00401274
00401083         JMP 0040121E

EDX의 값과 31EB8DB0을 서로 비교하고 있습니다. 만약에 값이 서로 같으면 JE로 인해 401083으로 점프하고, 그렇지 않을 경우 40106A로 넘어간 뒤에 에러 알림창을 띄우게 됩니다. 여기서 EDX에 저장된 값은 CRC 체크섬 값이며 이는 오류검증을 위한 부분이라고 생각할 수 있습니다. 만약 코드가 변조되었을 경우 EDX의 값은 31EB8DB0 값과 달라지기 때문에 파일이 변조되었다는 에러 알림창을 보실 수 있습니다. 다시 넘어와서, 401083을 보시면 40121E로 점프하는 것을 보실 수 있습니다. 한번 40121E으로 넘어가보도록 하겠습니다.

0040121E      PUSH 0
0040121E      PUSH 0                                ; /pModule = NULL
00401220      CALL <JMP.&kernel32.GetModuleHandleA> ; \GetModuleHandleA
00401225      MOV DWORD PTR DS:[403018],EAX         ;  unpackme.00401280
0040122A      PUSH 0                                ; /lParam = NULL
0040122C      PUSH 004010F5                         ; |DlgProc = unpackme.004010F5
00401231      PUSH 0                                ; |hOwner = NULL
00401233      PUSH 00403024                         ; |pTemplate = "TESTWIN"
00401238      PUSH DWORD PTR DS:[403018]            ; |hInst = NULL
0040123E      CALL <JMP.&user32.DialogBoxParamA>    ; \DialogBoxParamA
00401243      PUSH EAX                              ; /ExitCode = 401280
00401244      CALL <JMP.&kernel32.ExitProcess>      ; \ExitProcess

드디어 OEP 코드가 등장했습니다. 40121E 부분부터는 OEP 코드이며, 위 어셈블리 코드만 보았을 경우에는 GetModuleHandleA, DialogBoxParamA, ExitProcess 이렇게 3개의 API 함수가 호출되었음을 알 수 있습니다. 유심히 보셔야 할 부분이 DialogBoxParamA 함수를 호출하는 부분인데, 이 부분은 다이얼로그를 실행시키는 부분입니다. MSDN을 확인해보면 hInstance, lpTemplateName, hWndParent, lpDialogFunc, dwInitParam 이렇게 매개변수 5개를 받는다는 사실을 알 수 있고, 이 중 lpDialogFunc는 다이얼로그 박스 프로시저를 가리키는 포인터, 즉 주소를 의미합니다. 함수 호출시 매개변수는 역순으로 스택에 올라가게 되니, 40122C 주소의 4010F5 값이 lpDialogFunc의 값임을 알 수 있습니다. 바로 4010F5를 확인해보도록 하겠습니다.

004010F5      PUSH EBP
004010F6      MOV EBP,ESP
004010F8      ADD ESP,-40
004010FB      CMP DWORD PTR SS:[EBP+C],110
00401102      JNZ 004011D0
00401108      JMP SHORT 00401121
0040110A      ASCII "You must unpack "
0040111A      ASCII "me !!!",0
00401121      JMP SHORT 0040113F
00401123      ASCII "You must patch t"
00401133      ASCII "his NAG !!!",0
0040113F      JMP SHORT 00401165
00401141      ASCII "<<< Ap0x / Patch"
00401151      ASCII " & Unpack Me #1 "
00401161      ASCII ">>>",0

여기서 주목해야 할 부분은 40110A~401161이 되겠습니다. 우리가 패치하여야 될 문자열이 저기에 보이네요. 더 아래 부분을 보도록 합시다.

004011A5      PUSH 0040110A                      ; /Text = "You must unpack me !!!"
004011AA      PUSH 64                            ; |ControlID = 64 (100.)
004011AC      PUSH DWORD PTR SS:[EBP+8]          ; |hWnd = 7FFD8000
004011AF      CALL <JMP.&user32.SetDlgItemTextA> ; \SetDlgItemTextA
004011B4      PUSH 40                            ; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
004011B6      PUSH 00401141                      ; |Title = "<<< Ap0x / Patch & Unpack Me #1 >>>"
004011BB      PUSH 00401123                      ; |Text = "You must patch this NAG !!!"
004011C0      PUSH DWORD PTR SS:[EBP+8]          ; |hOwner = 7FFD8000
004011C3      CALL <JMP.&user32.MessageBoxA>     ; \MessageBoxA

위 부분에서는 아까 본 문자열을 매개변수로 전달하여 "You must patch this NAG !!!"라는 메시지 박스를 띄우고, 텍스트 박스 내에 "You must unpack me !!!"라고 설정합니다. 이제 패치해야 할 부분을 알아냈고, 흐름도 어느정도 보았으니 이제는 코드 케이브를 내부에 설치하여 문자열을 간접적으로 패치해보도록 하겠습니다. 이 코드 케이브는 파일의 빈 영역이나 마지막 섹션을 확장하여 설치하거나, 새로운 섹션을 추가하여 설치를 할 수 있는데 패치 코드가 얼마 안되므로 파일의 빈 영역을 통해서 코드 케이브를 설치하도록 하겠습니다. PEView 또는 Stud PE 같은 분석 도구를 이용하여 첫번째 섹션 헤더인 .text 섹션 헤더를 살펴보도록 하겠습니다.



위 사진에서 PointerToRawData를 보니 .text 섹션은 파일에서 400에서 시작하고, SizeOfRawData를 통해 .text 섹션이 파일에서 차지하는 크기가 400정도 된다는 것을 알 수 있습니다. 또한, VirtualSize를 통해서 280 크기만 메모리에 로딩한다는 사실을 알 수 있습니다. (실제로는 SectionAlignment의 배수 단위로 확장되기 때문에 1000이 됩니다.) 한번 헥스 에디터를 통해서 그 공간을 직접 확인해보도록 합시다.



위와 보시는 것과 같이 680~800은 사용되지 않는 빈 공간(NULL Padding)이며, 이곳에 코드 케이브를 설치하도록 하겠습니다. 우선은 올리 디버거로 다시 돌아가서, 이 빈 공간에 해당하는 메모리 영역으로 이동해보도록 하겠습니다. 가상 주소(VA)는 ImageBase에서 RVA를 더한 값이므로, ImageBase(400000) + RVA(1000) = VA(401000)이란 결과값을 얻을 수 있으며 VirtualSize까지 고려하게 된다면 401280부터 401400이 빈 공간에 해당하는 영역이라고 볼 수 있겠습니다. 한번 401280으로 가보도록 하겠습니다.

00401280      DB 00
00401281      DB 00

확인해보니 401280 부터 빈 영역이 등장하기 시작합니다. 여기서 코드 케이브를 설치하도록 하겠습니다. 401280에는 아래와 같이 패치 코드를 삽입하였습니다.

00401280      MOV ECX,11
00401285      MOV ESI,004012A8                     ;  ASCII "blog.eairship.kr"
0040128A      MOV EDI,00401123                     ;  ASCII "You must patch this NAG !!!"
0040128F      REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
00401291      MOV ECX,7
00401296      MOV ESI,004012B9                     ;  ASCII "su6net"
0040129B      MOV EDI,0040110A                     ;  ASCII "You must unpack me !!!"
004012A0      REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
004012A2      JMP 0040121E
004012A7      DB 00
004012A8      ASCII "blog.eairship.kr"
004012B8      ASCII 0
004012B9      ASCII "su6net",0

위 코드에서 ECX 레지스터에는 NULL 까지 고려한 문자열의 길이가 들어가게 되며, ESI에는 패치에 쓰일 문자열 주소가 들어가고, EDI에는 패치하려는 문자열 주소가 들어갑니다. 그리고 REP 명령을 통해 ECX 레지스터의 값만큼 'MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI]' 명령을 반복합니다. 이때, EDI 값과 ESI 값은 1씩 증가하면서 계속 변경됩니다. 마지막으로 4012A2 부분은 OEP로 점프하는 부분이 있습니다. 우선은 변경된 내용을 저장합시다. 자, 이제 코드 케이브를 설치했으니 OEP로 들어가기 전, 코드 케이브로 점프하여 원하는 문자열로 패치한 뒤 OEP로 넘어가도록 하겠습니다. 직접 디버거를 통해 401083에 있는 어셈블리 코드를 수정해도 되지만, 이 부분은 원래부터 암호화된 영역이였기 때문에 헥스 에디터를 통해서 수정하도록 하겠습니다. 옵셋 483(.text 섹션의 PointerToRawData가 400이므로 파일에서는 400에서 시작하며, 401083에서 ImageBase(400000), .text 섹션의 RVA(1000)을 빼면 83이 나옵니다. 400과 83을 더하면 483) 으로 이동하여 헥스 코드를 살펴보도록 합시다.



401283 부분은 원래 XOR 명령을 통해 암호화 되어있던 영역이므로, 헥스 코드를 수정할때는 XOR 7로 암호화하여 써넣어야 합니다. 

00401083      /E9 F8010000   JMP 00401280

위 401083에 해당하는 JMP 명령문의 Instruction은 E9 F8010000 입니다. 그리고 이걸 XOR 7로 암호화 하게 된다면, E9 F8 01 -> EE FF 06이 될 것입니다. 그럼 EE 91 06을 EE FF 06으로 수정하여 저장하도록 합시다. 그런 뒤에 패치된 파일을 실행시키시면 우리가 원하던 결과를 얻을 수 있습니다.