Lined Notebook

VMProtect 데모 버전 분석해보기. 🔒 - 작성중

by juraffe juraffe

가상화된 코드는 인증된 서명이 없다면 대부분 평범한 파일이라도 백신은 악성코드로 탐지하게 된다. 때문에 악성코드 제작자는 먼저 서명을 탈취하여 배포할 악성코드에 탈취한 서명을 넣어 배포한다. 그러면 악성코드의 생명주기(?)가 늘어나게 되어 많은 PC에 퍼지게 되고 오랜 기간 생존하게 된다. 그렇기에 VMProtect와 같이 강력한 가상화 코드를 생성하는 툴로 패킹된 악성코드 샘플이 많이 들어오게 된다고 한다. 그렇기에 미리 공부할 겸 작성하게 됐다.

 

분석 환경

  • Visual studio 2019 community - x86 compile
  • x64dbg
  • VMProtectDemo for Windows ver.3.4 (03.08.2019)

VMProtect

;-- VMProtect
Address   Size      Info                              Content                       Type   Protection   Initial                                                                                                                                
00D60000  00001000  consoleapplication1.vmp.exe                                     IMG    -R---        ERWC-
00D61000  00001000   ".text"                          Executable code               IMG    ER---        ERWC-
00D62000  00001000   ".rdata"                         Read-only initialized data    IMG    -R---        ERWC-
00D63000  00001000   ".data"                          Initialized data              IMG    -RW--        ERWC-
00D64000  00114000   ".vmp0"                                                        IMG    ER---        ERWC-
00E78000  000CC000   ".vmp1"                                                        IMG    ER---        ERWC-
00F44000  00001000   ".reloc"                         Base relocations              IMG    -R---        ERWC-
00F45000  00001000   ".rsrc"                          Resources                     IMG    -R---        ERWC-

;-- Original
Address  Size     Info                                                Content                    Typ Prote Initi
00720000 00001000 consoleapplication1.exe                                                        IMG -R--- ERWC-
00721000 00001000  ".text"                                            Executable code            IMG ER--- ERWC-
00722000 00001000  ".rdata"                                           Read-only initialized data IMG -R--- ERWC-
00723000 00001000  ".data"                                            Initialized data           IMG -RW-- ERWC-
00724000 00001000  ".rsrc"                                            Resources                  IMG -R--- ERWC-
00725000 00001000  ".reloc"                                           Base relocations           IMG -R--- ERWC-

 

먼저 VMProtect로 패킹된 파일과 원본 파일의 차이점이라면 EP의 위치에 있다. VMProtect의 EP는 .vmp1에 있는데 이곳에서 가상화 명령어를 가져와 해당 명령어 핸들러로 처리를 하는 과정이 수행된다.

00F2F5AC | FF3485 98EDF100       | push    dword ptr ds:[eax*4+0xF1ED98]          | Instruction handler
00F2F5B3 | C3                    | ret                                            |

 

과정은 EAX의 값에 따라서 명령이 수행되게 되는데 현재 사용한 VMProtect는 데모 버전이므로 처음 실행하게 되면 데모 버전으로 패킹되었다는 메시지 박스가 출력되게 된다. 때문에 MessageBoxW에 BP를 걸면 call 명령어 핸들러를 찾을 수 있게 된다. (+패킹된 바이너리는 수행전 파일의 무결성을 검증하기 때문에 SW BP를 대상 바이너리에 걸게 되면 무결성 검사 실패로 종료되게 된다.)

00E8191B | FFD0                  | call    eax                                    | call
00E8191D | 8B6C25 FC             | mov     ebp,dword ptr ss:[ebp-0x4]             |
00E81921 | 0FB7F7                | movzx   esi,di                                 |
00E81924 | 894425 00             | mov     dword ptr ss:[ebp],eax                 |
00E81928 | 66:0FCE               | bswap   si                                     |
00E8192B | 5E                    | pop     esi                                    |
00E8192C | E9 9C1C0B00           | jmp     consoleapplication1.vmp.F335CD         |

 

명령어를 트레이싱하며 해당 call에 들어갈 때 0x00F2F5AC의 eax에 어떤 값이 들어가는지 알아보면 다음과 같다.

INST>0x0070bdfd:25
CALL>0x0066bf2d:77690110 > 0x0d71e188
...
INST>0x0070bdfd:25
CALL>0x0066bf2d:77690110 > 0x0d71e110
...
INST>0x0070bdfd:25
CALL>0x0066bf2d:77690110 > 0x0d71dbe8

 

