1. 개요

2. 패킹된 바이너리

3. 언패킹

.... 3.0 No option

.... 3.1 API Redirection

.... 3.2 Code Redirection

.... 3.3 Remove OEP

.... 3.4 Debug Blocker

.... 3.5 etc

4. OEP 찾기





1. 개요

  PEspin(http://pespin.w.interiowo.pl/)은 전형적인 프로텍터로 보인다. Themida 같은 코드 가상화 기술은 사용하지 않지만 상당히 많은 안티 디버깅 기술이 사용된다. 물론 압축 알고리즘을 통해 보호 뿐만 아니라 패킹의 기능도 갖는다. 참고로 실행 압축 알고리즘은 aPLib을 사용한다.


  다음은 옵션들이다. 여기에서는 옵션 없이 처리한 바이너리부터 분석한 후에 각각의 옵션을 사용한 바이너리를 분석하여 어떤 방식으로 구현되었는지를 확인하고 정리할 것이다.


- Anti-debugger protection : [ Add Debugger detection ( Exit without any message, Display message and exit ) ]

- Protection : [ API Redirection ], [ Antidump protection ], [ Remove OEP ], [ Code redirection ], [ Debug Blocker ]

- Advanced : [ Compress resources ], [ Strip Overlays ], [ Strip .reloc section if possible ], [ Optimize MS-DOS header size ]

- Other : [ Password protection ], [ Close program after : "" 'minute ], [ Create backup file ]

- Section names : [ User name : ], [ Random known protector, packer ], [ Don't rename ]





2. 패킹된 바이너리

  이 부분은 "Section names" 옵션과 같이 설명하겠다. "Don't rename"을 사용한 경우에는 초기 섹션들 외에 .taz 섹션이 추가된 것을 볼 수 있다. 옵션 중 "Compress resources" 옵션을 사용하지 않았기 때문에 리소스 섹션(.rsrc)의 내용은 그대로이지만 나머지 섹션들은 모두 압축되어 있다. 만약 "User name" 옵션을 사용하여 이름을 짓는 경우 .rsrc 섹션명을 제외하고 .taz 섹션 포함 모든 섹션 이름이 그 이름으로 설정된다. 또한 "Random known protector, packer"로 설정한 경우 앞과 동일한 섹션들의 이름이 모두 .PELOCKnt, .ficken, .peco 등으로 변경된다.


  .taz 섹션은 일반적인 패커들처럼 언패킹 루틴과 압축된 데이터가 존재하며 동시에 Import Table이 존재한다. PEspin은 MessageBoxA(), InitCommonControls(), LoadLibraryA(), GetProcAddress()를 기본으로 임포트한다.





3. 언패킹

  PEspin은 암호화 및 복호화에 다형성 루틴을 사용한다. 이것은 하나의 바이너리를 패킹하더라도 생성될 때 마다 출력된 바이너리는 모두 다르다는 것을 의미한다. 압축된 데이터가 들어간 섹션들은 완전히 다르며 복호화 루틴 및 압축된 데이터가 저장된 .taz 섹션도 차이점이 존재한다. 복호화 루틴이 기본적으로 Self-modifying code인 면도 있다.


  여기서는 32비트 기준 작은 바이너리 두 개(하나는 calc.exe)를 대상으로 하여 분석했다. 그렇기 때문에 큰 바이너리의 경우 인지하지 못한 부분에서 차이가 있을 수 있다. 이 섹션에서는 아무런 옵션을 주지 않은 채로 생성된 바이너리를 언패킹하며 하위 섹션들에서 하나씩 옵션을 설정하여 비교하면서 분석하겠다. 참고로 프로텍터의 특징처럼 난독화 코드가 매우 많다. 기본적으로 garbage 코드를 비롯하여 "Jumping into the middle of an instruction" 또는 "Breaking Code Alignment"로 불리는 안티 디스어셈블러 기법이 사용되어 Ollydbg를 이용하여 분석하는데 많은 방해가 되었다. garbage 코드는 그렇다 치더라도 jmp 및 call을 이용하여 코드를 깨는 저 방식은 한 두개라면 감당하겠지만 저렇게 대량으로 존재하는 경우 Ollydbg를 이용하여 분석하는 것은 굉장히 힘든 일로 보인다. "Analyse Code" 즉 "Ctrl + A"도 이것들을 제대로 분석하지 못하기 때문에 어지간한 경우에는 그냥 분석을 해제하는 편이 낫다. 이것을 극복할 수 있는 방식을 최대한 찾을 것이며 없으면 만들어 보도록 시도라도 할 생각이다. 


  하나씩 옵션을 설정하면서 깨달은 것은 언패킹 루틴이 기본적으로 동일하며 옵션에 따라서 조금씩 기능이 추가되는 경우가 많았다는 점이다. 개인적으로 능력이 부족하여 대충 넘긴 부분이 특정 옵션 사용 시에 사용되어 이 때서야 깨닫는 것을 보며 확신하건데, 아마 이렇게 최종적으로 정리한 글에서도 놓친 부분이 많이 남아있을 것이다. 그렇기 때문에 이 글은 참고 용으로만 보고 정확한 분석은 직접 수행하는 것을 추천한다.



3.0 No option

  앞에서도 언급하였듯이 "Jumping into the middle of an instruction" 기법 때문에 Step Over (F8) 대신 Step Into (F7)를 이용해야 한다. 참고로 분석 대상 바이너리가 다를 수 밖에 없지만 이것을 설명하면서 주소를 적는 것이 참고하는데 많은 도움이 될 것 같아서 주소도 같이 적으려고 한다. 본인의 바이너리는 00406000이 .taz 섹션의 시작이며 EP도 이 섹션의 거의 처음 부분이다. 사족으로 EP의 앞 부분에는 Import Table이 존재한다.


  비록 Ollydbg에서는 우리의 섹션들이 모두 통합되어 보이지만 어쨌든 영역을 구분하는 것은 필요한 일이다. 참고로 왜 Ollydbg에서 통합되어 보이는지에 대한 확실한 이유는 알 수 없지만 추측은 다음과 같다. 원래 일반적인 경우 .text 섹션은 읽고 실행하기 즉 rx, .rdata 섹션은 r, .data 섹션은 rw 등과 같이 섹션 별로 접근 권한이 부여된다. 하지만 PEspin으로 패킹한 경우 전체 섹션들이 모두 rwx 권한을 갖게된다. 아마 Ollydbg는 이 섹션들이 일반적인 이름을 갖고 있지만 일반적인 권한을 갖고 있는 것이 아닌데다가 권한도 모두 같으니 한 뭉텅이로 통합하여 인식시킨 것으로 보인다.


  시작하자 마자 앞에서 언급한 "Jumping into the middle of an instruction" 기법과 garbage 코드들이 나온다. 가장 대표적인 garbage 코드는 분기문으로서 retn을 이용하는 방식이다. jmp와 call을 통한 분기 뿐만 아니라 주소를 push한 후에 retn함으로써 분기하거나 " ADD DWORD PTR SS:[ESP], 0C " 이후 " RETN " 을 이용해 분기하는 방식이 자주 사용된다.



a.

  EP는 004060D4이며 대부분 Step Into (F7)로 진행하였다. 앞에서 00406000 즉 .taz 섹션의 처음 부분은 Import Table이라고 말했다. 정확히 말해서 로드 이후 00406050에 MessageBoxA()의 주소가 들어가 있다. 이 값 즉 주소를 004084C1에 복사한다. 이 외에도 LoadLibraryA()는 00408566에, 0040856B에는 GetProcAddress()를 넣는다. 참고로 이 세 함수들은 모두 Import 하는 API들이다. 일종의 Import Table의 역할을 수행하는데 복잡하기 때문에 뒤에서 따로 정리하도록 하겠다.


  복호화 루틴이 자가 수정 코드 즉 Self-modifying code라고 했듯이 실행 중에 코드를 계속 수정해 가면서 진행한된. 이 수정 즉 복호화는 바로 다음 명령어가 해당되기도 하지만 뒤에서 나올 반복문 처럼 13A8 정도 되는 크기를 복호화하기도 한다. ECX가 13A8을 갖는 반복문인데 004085xx부터 004099xx 부근까지 복호화를 담당한다. 이후 ECX가 BA인 반복문도 있다. 이제 복호화된 부분으로 이동한다.


  참고로 ECX를 이용한 루프의 경우 간단하게 조건부 BP 즉 "Shift + F2"를 누른 후 조건으로 "ecx == 1"을 입력하고 F9 즉 실행을 누르면 해당 명령어 실행 중 ECX가 1이될 때 정지하게 된다. ECX를 이용한 루프는 이렇게 처리하기로 한다.


  다음 부분은 조금 설명이 필요하다. 패커들의 경우 대부분 LoadLibrary()와 GetProcAddress()를 임포트하며 언패킹 후 마지막으로 IAT를 복구하는데 두 함수를 사용한다. 패커는 위의 두 함수를 제외하면 API 함수들이 그다지 필요한 경우가 많이 없지만 프로텍터의 경우에는 원본 바이너리의 IAT 복구 외에도 언패킹 루틴에서 보호에 사용되는 여러 함수들이 필요할 수 있다. 하지만 대상이 어떤 함수를 사용하는지 알 수 있다는 것은 대상을 분석하는데 많은 힌트가 될 수 있으므로 직접 import 하지는 않는다. 대신 언패킹 루틴에서 사용되는 함수들도 이 두 함수를 이용해서 주소를 얻어와 사용하는 경우가 많다.


  뒷 부분에서 나올 때 마다 언급하겠지만 여타 프로텍터들처럼 PEspin도 보호를 위해 여러 함수들이 필요하다. 하지만 PEspin은 이 두 함수를 임포트하고 있어도 이 함수들을 사용하지 않는다. 즉 언패킹 루틴에 필요한 함수들 뿐만 아니라 원본 바이너리의 IAT 복구를 위한 함수들 주소도 이 두 함수를 이용하지 않는 것이다. 필요한 함수들은 Export Table을 이용하여 함수 주소를 얻어온다. 오직 LoadLibrary()만 마지막 즈음에 원본 바이너리의 임포트 테이블을 복구할 때 사용되기도 한다. 이 두 함수를 사용하여 필요한 함수들의 주소를 가져오지 않는 것은 아마 보호의 방식 중 하나로 보인다.


  Export Table을 이용하여 api 함수의 주소를 얻는 방식은 한국어로 되어있는 좋은 자료들이 존재하므로 (예를들면 [ http://ezbeat.tistory.com/283 ]) 자세히 설명하지 않겠다. 참고로 PEspin에서는 이미 LoadLibraryA() 등 kernel32.dll의 함수를 사용하기 때문에 kernel32.dll의 BaseAddress를 구하는데 PEB를 사용하지는 않는다. 그냥 LoadLibraryA()를 이용해서 BaseAddress를 구하며 이후 EAT를 이용한 방식은 비슷하다. 물론 문자열을 비교하는 부분 같이 세세한 부분은 다르지만 기본적으로 Export Table을 이용하는 메커니즘이 사용된다. LoadLibrary()와 GetProcAddress()를 이용하면 매우 간단할 것이지만 이 방식을 사용함으로 인해 익숙하지 않다면 막힐 수도 있다. 이 방식의 특징 상 차례대로 모든 API 함수 이름을 비교하면서 찾아내야 하기 때문이다. 


00408653 CMP BYTE PTR DS:[EDI+2], 69


  EDI에는 API 함수 이름 문자열의 주소가 들어가 있다. 저 명령어는 함수 이름의 세 번째 문자와 특정 알파벳을 비교한다. PEspin 개발자만의 방식으로 보인다. 이후 3번째 글자가 맞다면 나머지 부분도 비교한다.


00408738 MOV DWORD PTR DS:[EDI+1], EAX


  이 명령어는 api 함수 이름이 원하는 것과 같다면 그 주소를 아까 위에서 설명한 004084C1 이후 부분 즉 새로운 임포트 테이블에 쓰는 역할을 한다. 어쨌든 이 루틴을 벗어나는 것을 더 원할 것이다. 아래의 JE가 활성화되면 이 루틴이 종료된다. 그냥 00408740에 BP를 걸면 된다.


00408624 CMP DOWRD PTR DS:[EDI], 0

00408627 JE 00408740


  마지막으로 PEspin이 사용하는 api 함수들을 정리하겠다. 사실 여기서는 아무 옵션도 사용하지 않았기 때문에 아래의 함수들 중 몇 개만 사용된다. 옵션이 추가될 수록 사용되는 함수들이 많아질 것이다.


[ ExitProcess, VirtualProtect, Closehandle, VirtualAlloc, VirtualFree, CreateFileA, ReadFile, GetTickCount, GetModuleHandleA, CreateThread, Sleep, GetCurrentProcessId, OpenProcess, TerminateProcess, GetFileSize, GetModuleFilenameA, CreateMutexA, CreateProcessA, GetCommandLineA, GetLasterror, GetThreadContext, SetThreadContext, VirtualProtectEx, WaitForDebugEvent, ContinueDebugEvent, ReadProcessMemory, WriteProcessMemory, VirtualQueryEx ]


  계속 진행하다 00408467로 가고 또 진행하다 보면 ecx를 이용한 루프가 존재한다. 이것도 조건부 BP를 이용해 넘긴다. 



b.

  이후 다음 명령어들이 존재한다.


0040663A XCHG DWORD PTR FS:[EBX], EAX

; EBX는 0이다. 그래서 EAX에는 SEH를 가리키는 포인터가 들어간다.

0040663D LEA EBX, [EBP+403AFF]

; EBX에는 SEH 핸들러의 주소가 들어간다.

00406643 PUSH EBX

00406644 PUSH EAX

; 둘을 스택에 push함으로써 새로운 SEH 핸들러가 설치된다.

00406645 OR EDI, FFFFFFFF

00406648 OR ECX, EDI

0040664A REPE SCAS BYTE PTR ES:[EDI]

; EDI가 0xFFFFFFFF이므로 예외가 발생한다.


  위의 루틴은 뒤에서도 나오겠지만 전형적인 SEH를 이용한 안티 디버깅이다. 예외 핸들러를 설치하고 예외를 발생시키는 것이다. 우리는 간단하게 예외를 발생시키는 코드 실행 이전에 또는 이미 발생하여 Ollydbg가 아래의 패널에서 "Access violation when reading [FFFFFFFF] - Shift+Run/Step to pass exception to the program" 메시지를 내놓고 중지된 상황에서 스택 패널에 보이는 SEH 핸들러의 주소를 참조하여 핸들러에 BP를 걸고 "Shift + F9"하여 SEH 핸들러로 넘어올 수 있다. 이런 방식은 뒤에도 자주 나오는데 예외를 일으키는 방식만 다르지 메커니즘은 같다.


00406660 REP MOVS BYTE PTR ES:[EDI], BYTE DS:[ESI]


  핸들러에서는 위의 명령을 실행하여 ESI 즉 004085C0에서 EDI 즉 0040664D로 데이터를 옮긴다. 그리고 다시 위 즉 0040664D로 가서 명령어를 실행한다. 그냥 방금 사용한 SEH 핸들러를 제거하는 부분이다.



c.

  VirtualAlloc()으로 메모리를 할당한다. 명령어는 아래와 같은데 앞에서도 설명했듯이 참조하는 주소는 새로운 Import Table이다. 참고로 정확히 004084E4이다.


0040668B CALL DWORD PTR SS:[EBP+405996] ; VirtualAlloc()


  간략하게 정리해서 GetModuleFileNameA()로 이 바이너리의 파일 경로 명을 얻어와서 앞에서 할당한 메모리에 저장한다. 이후 이 경로명을 인자로 CreateFileA()를 이용해 파일의 핸들을 얻고 GetFileSize()로 크기를 얻는다. 그리고 이 크기만큼을 인자로하여 VirtualAlloc()으로 메모리를 할당한 후 ReadFile()로 파일을 읽어서 이 영역에 저장한다. 마지막으로 CloseHandle()로 핸들을 닫는다.


  읽어온 파일 자체의 데이터를 이용한 루틴이 수행된다. 뭔가 무결성 검사처럼 파일 전체 데이터를 이용하여 연산한 후 그 결과를 0040AE62에 저장한다. 이후 VirtualFree()로 할당한 두 메모리를 해제한다. 그리고 SEH 핸들러를 설치한 후 다음 명령어로 예외를 발생시킨다. 참고로 BL은 0이다.


0040684A DIV BL


  이번에 사용되는 핸들러에서는 retn을 이용해 반환하기 때문에 결국 ntdll로 들어가서 NtContinue()까지 가게된다. 예외와 관련하여 정리한 글이 이미 이 블로그에 존재하기 때문에 결과만 말하겠다. 인자로 넣은 pContext의 주소는 0019FBFC이다. 여기에 B8을 더하면 되는데 결과는 0019FCB4가 된다. 스택 패널에서 이 영역을 찾아가면 값이 0040859D인 것을 볼 수 있으며 이곳에 BP를 걸면 된다. 


  이후 계속 복호화는 수행된다. 이 부분은 조금 더 구체적인게 각 섹션 별로 주소를 EDI에 넣고 크기만큼 ECX에 넣고 복호화를 수행하는게 보인다. 참고로 아까 0040AE62에 저장한 값을 이용한다. 그리고 SEH 핸들러를 이용한 안티 디버깅이 존재한다. 조금 특이하기 때문에 설명을 더 하겠다. 인터럽트는 STI 명령어를 이용하는데 핸들러에서 핸들러의 주소를 변경한다. 이후 반환하는데 다시 이 주소로 복귀하여 STI를 실행하고 또 예외가 발생한다. 대신 이번 핸들러의 주소는 이전의 00408F8A가 아닌 변경된 00408F8F가 된다.


0040900B STI


  그리고 또 예외가 발생한다. 물론 다음 명령어 실행 이전에 핸들러 주소를 00408FDA로 변경하였으므로 여기에 BP를 건다.


00408FCF XLAT BYTE PTR DS:[EBX+AL]


  이후 00406C23부터 ECX 11D4 만큼의 자가 수정 부분을 지나 각 섹션에 대해 크기만큼 복호화를 수행한다.



d.

  예외가 발생한다.


00406C00 MOV DWORD PTR DS:[EAX], EBX


  이후 00406C25 부터 11D4만큼 자가 수정을 수행한다. 이것도 ECX를 이용한 루프문이므로 똑같이 처리한다. 다시 이곳으로 이동하여 진행한다. 조금 지나서 VirtualAlloc()을 호출한다.


004097B0 VirtualAlloc()


  여기서는 이제 마지막으로 복호화가 수행된다. 즉 이 과정을 거치고 난 후 코드 섹션 근처로 가보면 원본 바이너리와 거의 같은 형태로 복호화가 완료되었다는 것을 볼 수 있다. 조금 더 자세히 살펴보면 PUSHAD로 시작해 POPAD로 끝나는 루틴이다. 이 부분은 다른 부분과 다르게 "Analyse Code"를 이용하면 깔끔하게 분석된 것을 볼 수 있다.


004071E1 PUSHAD

...

00407288 POPAD

00407289 RETN


  이후 retn을 하면 004097FF로 이동하며 다음 명령어를 통해 할당된 메모리에서 코드 섹션으로 E00의 크기 만큼을 이동시키는 것을 볼 수 있다. 참고로 이 부분도 분석된 코드를 보면 깔끔하게 이해할 수 있다.


00409809 REP MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI]


  참고로 이 예제 바이너리에서 해당 루틴은 .text 섹션과 .rdata 섹션만 해당하는 것으로 보인다. 어쨌든 위에서 언급하였듯이 위의 명령어 수행 후 해당 섹션으로 가보면 원본 바이너리처럼 복호화되어 있는 것을 확인할 수 있다. 물론 다른 프로텍터나 패커처럼 JMP 및 CALL의 주소와 관련해서는 아직 남아있다. 이후 VirtualFree()로 할당한 메모리를 해제한다.


00409866 VirtualFree()



e.

004089E5 VirtualProtect()


  004001F0부터 528의 크기 만큼 PAGE_EXECUTE_READWRITE 권한을 부여한다. 이후 이 부근에 값들을 쓰고 이곳으로 이동하여 진행하다가 예외를 발생시킨다.


004001F7 JMP FAR FWORD PTR DS:[EDX+9090]


  핸들러의 주소인 00408A4D로 이동하여 핸들러를 지우고 GetTickCount()를 호출한다.


00408A8B GetTickCount()


  결과에 대하여 반복적으로 특정 연산을 수행하며 00406000부터 D4의 크기만큼 차례대로 결과값을 집어넣는다. 이후 자가 수정 코드를 진행하다 0040956F로 이동한다.



f.

  이제 Import Table 복구를 시작한다. 일반적인 패커들의 경우 로더가 하는 역할과 같이 LoadLibrary()로 해당 DLL에 대한 핸들을 얻어온 후 GetProcAddress()로 해당 API 함수의 주소를 얻어온 후 IAT에 주소를 복사해 넣는다. 이후 바이너리에서는 보통과 같이 IAT의 주소를 참고해 API 함수를 호출한다. 즉 로더의 역할을 대신해줄 뿐 차이점은 없는 것이다.


  하지만 PEspin의 경우에는 더 복잡하다. 프로텍터로서 언패킹 루틴에서 사용할 함수들이 많이 존재하기 때문이다. 게다가 보호의 목적인지 LoadLibrary(), GetProcAddress() 처럼 기본적인 API를 제외하고는 직접적으로 임포트 하지 않는다. 사실 개인적인 생각은 이 두 함수도 거의 사용되지 않는 것과 섹션 이름을 rename할 수 있는 기능이 있는 것으로 보아 다른 패커 및 프로텍터와 혼동시키게 하려는 목적도 있는 것 같다.


  어쨌든 초반부에 설명했 듯이 원래 임포트하던 MessageBoxA()는 주소 004084C1에 저장한다. 여기서는 사용되지 않지만 이것을 호출할 일이 있다면 이 주소를 IAT로 삼아서 함수를 호출할 것이다. 그 다음으로는 차례대로 00408566에 LoadLibraryA(), 0040856B에 GetProcAddress()를 저장한다. 다음으로는 Export Table을 이용해 ExitProcess()부터 VirtualQueryEx() 까지의 주소를 얻어와서 각각 MessageBoxA() 뒤의 위치에 저장한다. 이 주소들은 MessageBoxA()와 LoadLibraryA() 및 GetProcAddress() 사이에 저장되게 된다.


  지금까지 여러 API 함수들을 호출하였으며 모두 저 주소에 위치한 값을 주소로 사용해 API 함수들을 호출했었던 것을 알 수 있다. 이제부터는 언패킹 이후 사용되는 즉 바이너리 자체에서 실제로 사용되는 함수들을 위한 IAT를 복구할 것이다. 하지만 이것도 설명이 필요하다.


  참고로 현재 예제 바이너리는 Visual Studio 2015로 개발하여 PEspin 1.33에서는 그다지 호환되지 않는 것으로 보인다. 자세한 설명은 이 블로그의 "API Sets" 문서를 보면 될 것이다. 어쨌든 아까처럼 Export Table을 이용하여 kernel32.dll에서 임포트 하는 함수들부터 차례대로 문자열을 비교하며 주소를 얻어오는데 해당하는 경우에는 0040AE67을 시작으로 이곳에 주소를 저장한다. 즉 언패킹 이후 바이너리에서는 GetCurrentProcessId() 같은 kernel32.dll의 함수를 호출할 때 이 영역을 IAT로 생각하고 호출하게 될 것이다. 간단하게 말해서 새로운 IAT의 위치라고 할 수 있다.


  하지만 이 예제 바이너리는 PEspin 1.33에서 지원하지 않는 것으로 보이는 VCRUNTIME140.dll 등의 DLL에 위치한 함수들도 임포트한다. 이 경우 PEspin은 일반적인 바이너리처럼 00402000 영역에 해당 주소를 복구한다. 다시 말해서 패킹한 바이너리가 PEspin에서 지원되는 kernel32.dll 같은 DLL의 함수를 사용할 경우에 0040AE67에 새로운 IAT가 만들어지고 이곳이 사용된다. 지원되지 않는 DLL의 함수들을 사용할 때는 일반적인 경우처럼 원래 IAT에 함수를 복구하여 원래 IAT가 사용되는 것이다. 참고로 GetProcAddress()를 이용하지 않는 것은 앞 부분과 같지만 VCRUNTIME140.dll 같은 DLL의 핸들을 얻을 경우에는 LoadLibrary()가 사용된다. 이것을 보면 지원되는 경우에 PEspin은 LoadLibrary()와 GetProcAddress()를 임포트함에도 불구하고 전혀 사용하지 않는다는 것을 알 수 있다.


  참고할 코드 부분을 설명하겠다. 


00406F4E CMP BYTE PTR DS:[EDI], 5F


  이 부분은 구해온 API 함수 이름 문자열과 특정 알파벳을 비교하는 부분이다. 참고로 이 부분도 수도 없이 진행되기 때문에 더 참고할만한 부분을 설명한다.


00406FBB JBE SHORT 00406FF2


  이곳에 BP를 걸고 실행하다 보면 바이너리에서 사용되는 함수들이 EAX에 차례대로 보이는 것을 볼 수 있다. 어쨌든 넘기기 위해서는 0040708D의 JE 00407125가 활성화되어야 한다. 진행하다 보면 00407A84로 가고 계속 진행하다보면 다음을 볼 수 있다.


00407DA2 JMP 004012EA


  OEP로 가는 점프문이다.



3.1 API Redirection

  위의 [ f ] 과정 즉 IAT를 복구하기 이전에 먼저 VirtualAlloc()으로 메모리를 할당한다. 그리고 IAT 복구를 수행하는데 앞에서 일반적인 경우 0040AE67에 새로운 IAT가 들어가 있고 여기에 함수의 주소가 들어가 있다고 했다. API Redirection 옵션을 사용한 경우 이 주소는 할당한 메모리의 주소가 된다. 그리고 이 메모리의 그 주소에서는 API 함수의 처음 부분이 복사된다. 마지막 부분은 API의 다음 부분으로 분기하는 점프가 추가된다.


  즉 API 호출 시 할당된 메모리로 이동하며 거기서 함수의 처음 부분을 실행하고 이후 실제 함수의 다음 부분으로 분기하여 거기서 다시 실행하는 것이다. 이 예제를 이용해 설명하도록 하겠다. 참고로 "API Sets" 문서에 관련 내용이 정리되어 있다. 이 바이너리에서는 Kernel32.GetVersionExA()를 호출한다. 하지만 실제로 이 함수는 kernelbase.dll로 이동하였고 kernel32.dll에는 kernelbase.dll로 분기시켜 주는 스텁 코드가 들어있다. 즉 다음과 같다.


76FB56D0 MOV EDI, EDI

76FB56D2 PUSH EBP

76FB56D3 MOV EBP, ESP

76FB56D5 POP EBP

76FB56D6 jmp dword ptr ds:[<&api-ms-win-core-sysinfo-l1-2-1.GetVersionExA>]


  이 API를 바이너리에서 사용한 경우 새로운 IAT에는 이 함수에 대한 주소가 76FB56D0가 아닌 할당된 메모리인 001E00xx로 되어 있다. 즉 호출 시 이 주소로 가는데


001E00xx MOV EDI, EDI

001E00xx PUSH EBP

001E00xx MOV EBP, ESP

001E00xx POP EBP

001E00xx jmp 76FB56D6


  위와 같이 실제 kernel32.GetVersionExA()의 처음 부분이 (참고로 이 스텁 함수의 경우 매우 짧기 때문에 대부분이 실행되는 것처럼 보인다) 실행되고 이후 그 다음 주소인 76FB56D6으로 jmp하게 된다.



3.2 Code Redirection

  [ e ] 단계에서 004001F0 영역은 0xFF로 채워졌었다. 이 옵션을 사용하면 IAT가 완성되고 OEP로 이동하기 바로 전에 특정 데이터가 004001F0을 시작으로 해당 영역에 삽입되는 것을 볼 수 있다. 이것은 E9로 시작하는 jmp 문들이다. 다음은 0040AEA3에서 004001F0으로 166 크기의 데이터를 이동하는 명령어이다. 0040AEA3에 해당 내용을 삽입하는 부분은 분석하지 않았다.


00407C9E REP MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI]


  이후 언패킹 루틴을 종료하고 OEP로 간 후 실행해 보겠다. 실행 중 일반적인 분기문이 평소와 다르게 004001F0을 시작으로 하는 저 영역으로 가는 jmp 문으로 수정된 것을 볼 수 있다. 명령어를 실행하여 해당 영역으로 이동하면 E9를 시작으로 하는 jmp 문이 존재하는데 이 명령어들은 결국 원래 가려고 했던 곳의 주소를 가지고 있다. 이렇게 Code Redirection 옵션은 API 함수가 아닌 일반적인 분기문을 대상으로 Redirect를 수행하여 준다.



3.3 Remove OEP

  OEP는 004012EA이며 언패킹 루틴에서 OEP로 가는 마지막 분기는 다음과 같은 형태이다.


00407DA2 JMP 004012EA


  그리고 실제 바이너리의 OEP 부분은 다음과 같다.


004012EA CALL 0040155F [ 참고 : __security_init_cookie() ]

004012EF JMP 0040116E


  Remove OEP 옵션을 사용하는 경우에는 마지막 분기문이 다음과 같다.


00407D2F JMP 0040155F

00407D34 JMP 004012EF


  설명해 보자면 바로 OEP로 가지 않고 실제 OEP의 첫 코드 부분을 직접 호출한 후에 복귀하여 다시 그 다음 부분으로 가는 것이다. 실질적인 흐름은 차이가 없지만 OEP인 004012EA로 직접 가지는 않는다는 점이 Remove OEP 옵션의 의미인 것으로 보인다. 참고로 이쪽을 구현한 언패킹 루틴은 분석하지 않았다.



3.4 Debug Blocker

  이 방식은 전형적인 Debug Blocker (또는 Self-Debugging) 방식이다. 기본적인 형태와 관련해서는 많은 자료들이 있고 아직 분석하지 않아서 PEspin만의 특징을 알지 못하지만 추후에 분석한 경우 올리도록 할 것이며 여기서는 기본 구현만 알아보도록 한다. 


  앞에서 설명했듯이 [ a ] 단계에서는 이후에 수행할 영역에 대한 자가 수정 코드가 있으며 언패킹 루틴에서 사용될 함수들을 위한 새로운 Import Table을 생성한다. [ a ] 단계를 지나면 00409D86으로 간다.


  이제부터는 상당히 직관적이다. CreateMutexA()로 뮤텍스를 생성한 후 GetLastError()로 검사한다. 부모 프로세스의 경우에는 성공할 것이고 자식 프로세스의 경우에는 이미 있기 때문에 GetLastError()로 0x000000B7 즉 ERROR_ALREADY_EXISTS를 받을 것이다. 참고로 이 예제의 경우 004099C0에 저장된 URXYTLAH 문자열을 사용하며 이것은 [ a ] 단계의 마지막 루프에서 생성된다. 이후부터는 부모와 자식 프로세스 사이에서 차이가 발생한다. 


  부모 프로세스의 경우 이후 두 번의 VirtualAlloc()을 통해 메모리를 할당한다. GetModuleFileNameA()로 파일 이름을 얻어와 첫 번째 할당한 메모리에 저장하고 GetCommandLineA()으로 커맨드 라인을 받아온 후 이것들을 이용해 CreateProcessA()를 호출한다. 이 때 플래그는 DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS를 사용한다. 이후에는 WaitForDebugEvent(), ContinueDebugEvnet()를 반복적으로 호출하여 자식 프로세스와 통신한다. 부모 프로세스를 더 이상 분석하지 않았고 자식 프로세스도 아예 분석하지 않았다. 



3.5 etc

  옵션 중 눈여결 볼만한 [ Add Debugger detection ( Exit without any message, Display message and exit ) ], [ Antidump protection ], [ Close program after : "" 'minute ]는 다루지 않았다. "Antidump protection"의 경우에는 무 옵션과의 차이를 찾지 못했으며 몇 개는 굳이 시도하지 않았다.





4. OEP 찾기

  OEP를 찾기 위해서 예제에서 사용한 바이너리와 calc.exe를 비교하면서 분석해 보았다. 두 개의 바이너리를 사용하고 API Redirection 옵션을 사용해 4개의 샘플을 이용했으며 각각의 크기는 9KB, 113KB이다. 아래의 주소는 .taz 섹션의 시작 주소를 기반으로 한다.


a. 예제 바이너리 + No option

+ 00001CF3 ADD DWORD PTR SS:[ESP], 0C

+ 00001CF7 RETN

...

+ 00001DA2 JMP OEP


b. 예제 바이너리 + API Redirection 옵션

+ 00001CF3 ADD DWORD PTR SS:[ESP], 0C

+ 00001CF7 RETN

...

+ 00001DAE JMP OEP


c. calc.exe + No option

+ 00001C7D ADD DWORD PTR SS:[ESP], 0C

+ 00001C81 RETN

...

+ 00001D8E JMP OEP


d. calc.exe + API Redirection 옵션

+ 00001C7D ADD DWORD PTR SS:[ESP], 0C

+ 00001C81 RETN

...

+ 00001D8A JMP OEP


  비교해보면 알겠지만 주소가 조금씩 다르다. 하지만 "ADD DWORD PTR SS:[ESP], 0C", "RETN" 명령어 및 "JMP" 명령어는 항상 동일한 것으로 보이므로 이것을 이용해 OEP를 쉽게 구할 수 있을 것으로 보인다. 즉 특정 API에 BP를 설치하거나 예외를 단계로 보고 뒷 부분까지 넘기다가 저 부근의 주소에서 해당 명령어를 찾은 후 OEP로 실행시키면 될 것으로 보인다.



Posted by SanseoLab

블로그 이미지
Malware Analyst
SanseoLab

태그목록

공지사항

Yesterday
Today
Total

달력

 « |  » 2024.4
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

최근에 올라온 글

최근에 달린 댓글

글 보관함