리버스 엔지니어링 스터디 3편. 인라인 코드 패치(Inline Code Patch)
[리버스 엔지니어링 스터디]
[이 글은 저자인 이승원씨가 작성한 '리버싱 핵심원리: 악성 코드 분석가의 리버싱 이야기'를 참고하여 쓰여졌습니다.]
코드 케이브를 삽입 후 프로그램을 패치한다!
인라인 코드 패치(Inline Code Patch)
오늘은 인라인 코드 패치(Inline Code Patch)에 대해서 공부해보도록 하겠습니다. 인라인 코드 패치란 원하는 위치에 있는 코드를 직접 패치하기 어려울 때 코드 케이브(Code Cave)라고 하는 패치 코드를 삽입한 후 실행하여 프로그램을 패치시키는 기법이라 합니다. 주로 패킹 혹은 암호화된 프로그램은 EP(Entry Point)에서 OEP(Original Entry Point) 코드를 복호화 시킨 뒤, 복호화 된 OEP 부분으로 이동합니다. 만약, 우리가 패치하려는 코드가 암호화된 OEP 영역에 존재한다면 직접적인 패치가 곤란할 것입니다. 복호화 과정을 거치면서 직접 패치한 코드를 전혀 다른 코드로 복호화 하기 때문입니다.
여기서 앞서 배우게될 코드 케이브란 것을 설치하게 된다면 어떨까요? EP 코드에 있는 복호화 과정을 거친 뒤 JMP 명령을 수정하여 코드 케이브로 이동하게 만듭니다. 그 후에, OEP는 이미 복호화 과정을 거친 상태이므로 코드 케이브 내의 패치 코드를 통해 간접적인 수정이 가능합니다. 코드 케이브의 패치 코드가 끝나면 복호화된 OEP 코드 부분으로 다시 이동하게 됩니다. 위 그림처럼, 코드 케이브는 사용되지 않는 메모리 영역을 통해 임의의 코드를 삽입할 수 있는 곳을 말합니다. 간단하게, 코드 케이브란 함수를 호출하여 간접적인 수정을 하는 것이라고 이해를 하셔도 됩니다. 오히려, 그러는 편이 이해가 더 쉬울지도 모릅니다. 위 그림과 같이 코드 케이브를 설치하면, 프로그램이 실행될 때마다 프로세스 메모리의 코드를 패치하여 그때그때 처리하기 때문에 이 기법을 인라인 코드 패치라고 부릅니다. 이 글에서는 위에서 설명한 패치 기법을 통해 이중 암호화된 프로그램을 패치할 것입니다. 우선 아래의 파일을 다운로드 받아주세요.
위 파일을 다운로드 받았으면 먼저 실행시켜 보도록 하겠습니다.
프로그램을 실행시키니, 잔소리를 패치하라고 알림창이 뜨는 것을 보실 수 있습니다. 그리고 확인을 누르면 아래와 같은 창이 등장합니다.
위 창을 살펴보니, 텍스트 박스 내에 자신을 언팩(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으로 수정하여 저장하도록 합시다. 그런 뒤에 패치된 파일을 실행시키시면 우리가 원하던 결과를 얻을 수 있습니다.
'정리 > Reverse Engineering' 카테고리의 다른 글
리버스 엔지니어링 스터디 2편. 스택 프레임(Stack Frame) (4) | 2014.05.05 |
---|---|
Babylon Keygenme(바빌론 키젠미)를 크랙해보자. (3) | 2014.03.03 |
리버스 엔지니어링 스터디 1편. PE(Portable Executable) 구조 (21) | 2013.08.11 |
Abex Crack-Me 1번문제. (5) | 2012.08.16 |