이로써 call을 하기 위한 핸들러는 eax=25 임을 알 수 있다. 이 과정을 거치면서 알게 된 정보인데 VMProtect는 여러 패턴을 가지고 매번 다른 형태의 핸들러를 만들고 새로운 가상 명령어를 사용한다. 때문에 이 파일처럼 쉽게 예상할 수 있는 호출되는 API(MessageBoxW)가 없다면 이 값을 찾기가 난해해질 듯하다.

;-- VMProtect로 패킹된 바이너리 print_msg 함수
00351080 | 55                    | push    ebp                                             |
00351081 | 8BEC                  | mov     ebp,esp                                         |
00351083 | 68 36010000           | push    0x136                                           |
00351088 | 68 08213500           | push    consoleapplication1.vmp.352108                  | 352108:L"Account Details"
0035108D | 68 28213500           | push    consoleapplication1.vmp.352128                  | 352128:L"Resource not available\nDo you want to try again?"
00351092 | 6A 00                 | push    0x0                                             |
00351094 | 56                    | push    esi                                             | esi:&"C:\\Users\\User\\Source\\Repos\\ConsoleApplication1\\Release\\ConsoleApplication1.vmp.exe"
00351095 | E8 86310000           | call    consoleapplication1.vmp.354220                  |
0035109A | 68 8C213500           | push    consoleapplication1.vmp.35218C                  | 35218C:"Hello, World!\n"
0035109F | E8 9CFFFFFF           | call    consoleapplication1.vmp.351040                  |
003510A4 | 83C4 04               | add     esp,0x4                                         |
003510A7 | 68 9C213500           | push    consoleapplication1.vmp.35219C                  | 35219C:"Goodbye, World!\n"
003510AC | E8 8FFFFFFF           | call    consoleapplication1.vmp.351040                  |
003510B1 | 83C4 04               | add     esp,0x4                                         |
003510B4 | 5D                    | pop     ebp                                             |
003510B5 | C3                    | ret                                                     |

;-- 원본 바이너리의 print_msg 함수
00E41080 | 55                    | push    ebp                                   | ConsoleApplication1.cpp:11
00E41081 | 8BEC                  | mov     ebp,esp                               |
00E41083 | 68 36010000           | push    0x136                                 | ConsoleApplication1.cpp:12
00E41088 | 68 0821E400           | push    consoleapplication1.E42108            | E42108:L"Account Details"
00E4108D | 68 2821E400           | push    consoleapplication1.E42128            | E42128:L"Resource not available\nDo you want to try again?"
00E41092 | 6A 00                 | push    0x0                                   |
00E41094 | FF15 3420E400         | call    dword ptr ds:[<&MessageBoxW>]         |
00E4109A | 68 8C21E400           | push    consoleapplication1.E4218C            | ConsoleApplication1.cpp:16, E4218C:"Hello, World!\n"
00E4109F | E8 9CFFFFFF           | call    <consoleapplication1._printf>         |
00E410A4 | 83C4 04               | add     esp,0x4                               |
00E410A7 | 68 9C21E400           | push    consoleapplication1.E4219C            | ConsoleApplication1.cpp:17, E4219C:"Goodbye, World!\n"
00E410AC | E8 8FFFFFFF           | call    <consoleapplication1._printf>         |
00E410B1 | 83C4 04               | add     esp,0x4                               |
00E410B4 | 5D                    | pop     ebp                                   | ConsoleApplication1.cpp:18
00E410B5 | C3                    | ret                                           |

 

VMProtect 데모에서는 별다른 안티 디버깅이 없는지 원본 코드가 암호화된 상태에서 .vmp1에 가상화된 복호화 코드를 거쳐 .text로 원본 코드를 복호화한다. 때문에 ImageBase + AddressOfEntryPoint 위치에 BP를 걸고 RUN 하게 되면 코드가 .text에 풀리고 원본 바이너리 코드로 넘어가게 된다. 코드는 원본 코드와 크게 다르지 않지만 위 코드에서 같이 0x351094ESI 레지스터 값을 추가적으로 push 한다.

;-- stage1
00354220 | 90                    | nop                                                     |
00354221 | BE 4B6AF568           | mov     esi,0x68F56A4B                                  | 
00354226 | 5E                    | pop     esi                                             | 
00354227 | 873424                | xchg    dword ptr ss:[esp],esi                          | swap [return address], esi value
0035422A | E9 36E50600           | jmp     consoleapplication1.vmp.3C2765                  |

;-- stage2
003C2765 | 56                    | push    esi                                             | 
003C2766 | BE 4E1D3500           | mov     esi,consoleapplication1.vmp.351D4E              | ESI = function table entry
003C276B | E9 1AFF0600           | jmp     consoleapplication1.vmp.43268A                  |

;-- stage3
0043268A | 8BB6 D5660000         | mov     esi,dword ptr ds:[esi+0x66D5]                   | ESI = function table address
00432690 | E9 6E17F3FF           | jmp     consoleapplication1.vmp.363E03                  |

