좀 열심히 쓴 글

WTF(WHAT THE FUZZ) TUTORIAL

ch4rli3kop 2022. 6. 3. 19:57
반응형

WTF(WHAT THE FUZZ) TUTORIAL

wtf(What The Fuzz) fuzzer가 실제로 취약점을 잘 찾을 수 있는지 궁금해서 테스트 프로그램으로 퍼징을 돌려보려고 한다.

Target Program

퍼징할 대상은 대충 다음과 같이 만들었다. fgets로 파일 데이터를 buf에 저장한 상태에서 snapshot을 찍고, 퍼저에서 해당 buf 메모리에 mutation 된 데이터를 직접 memory write 함으로써, 퍼징을 수행할 예정이다.

컴파일해서 vm 내에 넣어주면 되는데, 중요한 점은 Debug가 아니라 Release로 빌드해야 한다는 점이다. 분석을 쉽게하기 위해서 코드 최적화도 꺼놨다. (Debug 모드로 빌드할 시, vcruntime140.dll 에서 크래시를 탐지해서 crash_detection 코드를 추가해야 함) 위 프로그램을 컴파일해서 vm 내에 넣어준다.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
​
char buf[0x1000];
​
void fuzzme ( char* buf ) {
  char localbuf[0x200] = {0, };
  if (buf[0] == 'A')
    memcpy(localbuf, buf, strlen(buf)); // <- here is vuln
  else
    memcpy(localbuf, buf, 0x10);
  
  printf("%s\n", localbuf);
}
​
int main(int argc, char* argv[]) {
​
  if (argc < 2) {
    fprintf(stderr, "no input file\n");
    exit(-1);
  }
​
  FILE* f = fopen(argv[1], "rb");
  fgets(buf, 0x1000, f);
  fuzzme(buf);
  return 0;
}

Kernel Debugging & Snapshot

이제 host에서는 kernel live debugging을 하면서 다음과 같이 입력한다.

kd> !gflag +ksl
Current NtGlobalFlag contents: 0x00040000
    ksl - Enable loading of kernel debugger symbols
kd> sxe ld test_wtf_harness.exe
kd> g

vm 내에서 다음과 같이 프로세스를 실행한다.

Microsoft Windows [Version 10.0.19044.1706]
(c) Microsoft Corporation. All rights reserved.
​
C:\Users\pch21\Desktop\Fuzz\test_wtf>.\test_wtf_harness.exe .\input.txt

host 에서는 대충 심볼 추가하고 main 시점에서 브포를 건다.

