Time Travel Debugging을 사용한 윈도우즈 리버스 엔지니어링 1
리버스 엔지니어링 방법론은 계속 변화하고 있습니다. 전통적인 디버거를 사용하는 방법에서, 최근에는 급격히 자동화 된 툴들을 사용하는 경향성이 커지고 있습니다. 특히, 취약점 연구에 있어서 Code Coverage Guided Fuzzing과 같이 코드의 실행 커버리지를 측정하여 인풋의 변이를 조정하는 형태의 퍼저들이 유행하고 있습니다. 이러한 여러 추세에 맞추어 윈도우즈에서 리버스 엔지니어링에 유용하게 사용될 수 있는 Time Travel Debugging에 대해서 정리하였습니다.
Dynamic Binary Instrumenation이란 무엇인가?
DBI(Dynamic Binary Instrumentation)는 이미 존재하는 바이너리의 인스트럭션들을 새롭게 재가공하여 새로운 인스트럭션을 만들어 내어 원하는 행위를 수행하는 것을 말합니다.
DBI의 목적은 필요에 따라서 여러가지가 있을 수 있습니다.
-
새로운 플랫폼에서의 실행: 예를 들어 인텔 플랫폼에서의 바이너리를 새롭게 해석하여 ARM 플랫폼에서 동작하게 만드는 등의 작업이 가능합니다.
-
프로그램 행위 분석: 프로그램의 성능 프로파일링이나 행위 추적을 위해서 사용 가능합니다.
-
취약점 연구: 취약점을 찾아 내기 위한 용도로 사용됩니다. 대표적으로 Code Coverage Guided Fuzzing을 위해서 사용됩니다. AFL(American Fuzzy Lop)은 초기에 소스코드에 instrumentation코드를 집어 넣는 Compile-Time Instrumentation 형태로 개발되었지만, WinAFL과 같은 윈도우즈에 포팅된 코드에서는 DynamoRIO라는 DBI 툴을 사용하여 코드 커버리지를 확인합니다.
-
취약점 Triage: Triage라는 용어는 한국어로 번역하기에 굉장히 애매한 단어로서, 전쟁 중의 부상자들을 임시 수용소 등에서 응급처리하는 등의 행위 등을 일컫는 용어 입니다. 소프트웨어에서의 triage는 이러한 프로그램의 취약점이나 문제점등에 대해서 원인을 파악하고 (root cause analysis) 향후 대책을 논의하는 행위를 의미합니다.
DBI 툴들
-
- Dynamic Binary Instrumentation 등의 문서에서 이미 DBI와 같은 개념과 DynamoRIO등을 소개하고 있습니다.
-
- Dynamic Binary Instrumentation Primer와 같은 문서를 통해서 간단하게 PIN툴등을 사용하여 어떻게 이러한 작업이 가능한지 알 수 있습니다.
-
- TTD는 마이크로소프트 내부에서 소프트웨어 triaging나 퍼징 등을 위한 프레임워크로 사용되던 툴입니다. 최근 일반에 툴이 공개되었고 여러 목적으로 사용이 가능해졌습니다. TTD의 장점은 아무런 프로그램 작성 없이 바로 사용이 가능하다는 점입니다. 단점은 지원되는 플랫폼이 윈도우즈로 한정 되어 있다라는 점입니다. 윈도우즈 플랫폼에서 행위 분석이나 Root Cause Analysis등을 수행하기 위해서 적절한 툴로 생각 할 수 있습니다.
TTD란 무엇인가?
Time Travel Debugging의 개념은 이미 2006 마이크로소프트사의 유명한 논문인 Framework for Instruction-level Tracing and Analysis of Program Executions를 통해서 제시 되었습니다. 이 툴에서 언급된 DBI 프레임 워크는 이후 마이크로소프트의 Code Coverage Guided Fuzzing 방법론과 툴 개발에도 지대한 영향을 끼쳤습니다.
이 프레임워크는 Nirvana라는 instrumentation 툴을 타겟 프로세스에서 활성화 시켜, 코드들을 instrumentation하고 iDNA Trace Writer로 효과적으로 압축된 Trace 파일을 생성해 냅니다. 이렇게 생성된 Trace 파일은 iDNA Trace Reader를 통해서 읽혀지며, WinDbg 등의 툴들을 이용해서 로딩하는 것이 가능해집니다. 이러한 저장과 로딩의 개념을 통해서 소프트웨어의 문제가 생겼을 경우 향후 triage나 Root Cause Analysis가 가능해집니다. WinAFL에서 DynamoRIO를 사용해 Code Coverage Guided Fuzzing이 가능하듯이, Nirvana와 iDNA 프레임워크를 사용해서 똑같은 개념의 퍼징을 수행하였습니다. 현재 이러한 퍼징 방법론은 Azure를 통해서 Microsoft Security Risk Detection Service (MSRD)이라는 서비스로 제공되고 있습니다.
- 출처: “Framework for Instruction-level Tracing and Analysis of Program Executions”
TTD
TTD는 WinDbg Preview의 컴퍼넌트로 제공됨으로, 다음 링크를 통해서 다운로드 받고 인스톨할 수 있습니다. 윈도우즈 스토어를 통해서 인스톨이 가능하지만, 한 기계에서 한번 인스톨된 바이너리들만 다른 기계로 카피하여 사용하는 것도 가능합니다.
기본적인 사용법
WinDbg Preview를 실행하면 다음과 “Start Debugging” 메뉴 아래의 “Launch executable (advanced)” 메뉴를 통해서 TTD가 활성화 된 상태로 프로그램을 실행하거나, “Attach to process” 메뉴를 통해서 이미 존재하는 프로세스에 대해서 TTD 모듈을 인젝션할수 있습니다.
다음은 notepad.exe를 실행시키면서 TTD 모듈을 인젝션하는 화면입니다.
다음은 이미 실행된 프로세스에 대해서 TTD 모듈을 인젝션하는 화면입니다.
덤프 저장과 열기
기본적인 테스트를 위해서 다음과 같이 TTD 모듈이 인젝션된 상태에서 테스트 메시지를 노트패드에 입력하고 test.txt라는 파일로 저장합니다.
TTD 레코딩을 마치기 위해서는 WinDbg에서 다음과 같이 “Stop and Debug” 버튼을 클릭해 줍니다.
레코딩이 모두 이루어지고, WinDbg에서 해당 Trace 레코딩 파일을 다시 읽게 되는데, 성공적으로 로딩될 경우 다음과 같은 화면을 볼 수 있습니다.
먼저 위쪽 타이틀 바에는 trace 파일의 이름이 디스플레이 됩니다. 그리고, 리본 메뉴에는 “Reverse Flow Control” 항목에 Time Travel을 위한 여러 컨트롤 버튼들이 위치합니다. 아래쪽 커맨트 페인에는 Time Travel Position이 2B:0가 디스플레이 되어 있습니다. 이 위치 마커는 현재 인스턹션의 위치를 표시합니다. 이 마커를 기록해 두고, 나중에 다시 똑같은 실행 위치로 되돌아 가는 것도 가능합니다.
예를 들어 여러 가지 디버깅 오퍼레이션을 진행하다가, !ttdext.tt 명령을 사용하여, 2B:0 위치로 되돌아 가는 것이 가능합니다.
0:000> !ttdext.tt 2B:0
Setting position: 2B:0
(418.540c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: 2B:0
ntdll!LdrInitializeThunk:
00007ffe`991d75d0 4053 push rbx
!ttdext.help 명령을 사용하면, 기본적으로 Time Travel을 위한 여러 명령어들을 확인할 수 있습니다.
0:000> !ttdext.help
TtdExt - This extension provides commands, model types and model objects
to allow the user to interact with a loaded Time Travel Debugging
trace.
Commands:
help - Print help about all TTD extension commands.
help <command> - Print help about a specific TTD extension command.
tt <position> - Time travel to the given position in the trace.
If <position> is a decimal number between 0 and 100,
it travels to approximately that percent into the trace.
If <position> is #:#, where # are a hexadecimal numbers,
it travels to that position.
If the number after : is omitted, it's defaulted to zero.
If the : is omitted, then the second number must have
precisely 16 hexadecimal digits, with zeros for left-padding.
position [-c] - Displays the current trace position (-c is optional)
positions - Displays all the active threads, including their current positions.
index - Run an indexing pass over the current trace.
If the current trace is already indexed, this does nothing.
index -force - Equivalent to !ttdext.index with no options, except that
if an unloadable index file exists on disk, it will be deleted.
index -status - Reports the status of the trace index
Use '!ttdext.help examples' to view examples of all the commands
컨트롤 플로우
기본적으로 WinDbg에 기존에 제공하는 여러 명령어들을 그대로 사용할 수 있습니다. 다만, 다음과 같은 기존의 컨트롤 플로우 명령어들은 그에 대응하는 백워드 명령어들이 존재합니다.
명령어 | 백워드 명령어 | 작용 |
---|---|---|
t | t- | 일반적으로 스텝 명령어에 해당하면 하나의 인스트럭션을 실행시킵니다. 서브루틴이나 인터럽트가 발생하면 해당 루틴으로 들어 갑니다. |
p | p- | 하나의 명령어를 실행 시키며, 서브루틴이나 인터럽트가 발생할 경우 해당 루틴으로 들어 가지 않고 다음 명령으로 넘어 갑니다. |
g | g- | 현재 위치에서 브레이크 포인트를 만날 때까지 프로그램의 실행을 진행합니다. g-의 경우 프로그램을 거꾸로 실행합니다. |
기존의 bp, bu, bm, ba와 같은 breakpoint 명령은 모두 그대로 사용 가능합니다.
예제 API 추적하기
노트패드 예제를 다음과 같이 실행해 보겠습니다. 이 예제의 목적은 WriteFile API를 추적하여 어떠한 컨텐츠를 파일로 저장하는지를 찾아 내는 것입니다. 물론 일반적인 디버거를 통해서도 이러한 작업은 손쉽게 이루어지지만, Time Travel Debugging의 기본적인 사용법의 예제로 간단히 테스트해 보겠습니다.
일단 kernelbase 모듈에 존재하는 WriteFile로 시작하는 API들을 리스팅해 봅니다.
0:000> x kernelbase!WriteFile*
00007ffe`95f5dd40 KERNELBASE!WriteFileGather (void)
00007ffe`95f100b0 KERNELBASE!WriteFile (void)
00007ffe`95f6acc0 KERNELBASE!WriteFile$filt$0 (void)
00007ffe`95f50de0 KERNELBASE!WriteFileEx (void)
KERNELBASE!WriteFile 함수에 bp 명령을 사용하여 브레이크포인트를 겁니다.
0:000> bp KERNELBASE!WriteFile
이제 Time Travel 기능의 g- 명령을 이용하여 타겟을 거꾸로 실행을 합니다. 이때에 실제로 프로그램의 실행은 일어나지 않으며 다만 Nirvana 엔진을 사용하여 시뮬레이션이 일어 납니다.
0:009> g-
다음과 같이 WriteFile 함수에 브레이크 포인크가 걸립니다.
Breakpoint 1 hit
Time Travel Position: 32486C:36F
KERNELBASE!WriteFile:
00007ffe`95f100b0 48895c2410 mov qword ptr [rsp+10h],rbx ss:0000009e`2394e378=0000000000000400
WriteFile 함수의 프로토타입은 다음과 같습니다. 우리가 알아 내려고 하는 내용은 두번째 인자인 lpBuffer로서 LPCVOID로 선언되어 있는 메모리의 컨텐츠입니다.
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
64비트 환경에서 두번째 인자는 rdx 레지스터를 통해서 전달 됩니다. 이 레지스터가 가리키는 메모리의 내용을 db 명령을 통해서 덤프해 보겠습니다.
0:000> db @rdx
000001de`fa8b2a90 54 68 69 73 20 69 73 20-61 20 74 65 73 74 20 6d This is a test m
000001de`fa8b2aa0 65 73 73 61 67 65 21 0d-0a 0d 0a 0d 0a 00 00 00 essage!.........
000001de`fa8b2ab0 00 00 00 00 00 00 00 00-28 fa 5e 77 00 37 00 80 ........(.^w.7..
000001de`fa8b2ac0 e8 85 4d 96 fe 7f 00 00-00 00 00 00 00 00 00 00 ..M.............
000001de`fa8b2ad0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001de`fa8b2ae0 00 00 00 00 00 00 00 00-2d fa 59 77 00 38 00 80 ........-.Yw.8..
000001de`fa8b2af0 e8 85 4d 96 fe 7f 00 00-00 00 00 00 00 00 00 00 ..M.............
000001de`fa8b2b00 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
세번째 인자인 nNumberOfBytesToWrite를 통해서 정확하게 몇바이트를 쓰려고 했는지를 알아 낼 수 있습니다. 세번째 인자는 r8 인자로 전달 됩니다. 다음과 같이 길이 옵션인 L 뒤에 @r8을 넣어서 정확한 데이타를 덤프할 수 있습니다.
0:000> db @rdx L@r8
000001de`fa8b2a90 54 68 69 73 20 69 73 20-61 20 74 65 73 74 20 6d This is a test m
000001de`fa8b2aa0 65 73 73 61 67 65 21 0d-0a 0d 0a 0d 0a essage!......
메모리 버퍼 추적하기
일단 API나 함수를 추적하는 작업은 손쉽게 이루어집니다. 노트패드는 일종의 테스팅을 위한 타겟으로서 연습을 위해서 이 메모리 버퍼의 내용이 어떻게 생성되고 WriteFile 함수로 전달 되었는지를 추적해 보겠습니다. 이러한 류의 메모리 컨텐츠 트레이싱 작업은 여러 RCA나 크래쉬 분석 등을 위해서 자주 수행해야 하는 작업입니다.
추적하려는 메모리의 내용이 rcx 레지스터에 존재하므로 ba 명령으로 메모리 브레이크 포인트를 걸어 주고, g-로 실행을 거꾸로 해 주어 그 메모리에 쓰는 인스트럭션을 찾습니다.
0:000> ba w1 @rdx
0:000> g-
다음과 같이 브레이크 포인트가 걸리면, 해당 인스트럭션에서 t- 명령으로 바로 전 명령어로 움직입니다.
Breakpoint 0 hit
Time Travel Position: 32486C:15E
KERNELBASE!GetMBDefault+0x9a:
00007ffe`95f373da 448bc1 mov r8d,ecx
0:000> t-
Time Travel Position: 32486C:15D
KERNELBASE!GetMBDefault+0x97:
00007ffe`95f373d7 418809 mov byte ptr [r9],cl ds:000001de`fa8b2a90=00
ba 명령의 경우 억세스가 일어난 직후에 브레이크 포인트가 떨어지기 때문에 t-로 한 인스트럭션 앞으로 전진해 주어야 메모리에 값을 넣는 진짜 명령어를 볼 수 있습니다.
이 상태에서 r9과 cl의 내용은 다음과 같습니다. r9이 타겟 메모리의 주소이고, cl에 그 주소에 쓰려는 문자(0x54)가 들어 있습니다.
0:000> r r9
r9=000001defa8b2a90
0:000> r cl
cl=54
더 t-를 실행하여 앞의 명령을 계속 추적하면, cl은 rax+rbx 메모리에서 꺼내어져 온다는 것을 파악할 수 있습니다.
0:000> t-
Time Travel Position: 32486C:15C
KERNELBASE!GetMBDefault+0x93:
00007ffe`95f373d3 0fb60c18 movzx ecx,byte ptr [rax+rbx] ds:00007df5`d4eb0276=54
rax와 rbx의 내용을 살펴 보면, rbx는 주소이고 rax가 일종의 인덱스로 사용됨을 파악할 수 있습니다.
0:000> r rax, rbx
rax=0000000000000054 rbx=00007df5d4eb0222
rbx가 가리키는 메모리를 덤프해 보면 다음과 같이 일종의 아스키 테이블이 존재함을 알 수 있습니다. rax로 전달된 0x54는 아스키 값으로서 이 테이블에 의해서 아스키인 0x54로 그대로 유지 됨을 알 수 있습니다.
0:000> db rbx
00007df5`d4eb0222 00 01 02 03 04 05 06 07-08 09 0a 0b 0c 0d 0e 0f ................
00007df5`d4eb0232 10 11 12 13 14 15 16 17-18 19 1a 1b 1c 1d 1e 1f ................
00007df5`d4eb0242 20 21 22 23 24 25 26 27-28 29 2a 2b 2c 2d 2e 2f !"#$%&'()*+,-./
00007df5`d4eb0252 30 31 32 33 34 35 36 37-38 39 3a 3b 3c 3d 3e 3f 0123456789:;<=>?
00007df5`d4eb0262 40 41 42 43 44 45 46 47-48 49 4a 4b 4c 4d 4e 4f @ABCDEFGHIJKLMNO
00007df5`d4eb0272 50 51 52 53 54 55 56 57-58 59 5a 5b 5c 5d 5e 5f PQRSTUVWXYZ[\]^_
00007df5`d4eb0282 60 61 62 63 64 65 66 67-68 69 6a 6b 6c 6d 6e 6f `abcdefghijklmno
00007df5`d4eb0292 70 71 72 73 74 75 76 77-78 79 7a 7b 7c 7d 7e 7f pqrstuvwxyz{|}~.
조금 더 그렇다면 이 원래의 rax 인덱스값은 어디에서 왔는지 똑같은 방법으로 추적해 봅니다. t- 명령을 실행해 보면 eax가 rdx가 가리키는 메모리에서 왔다는 것을 파악할 수 있습니다.
0:000> t-
Time Travel Position: 32486C:15B
KERNELBASE!GetMBDefault+0x90:
00007ffe`95f373d0 0fb702 movzx eax,word ptr [rdx] ds:000001de`f3e5cbc0=0054
rdx가 가리키는 곳에 원래의 캐릭터 코드들이 존재합니다.
0:000> dw rdx
000001de`f3e5cbc0 0054 0068 0069 0073 0020 0069 0073 0020
000001de`f3e5cbd0 0061 0020 0074 0065 0073 0074 0020 006d
000001de`f3e5cbe0 0065 0073 0073 0061 0067 0065 0021 000d
000001de`f3e5cbf0 000a 000d 000a 000d 000a 0000 0000 0000
000001de`f3e5cc00 0000 0000 0000 0000 0008 f7f1 01de 0000
000001de`f3e5cc10 0000 0000 0000 0000 21a6 527d 98c6 1000
000001de`f3e5cc20 d800 f3e5 01de 0000 b930 f3e5 01de 0000
000001de`f3e5cc30 d810 f3e5 01de 0000 b940 f3e5 01de 0000
rdx의 내용을 캐릭터들과 함께 덤프해 보면 더 명확해 집니다.
0:000> db rdx
000001de`f3e5cbc0 54 00 68 00 69 00 73 00-20 00 69 00 73 00 20 00 T.h.i.s. .i.s. .
000001de`f3e5cbd0 61 00 20 00 74 00 65 00-73 00 74 00 20 00 6d 00 a. .t.e.s.t. .m.
000001de`f3e5cbe0 65 00 73 00 73 00 61 00-67 00 65 00 21 00 0d 00 e.s.s.a.g.e.!...
000001de`f3e5cbf0 0a 00 0d 00 0a 00 0d 00-0a 00 00 00 00 00 00 00 ................
000001de`f3e5cc00 00 00 00 00 00 00 00 00-08 00 f1 f7 de 01 00 00 ................
000001de`f3e5cc10 00 00 00 00 00 00 00 00-a6 21 7d 52 c6 98 00 10 .........!}R....
000001de`f3e5cc20 00 d8 e5 f3 de 01 00 00-30 b9 e5 f3 de 01 00 00 ........0.......
000001de`f3e5cc30 10 d8 e5 f3 de 01 00 00-40 b9 e5 f3 de 01 00 00 ........@.......
추가적으로 이 와이드 캐릭터 스트링이 어디에서 생성되었는지 추적해 보려면 rdx 메모리에 대해서 브레이크 포인트를 걸어주면 됩니다.
0:000> ba w1 @rdx
0:000> g-
브레이크 포인트를 확인해 보면, memcpy에 의해서 메모리 카피가 일어 났다는 것을 알 수 있고, 콜스택을 확인하면 COMCTL32!Edit_InsertText가 노트패드의 텍스트를 저장하는 함수라는 것을 알 수 있습니다.
Breakpoint 1 hit
Time Travel Position: 199236:56
msvcrt!memcpy+0x1ec:
00007ffe`96cc4a2c 75f2 jne msvcrt!memcpy+0x1e0 (00007ffe`96cc4a20) [br=0]
0:000> kp 10
# Child-SP RetAddr Call Site
00 0000009e`2394f098 00007ffe`82d69f41 msvcrt!memcpy+0x1ec
01 0000009e`2394f0a0 00007ffe`82e319de COMCTL32!Edit_InsertText+0x10d
02 0000009e`2394f130 00007ffe`82e31286 COMCTL32!EditML_InsertText+0xfa
03 0000009e`2394f200 00007ffe`82dd898f COMCTL32!EditML_Char+0x1aa
04 0000009e`2394f250 00007ffe`82d692c5 COMCTL32!EditML_WndProc+0x6e83f
05 0000009e`2394f3b0 00007ffe`98f9ca66 COMCTL32!Edit_WndProc+0x325
06 0000009e`2394f470 00007ffe`98f9c582 USER32!UserCallWinProcCheckWow+0x266
07 0000009e`2394f5f0 00007ff7`b752448d USER32!DispatchMessageWorker+0x1b2
08 0000009e`2394f670 00007ff7`b753ae07 notepad!WinMain+0x255
09 0000009e`2394f770 00007ffe`974b7e94 notepad!__mainCRTStartup+0x19f
0a 0000009e`2394f830 00007ffe`991c7ad1 KERNEL32!BaseThreadInitThunk+0x14
0b 0000009e`2394f860 00000000`00000000 ntdll!RtlUserThreadStart+0x21
결론
이번화에서는 윈도우즈 플랫폼에서 여러 용도로 사용 가능한 TTD의 사용법에 대해서 간단히 알아 보았습니다. 간략한 기능 소개를 위해서 노트패드라는 대상을 사용하여 함수 추적이나 메모리 추적이 어떻게 가능한지에 대해서 알아 보았습니다. 여기에 나오는 몇몇 작업들은 TTD가 아니면 정말 많은 시간과 노력을 가해야 파악할 수 있는 내용들이 많습니다. 일반적인 디버깅의 경우 일단 어떠한 인스트럭션들이 실행되고 나면 다시 되돌아 가기가 힘들기 때문에 조금의 역추적도 수많은 정적 분석과 함께 많은 시간에 걸쳐 이뤄지는 경우가 많습니다. TTD는 많은 경우 이러한 수고를 낮추어주는 역할을 합니다.
이러한 Time Travel Debugging은 다른그림의 영문 리서치 블로그인 Vulnerability Time Travel Debugging Basics에서 언급한 바와 같이 굉장히 분석이 어려운 Crash 케이스에 대해서도 RCA를 수행하기 위해서 효율적으로 사용 가능합니다. 다음 화에는 이러한 RCA를 위해서 사용 가능한 여러 TTD의 기능과 함께 간단한 예제에 대해서 학습해 보겠습니다.
트레이닝 정보
다른그림은 Threat 인텔리젼스와 지식 제공 회사로서 다음과 같은 TTD 와 관련된 트레이닝을 제공합니다. 많은 관심 바랍니다.