;-- stage4
00363E03 | 8DB6 4236F023         | lea     esi,dword ptr ds:[esi+0x23F03642]               | get function address
00363E09 | E9 B33BFFFF           | jmp     consoleapplication1.vmp.3579C1                  |

;-- stage5
003579C1 | 873424                | xchg    dword ptr ss:[esp],esi                          | eip control #esi = call function
003579C4 | E9 069DFFFF           | jmp     consoleapplication1.vmp.3516CF                  |

;-- stage6
003516CF | C3                    | ret                                                     | = pop eip

 

ESI는 VMProtect로 패킹된 바이너리를 분석하면 굉장히 자주 이용되는 걸 볼 수 있는데 이 부분에서도 함수 호출하면서 .vmp0 쪽으로 넘어가고 ESI를 이용해 함수의 주소를 구하게 된다. 또 이런 괴상한 방식으로 호출을 하게 되면 분석 툴에서 어떤 함수가 호출되는지 가려 분석에 혼란을 주게 된다.

 

분석가E님의 글을 보면 이 과정을 자동으로 정적 분석하여 디코더를 만드셨는데 나 또한 비슷한 과정을 해보려고 한다. 그래서 OEP에서 메모리 덤프를 한 결과를 가져오려고 했지만 x64dbg 자체에 있는 Scylla의 기능은 메모리 영역 일부만 가져올 수 있었고 덤프 시 사이즈를 전체 메모리 영역으로 주면 원하는 결과를 얻을 수 있는지 알고 했지만 안돼서 한참이나 돌아다녔다.. 그래서 OllyDumpEx 플러그인을 이용해서 덤프를 뜨고 진행했다.

   Compiling vmp_api_decoder v0.1.0 (/Users/junomacbook/Desktop/repos/vmp_api_decoder)
    Finished dev [unoptimized + debuginfo] target(s) in 0.70s
     Running `target/debug/vmp_api_decoder`
Found 1555 instructions
CALL -> 0x141000
CALL -> 0x141c70
CALL -> 0x141585
... 중략 ...
CALL -> *0x1420cc
CALL -> *%esi
CALL -> 0x141ce8
CALL -> *0x1420cc
CALL -> *%edi
CALL -> *0x1420cc
CALL -> *%edi
CALL -> 0x141cf4
... 중략 ...
CALL -> 0x147eb1
CALL -> 0x151ef6

 

잘 동작하여 호출하는 지점의 주소를 구할 수 있다. 그럼 이제 각 섹션의 VADDR을 구해서 그쪽으로 넘어가는 부분을 구분해야한다.

;-- radare2
[0x0025f213]> iS
[Sections]

nth paddr           size vaddr          vsize perm name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00001000    0x1000 0x00141000    0x1000 -r-x .text
1   0x00002000    0x1000 0x00142000    0x1000 -r-- .rdata
2   0x00003000    0x1000 0x00143000    0x1000 -rw- .data
3   0x00004000  0x114000 0x00144000  0x114000 -r-x .vmp0
4   0x00118000   0xc3000 0x00258000   0xc3000 -r-x .vmp1
5   0x001db000    0x1000 0x0031b000    0x1000 -r-- .reloc
6   0x001dc000    0x1000 0x0031c000    0x1000 -r-- .rsrc

;-- vmp_api_decoder
   Compiling vmp_api_decoder v0.1.0 (/Users/junomacbook/Desktop/repos/vmp_api_decoder)
    Finished dev [unoptimized + debuginfo] target(s) in 0.77s
     Running `target/debug/vmp_api_decoder`
Found 1555 instructions
call -> 0x21a3f1 [.vmp0]
call -> 0x222e2d [.vmp0]
call -> 0x144220 [.vmp0]
... 중략 ...
call -> 0x151e96 [.vmp0]
call -> 0x147eb1 [.vmp0]
call -> 0x151ef6 [.vmp0]

 

0x144000-0x257FFF까지 범위의 호출은 .vmp0에서 디코딩되기를 바라는 호출이므로 이 부분을 unicorn을 이용해 에뮬레이팅을 진행한다.

 

'🔬 연구' 카테고리의 다른 글

VMProtect 데모 버전 분석해보기. 🔒 - 작성중  (0) 2020.03.05
포너블 스터디 #5  (0) 2019.06.21
포너블 스터디 #4  (0) 2019.06.01
포너블 스터디 #3  (0) 2019.05.25
포너블 스터디 #2  (0) 2019.05.19
poison_null_byte  (0) 2018.10.30

블로그의 정보

🦒 Juraffe's note

juraffe juraffe

활동하기