kd> .sympath+ C:\Users\Charlie\Desktop\fuzzing\test_wtf_harness\x64\Release
Symbol search path is: srv*;C:\Users\Charlie\Desktop\fuzzing\test_wtf_harness\x64\Release
Expanded Symbol search path is: cache*;SRV*https://msdl.microsoft.com/download/symbols;c:\users\charlie\desktop\fuzzing\test_wtf_harness\x64\Release
​
************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       srv*
OK                                             C:\Users\Charlie\Desktop\fuzzing\test_wtf_harness\x64\Release
kd> bu test_wtf_harness!main
*** WARNING: Unable to verify checksum for test_wtf_harness.exe
kd> g
Breakpoint 0 hit
test_wtf_harness!main:
0033:00007ff6`f3271180 4889542410         mov     qword ptr [rsp+10h], rdx

main 코드를 살펴보면 다음과 같은데, 우선 wtf 를 사용하는데 중요한 점은 snapshot을 뜨는 것과 wtf backend breakpoint 지점을 결정해야 한다는 점이다. 이 예제의 경우 fuzzme 함수를 대상으로 퍼징을 수행할 것이기 때문에, 해당 함수 진입시점에서 snapshot을 뜰 것이며, 함수 종료 후 실행하는 명령어 주소를 breakpoint로 지정하여, 성공적으로 fuzzme 함수가 실행되었을 때 빠르게 종료하도록 퍼저를 작성할 것이다. fuzzme 함수와 main 함수의 주소는 다음과 같다.

test_wtf_harness!fuzzme:
0033:00007ff6`f32710d0 48894c2408           mov     qword ptr [rsp+8], rcx
0033:00007ff6`f32710d5 57                   push    rdi
0033:00007ff6`f32710d6 4881ec30020000       sub     rsp, 230h
​
...
​
​
test_wtf_harness!main:
0033:00007ff6`f3271180 4889542410         mov     qword ptr [rsp+10h], rdx
0033:00007ff6`f3271185 894c2408           mov     dword ptr [rsp+8], ecx
0033:00007ff6`f3271189 4883ec38           sub     rsp, 38h
0033:00007ff6`f327118d 488d0d703e0000     lea     rcx, 
...
0033:00007ff6`f32711fe 488d0d7b440000     lea     rcx, [test_wtf_harness!buf{[0]} (7ff6f3275680)]
0033:00007ff6`f3271205 ff15851f0000       call    qword ptr [test_wtf_harness!__imp_fgets (7ff6f3273190)]
0033:00007ff6`f327120b 488d0d6e440000     lea     rcx, [test_wtf_harness!buf{[0]} (7ff6f3275680)]
0033:00007ff6`f3271212 e8b9feffff         call    test_wtf_harness!fuzzme (7ff6f32710d0)
0033:00007ff6`f3271217 33c0               xor     eax, eax
0033:00007ff6`f3271219 4883c438           add     rsp, 38h
0033:00007ff6`f327121d c3                 ret     

test_wtf_harness!fuzzme 에 bp를 걸어 해당 지점에서 break를 한다. 레지스터 정보를 살펴보면 다음과 같다.

kd> bu test_wtf_harness!fuzzme
kd> g
​
...
​
kd> r
rax=00007ff6f3275680 rbx=0000025f3cbc30c0 rcx=00007ff6f3275680
rdx=0000000000000000 rsi=0000000000000000 rdi=0000025f3cbc6de0
rip=00007ff6f32710d0 rsp=000000da4e8ffd08 rbp=0000000000000000
 r8=000000da4e8ffb18  r9=0000000000001000 r10=0000000000000000
r11=0000000000000246 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b
test_wtf_harness!fuzzme:
0033:00007ff6`f32710d0 48894c2408      mov     qword ptr [rsp+8],rcx ss:002b:000000da`4e8ffd10=0000025fffffffff

이 시점에서 bdump script를 이용하여 메모리 덤프를 수행한다.

kd> .scriptload D:\Tools\bdump-master\bdump.js
[bdump] Usage: !bdump "C:\\path\\to\\dump"
[bdump] Usage: !bdump_full "C:\\path\\to\\dump"
[bdump] Usage: !bdump_active_kernel "C:\\path\\to\\dump"
[bdump] This will create a dump directory and fill it with a memory and register files
[bdump] NOTE: you must include the quotes and escape the backslashes!
JavaScript script successfully loaded from 'D:\Tools\bdump-master\bdump.js'
​
​
kd> !bdump_full "D:\\Tools\\bdump-master\\state_full"
[bdump] creating dir...
[bdump] saving regs...
[bdump] register fixups...
[bdump] don't know how to get mxcsr_mask or fpop, setting to zero...
[bdump]
[bdump] don't know how to get avx registers, skipping...
[bdump]
[bdump] tr.base is not cannonical...
[bdump] old tr.base: 0x79f76000
[bdump] new tr.base: 0xfffff80579f76000
[bdump]
[bdump] rip and gs don't match kernel/user, swapping...
[bdump] rip: 0x7ff6f32710d0
[bdump] new gs.base: 0xda4e6a5000
[bdump] new kernel_gs_base: 0xfffff8057265f000
[bdump]
[bdump] non-zero IRQL in usermode, resetting to zero...
[bdump] saving mem, get a coffee or have a smoke, this will probably take around 10-15 minutes...
[bdump] Creating D:\Tools\bdump-master\state_full1\mem.dmp - Full kernel dump
[bdump] 0% written.
[bdump] 5% written. 42 sec remaining.
[bdump] 10% written. 41 sec remaining.
[bdump] 15% written. 37 sec remaining.
[bdump] 20% written. 37 sec remaining.
[bdump] 25% written. 34 sec remaining.
[bdump] 30% written. 31 sec remaining.
[bdump] 35% written. 28 sec remaining.
[bdump] 40% written. 27 sec remaining.
[bdump] 45% written. 24 sec remaining.
[bdump] 50% written. 21 sec remaining.
[bdump] 55% written. 20 sec remaining.
[bdump] 60% written. 16 sec remaining.
[bdump] 65% written. 9 sec remaining.
[bdump] 70% written. 7 sec remaining.
[bdump] 75% written. 6 sec remaining.
[bdump] 80% written. 10 sec remaining.
[bdump] 85% written. 6 sec remaining.
[bdump] 90% written. 4 sec remaining.
[bdump] 95% written. 2 sec remaining.
[bdump] Wrote 4.0 GB in 42 sec.
[bdump] The average transfer rate was 97.5 MB/s.
[bdump] Dump successfully written
[bdump] done!
@$bdump_full("D:\\Tools\\bdump-master\\state_full")

Write fuzzer code

이제 fuzzer를 작성해야 하는데, \src\wtf 디렉토리에 fuzzer_test_wtf.cpp 이름으로 파일을 하나 생성한 뒤, 다음과 같이 코드를 작성한다.

#include "backend.h"
#include "targets.h"
#include <fmt/format.h>
#include "crash_detection_umode.h"
​
namespace fs = std::filesystem;
​
namespace Test_wtf {
​
constexpr bool LoggingOn = false;
​
template <typename... Args_t>
void DebugPrint(const char *Format, const Args_t &...args) {
  if constexpr (LoggingOn) {
    fmt::print("Test WTF : ");
    fmt::print(fmt::runtime(Format), args...);
  }
}
​
bool InsertTestcase(const uint8_t *Buffer, const size_t BufferSize) {
  
  const Gva_t buf = Gva_t(g_Backend->Rcx());
  if (!g_Backend->VirtWriteDirty(buf, Buffer, BufferSize)){
        DebugPrint("VirtWriteDirty failed\n");
        return false;
  }
  
​
  return true;
}
​
bool Init(const Options_t &Opts, const CpuState_t &) {
  
  if (!g_Backend->SetBreakpoint(Gva_t(0x007ff6f327121d), [](Backend_t *Backend) {
        DebugPrint("fuzzme finish\n");
        Backend->Stop(Ok_t());
      })) {
    DebugPrint("Failed to SetBreakpoint main+0x9d\n");
    return false;
  }
​
  SetupUsermodeCrashDetectionHooks();
​
  return true;
}
​
//
// Register the target.
//
​
Target_t Test_wtf("test_wtf", Init, InsertTestcase);
​
}

하나 하나 자세히 살펴보면 다음과 같다.

Init

먼저 Init 함수의 경우, 빠른 프로세스 종료나 crash detection 동작을 수행하기 위해 backend의 breakpoint를 설정하는 부분이다. 그냥 적당히 fuzzme 호출 후, main 함수의 에필로그 주소를 대상으로 bp를 등록했다.

0033:00007ff6`f3271212 e8b9feffff         call    test_wtf_harness!fuzzme (7ff6f32710d0)
0033:00007ff6`f3271217 33c0               xor     eax, eax
0033:00007ff6`f3271219 4883c438           add     rsp, 38h
0033:00007ff6`f327121d c3                 ret   

main의 에필로그 opcode의 주소인 0x0007ff6f327121d에 bp를 걸어 해당 주소의 명령어가 호출될 때, backend를 종료하도록 다음과 같이 작성했다. Ok_t()를 인자로 전달하여 코드가 정상적으로 수행되었음을 나타낼 수 있다. 또한, crash_detection_umode.h에 존재하는 SetupUsermodeCrashDetectionHooks를 호출하여 crash detection을 등록했다. 코드를 살펴보면, stack overflow 등으로 메모리 exception이 발생하면 dispatcher 함수가 호출되는데, 해당 함수들에 bp를 걸어 crash 처리를 하도록 했다.

bool Init(const Options_t &Opts, const CpuState_t &) {
  
  if (!g_Backend->SetBreakpoint(Gva_t(0x007ff6f327121d), [](Backend_t *Backend) {
        DebugPrint("fuzzme finish\n");
        Backend->Stop(Ok_t());
      })) {
    DebugPrint("Failed to SetBreakpoint main+0x9d\n");
    return false;
  }
​
  SetupUsermodeCrashDetectionHooks();
​
  return true;
}

InsertTestcase

앞서 설명했다시피, 이미 파일 데이터가 쓰인 buf 메모리에 mutation된 데이터를 덮어쓸 계획이다. fuzzme 함수의 첫 번째 인자를 살펴보면 다음과 같이 buf의 주소이며, ZXCV라는 파일 데이터가 들어있는 것을 확인할 수 있다.

kd> db @rcx
00007ff6`f3275680  5a 58 43 56 00 00 00 00-00 00 00 00 00 00 00 00  ZXCV............
00007ff6`f3275690  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`f32756a0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`f32756b0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`f32756c0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`f32756d0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`f32756e0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00007ff6`f32756f0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

Windows x64 calling convention을 기준으로 함수의 첫 번째 인자는 rcx이기 때문에, rcx 값을 가져와서 buf의 주소를 얻고 해당 주소에 VirtWriteDirty 함수를 이용하여 mutation 된 데이터를 덮어쓴다.

bool InsertTestcase(const uint8_t *Buffer, const size_t BufferSize) {
  
  const Gva_t buf = Gva_t(g_Backend->Rcx());
  if (!g_Backend->VirtWriteDirty(buf, Buffer, BufferSize)){
        DebugPrint("VirtWriteDirty failed\n");
        return false;
  }
  
​
  return true;
}

Register

작성한 fuzzer를 wtf에 등록한다. 이 때 등록한 이름을 사용하여 wtf.exe를 실행할 때 퍼저를 특정하게 된다.

Target_t Test_wtf("test_wtf", Init, InsertTestcase);

아무튼 잘 빌드하면 된다.

Build fuzzer

Readme에 있는 것처럼 잘 빌드하면 된다.

D:\Tools\wtf\src\build_msvc>..\build\build-release-msvc.bat
​
D:\Tools\wtf\src\build_msvc>cmake ..
-- Selecting Windows SDK version 10.0.22000.0 to target Windows 10.0.19043.
-- Configuring done
-- Generating done
-- Build files have been written to: D:/Tools/wtf/src/build_msvc
​
D:\Tools\wtf\src\build_msvc>cmake --build . --config RelWithDebInfo
.NET Framework용 Microsoft (R) Build Engine 버전 16.11.2+f32259642
Copyright (C) Microsoft Corporation. All rights reserved.
​
  hevd_client.vcxproj -> D:\Tools\wtf\src\build_msvc\RelWithDebInfo\hevd_client.exe
  tlv_server.vcxproj -> D:\Tools\wtf\src\build_msvc\RelWithDebInfo\tlv_server.exe
  fuzzer_test_wtf.cc
  코드를 생성하고 있습니다.
  0 of 12308 functions ( 0.0%) were compiled, the rest were copied from previous compilation.
    0 functions were new in current compilation
    0 functions had inline decision re-evaluated but remain unchanged
  코드를 생성했습니다.
  wtf.vcxproj -> D:\Tools\wtf\src\build_msvc\RelWithDebInfo\wtf.exe
​

Run Fuzzer

wtf는 data mutation을 하는 server와 input를 전달하여 backend에서 실행하는 client로 이루어져 있다. 아래와 같이 실행하면 된다.

Server

D:\Tools\wtf\targets\test_wtf>..\..\src\build_msvc\RelWithDebInfo\wtf.exe master --max_len=2048 --runs=1000000 --target . --name test_wtf
Seeded with 5247091273893732672
Iterating through the corpus..
Sorting through the 1 entries..
Running server on tcp://localhost:31337..
#0 cov: 0 (+0) corp: 0 (0.0b) exec/s: 0.0 (0 nodes) lastcov: 14.0s crash: 0 timeout: 0 cr3: 0 uptime: 14.0s
#0 cov: 0 (+0) corp: 0 (0.0b) exec/s: -nan (1 nodes) lastcov: 14.0s crash: 0 timeout: 0 cr3: 0 uptime: 14.0s
Saving output in .\outputs\cr3-5934aa3139a87e36f522ee7f2ec452f4
Saving output in .\outputs\cr3-c743ac7cf4b00b1bc44786c765e874d0
Saving output in .\outputs\crash-39990eed0c80b9a4b266fd2cf2162606
Saving output in .\outputs\cr3-75711a754a442814ea3e048714639929
#7290 cov: 12194 (+12194) corp: 29 (3.7kb) exec/s: 729.0 (1 nodes) lastcov: 7.0s crash: 189 timeout: 0 cr3: 7101 uptime: 24.0s
#14127 cov: 12194 (+0) corp: 29 (3.7kb) exec/s: 706.4 (1 nodes) lastcov: 17.0s crash: 377 timeout: 0 cr3: 13750 uptime: 34.0s

Client

D:\Tools\wtf\targets\test_wtf>..\..\src\build_msvc\RelWithDebInfo\wtf.exe fuzz --backend=bochscpu --limit 100000 --target D:\Tools\wtf\targets\test_wtf --name test_wtf
Initializing the debugger instance.. (this takes a bit of time)
D:\Tools\wtf\targets\test_wtf\state\mem.dmpSetting debug register status to zero.
Setting debug register status to zero.
Could not set a breakpoint at hal!HalpPerfInterrupt.
Failed to set breakpoint on HalpPerfInterrupt, but ignoring..
Dialing to tcp://localhost:31337/..
#4710 cov: 12194 exec/s: 471.0 lastcov: 6.0s crash: 150 timeout: 0 cr3: 4560 uptime: 10.0s

Conclusion

실제 실행하면 타겟 프로그램 대상으로 금방 크래시를 찾는다. 다음 편은 이제 이걸로 실제 프로그램 퍼징하는 내용을 써야쥐.

Trouble Shoot

1. Error: Unable to create file [at bdump (line 296 col 5)]

kd> !bdump "D:\\Tools\\bdump-master\\state_normal"
[bdump] creating dir...
[bdump] saving regs...
[bdump] register fixups...
[bdump] don't know how to get mxcsr_mask or fpop, setting to zero...
[bdump]
[bdump] don't know how to get avx registers, skipping...
[bdump]
[bdump] tr.base is not cannonical...
[bdump] old tr.base: 0x79f76000
[bdump] new tr.base: 0xfffff80579f76000
[bdump]
[bdump] rip and gs don't match kernel/user, swapping...
[bdump] rip: 0x7ff6f32710d0
[bdump] new gs.base: 0xda4e6a5000
[bdump] new kernel_gs_base: 0xfffff8057265f000
[bdump]
[bdump] non-zero IRQL in usermode, resetting to zero...
Error: Unable to create file [at bdump (line 296 col 5)]
DBGHELP: test_wtf_harness is not source indexed

기존에 메모리 파일이 존재하는데, 덮어쓰기가 안돼서 발생하는 문제로, 새로운 디렉토리로 이름을 변경하거나 기존 dmp 파일을 삭제하면 해결할 수 있음.

2. OpenDumpFile(D:\Tools\wtf\targets\test_wtf\state\mem.dmp) failed with hr=-0x7ff8ffa9

# when running wtf.exe with fuzz mode
OpenDumpFile(D:\Tools\wtf\targets\test_wtf\state\mem.dmp) failed with hr=-0x7ff8ffa9

유저모드 프로세스를 대상으로 덤프할 때, 그냥 !bdump 로 덤프할 경우 가끔 발생하는 문제같음. 그냥 !bdump_full 명령어로 full 메모리 덤프하면 해결할 수 있음.

3. [bdump] could not recover fs!

kd> !bdump_active_kernel "D:\\Tools\\bdump-master\\state_kernel"
[bdump] creating dir...
[bdump] saving regs...
[bdump] could not recover fs!
Error: Unable to set property 'base' of undefined or null reference [at bdump (line 73 col 5)]
DBGHELP: test_wtf_harness is not source indexed
kd> dg cs
                                                    P Si Gr Pr Lo
Sel        Base              Limit          Type    l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0033 00000000`00000000 00000000`00000000 Code RE Ac 3 Nb By P  Lo 000002fb

유저모드 프로세스 덤프할 때, 커널 메모리 덤프할 때 발생하는 문제같음. 유저모드 프로세스 대상으로 덤프할 때는 !bdump!bdump_full을 사용해야 함.

반응형