今天,我们开始小心翼翼地着手对《正当防卫3》(Just Cause 3)进行解包操作,首先……从起点开始,也就是查找原始入口点(OEP,Original Entry Point,即游戏在打包前的入口点)。
有几种或多或少具有通用性的技术可用于查找程序的原始入口点,这并非最复杂的一步。许多学术论文还经常吹嘘自己能够自动脱壳一大堆加壳程序,而实际上它们所做的不过是找出加壳程序的原始入口点而已。这些通用方法通常基于已写入和已执行页面的历史记录:如果内存中的某一页面先被写入然后被执行,那么它极有可能是经过解密/解压/解码的原始代码的一部分。有几种方法可以做到这一点,最简单的方法是修改页面权限,以便写入或执行操作会引发异常。其他方法则采用了一些更高级的技术,如数据污染(data tainting)、模拟等,但这些方法往往有些大材小用……
通常,要找到某个特定加壳程序对应的原始入口点(OEP),较好的方法组合是:对已知在脱壳结束阶段会被调用的API设置钩子(hooks),然后使用VirtualProtect函数修改可能包含OEP的页面(即PE文件中可执行段)的权限。接着,通过在KiUserExceptionDispatcher(或更底层的API以避免被检测到)中设置一个小钩子,或使用结构化异常处理程序(vectored exception handler)来监控异常,从而找到原始入口点。
有时,加壳程序会使用“偷取字节”(stolen bytes)技术,这会使任务变得复杂(OEP处的前X条指令会从其原始位置被擦除、进行多态处理,并放置到分配的内存中),因此严格来说,我们不再有一个单一的OEP。此时,通用方法就不再适用了,需要重建原始代码,找到一种特定的方法来确定加壳程序的代码在哪里结束,以及原始可执行文件的代码在哪里开始,不过这就是另一个故事了……
我们还是言归正传吧。当我要对一个目标程序进行脱壳时,我做的第一件事就是直接运行它,并挂接到该程序上,以便对其代码进行一番探索。为避开反调试(anti-X)机制,最简单的方法就是挂起(暂停)该进程(例如,通过Process Hacker来实现,顺便说一句,它比Process Explorer好用多了)。完成这一操作后,目标程序检测调试器的唯一可能方式就是利用第三方程序(进程、服务、驱动程序)或钩取(hook)DbgUiRemoteBreakin函数。对于第一种情况,只需挂起或卸载所有可能检测到我们的程序(对于驱动程序,WIN64AST特别有用)即可。对于第二种情况,只需在调试器中钩取DbgUiIssueRemoteBreakin函数,以防止其在目标进程中创建线程(说来奇怪,我还没见过哪个插件这样做)。就《正当防卫3》(Just Cause 3)而言,似乎并未设置此类保护机制,因此只需挂起所有Steam进程以及《正当防卫3》进程,然后使用x64dbg挂接到该进程即可。
挂接到进程后,我们可以观察到以下不同情况:
- 《正当防卫3》似乎是用Visual Studio编译的(使用了MSVCR100.dll)。
- 代码似乎位于倒数第二节.data段(具有读、写、执行权限……)。
- 除了输入地址表(IAT)中的两个无效地址外,似乎没有API重定向(???)。
- 通过回溯主线程的调用堆栈,我们很快就能找到入口点,而且似乎没有出现偷取字节(stolen byte)的情况。
另一种快速查找用Visual Studio编译的程序的原始入口点(OEP)的方法,是搜索常量值0x2B992DDFA232。实际上,这是程序开始执行时,在极早阶段初始化的__security_cookie的默认值。在下面的截图(用Paint软件优雅地放大了)中,你可以看到x64dbg(顺便说一句,这是一款出色的调试器)的入口点(EP)及其相关符号,下方则是《正当防卫3》(JC3)的原始入口点(OEP)。我们一眼就能看出它们的相似之处,以及那个著名的常量值。
现在,我们已经找到了原始入口点(OEP),接下来我们希望在该位置设置一个断点,以便让程序处于可转储(dumpable)的状态。然而,这并不像听起来那么容易,原因如下:
- 在游戏加载过程中,可能到处都设置了反调试(anti-X)机制;
- 没有Steam账户就无法玩《正当防卫3》(我觉得这有点糟糕……);
- Steam游戏是由Steam启动的,加载过程涉及多个进程,因此需要跟踪新创建的进程;
- Windows系统下没有简单易用的“跟随子进程创建”(follow fork)模式。
因此,我们将使用Frida!乍一看,Frida这个工具似乎毫无意义,实际上,它是一个框架,允许(除其他功能外)创建Python脚本(也支持JavaScript、QML、Swift、.NET),这些脚本能将用JavaScript编写的C/C++代码(通过v8或duktape引擎)注入到目标程序中,用于逆向汇编代码(支持x86、x86_64、ARM、ARM64架构,还支持更高级的语言,如Objective-C或Dalvik),且适用于Windows(以及Mac、Linux、iOS、Android和……QNX)系统。起初我对此持怀疑态度,但事实证明,选择JavaScript作为脚本语言相当明智(它具有可移植性、依赖性极低、存在大量纯JavaScript库、原生支持异步等优点)。此外,Frida的代码编写得极为出色,使用起来非常愉快,而且其开发者oleavr非常平易近人且乐于助人。不过,闲话少说,Frida本身并不支持进程跟踪功能,因此我们需要自己实现这一功能。
其原理相对简单:我们只需启动Steam,并将Frida附加到其上;一旦有新进程创建,我们就将Frida注入到该进程中。为此,我们钩取(hook)CreateProcessInternalW函数,每当有进程创建时,就从JavaScript代码向我们的Python脚本发送一个事件。然后,Python脚本会使用WinAppDbg在RtlUserThreadStart上设置一个断点指令(EBFE)。这样,在新进程初始化完成且新进程的入口点被调用之前,我们就能将Frida注入到该进程中。最后,我们向父进程发送一个消息,告知其可以继续执行。在处理32位和64位进程(即Steam进程与游戏进程)时,有一些需要注意的细节,但总体来说处理得还算顺利……Frida还存在一个小错误(目前正在测试补丁),它无法正确附加到以“不受信任”(untrusted)完整性级别启动的进程上,而Steam的某些进程(特别是那些显示网页内容的进程)正是这种情况。不过幸运的是,这个错误并不妨碍我们启动游戏。
现在,我们已经成功将Frida注入到所有进程(包括《正当防卫3》的游戏进程)中,接下来只需钩取(hook)一个在进程执行初期就会被调用的API,就能达到我们的目的。我随意选择了RtlQueryPerformanceCounter这个API进行钩取,它同样被用于计算__security_cookie的值。
以下是相关代码(首先是Python脚本,然后是JavaScript代码(是的,我知道在博客文章里直接内嵌代码很傻,尤其是我还有GitHub账号,但我才不在乎呢)):
- # coding: utf-8
- import frida
- import threading
- import sys
- import os
- from winappdbg import System, Process, HexDump
- from pprint import pprint
- from time import sleep
-
- sessions = []
- PIDs = []
- threads = []
- continue_events = []
-
- system = System()
-
- RtlUserThreadStart = {}
-
- def get_RtlUserThreadStart_addr(path, ntdll_path):
- pid = frida.spawn((path,))
- session = frida.attach(pid)
- for m in session.enumerate_modules() :
- if m.path.lower().endswith(ntdll_path) :
- for x in m.enumerate_exports() :
- if x.name == 'RtlUserThreadStart' :
- break
- else :
- session.detach()
- frida.kill(pid)
- raise Exception('unable to find RtlUserThreadStart')
- break
- else :
- session.detach()
- frida.kill(pid)
- raise Exception('unable to find ntdll')
- session.detach()
- frida.kill(pid)
- return x.relative_address + m.base_address
-
- RtlUserThreadStart[32] =get_RtlUserThreadStart_addr('%s\\SysWow64\\notepad.exe'%os.environ['WINDIR'], 'syswow64\\ntdll.dll')
- RtlUserThreadStart[64] =get_RtlUserThreadStart_addr('%s\\notepad.exe'%os.environ['WINDIR'], 'system32\\ntdll.dll')
-
- def follow_proc_callback(script):
- def follow_proc_callback_priv(message, data) :
- if message['type'] == 'error' :
- pprint(message)
- print message['stack']
- return
- elif message['type'] == 'send' :
- payload = message['payload']
- event = payload.get('event', '')
- if event == 'new process' :
- print 'New process!'
- pid = payload['PID']
- creation_flags = payload['creation_flags']
- p = Process(pid)
- bps = {}
- for bp_address in RtlUserThreadStart.values() :
- if p.is_address_readable(bp_address) :
- bps[bp_address] = p.read(bp_address, 2)
- p.write(bp_address, '\xEB\xFE')
- t = p.get_thread(p.get_thread_ids()[0])
- p.resume()
- while not bps.has_key(t.get_pc()) :
- sleep(0.1)
- p.suspend()
- for addr, v in bps.items() :
- p.write(addr, v)
- follow_proc(pid, js, follow_proc_callback)
- if creation_flags & 0x4 == 0 :
- p.resume()
- script.post({'type': 'continue_%d'%pid})
- return
- elif event == 'possible OEP' :
- continue_events.append((script, payload['continue_event']))
- print "We reached the OEP (%s-%s), you can know attach your debugger (or press r to resume the process)"%(payload['OEP'], payload['OEP_VA'])
- return
- raise Exception('unknown message')
- return follow_proc_callback_priv
-
- def follow_proc(pid, js, callback) :
- PIDs.append(pid)
- session = frida.attach(pid)
- session.disable_jit()
- sessions.append(session)
- script = session.create_script(js)
- script.on('message', callback(script))
- script.load()
-
- with open("find_oep.js") as f :
- js = f.read()
-
- pid = frida.spawn(("C:\\Program Files (x86)\\Steam\\Steam.exe",))
- follow_proc(pid, js, follow_proc_callback)
- frida.resume(pid)
-
- try :
- cmd = ''
- while cmd != 'q' :
- cmd = sys.stdin.readline().strip().lower()
- if cmd == 'r' :
- for script, code in continue_events :
- script.post({'type': code})
- except :
- pass
-
- map(lambda s: s.detach(), sessions)
- for pid in PIDs :
- try :
- Process(pid).kill()
- except :
- pass
复制代码
- "use strict";
- var QueryPerformanceCounter = Module.findExportByName('kernel32', 'QueryPerformanceCounter')
- var CreateProcessInternalW = Module.findExportByName('kernel32', 'CreateProcessInternalW')
- var SetTokenInformation = Module.findExportByName('kernel32', 'SetTokenInformation')
- if (SetTokenInformation == null)
- SetTokenInformation = Module.findExportByName('kernelbase', 'SetTokenInformation')
- var RtlQueryPerformanceCounter = Module.findExportByName('ntdll', 'RtlQueryPerformanceCounter')
-
- var CREATE_SUSPENDED = 0x00000004
-
- Interceptor.attach(CreateProcessInternalW, {
- onEnter: function (args) {
- console.log('new process created!');
- if (args[1] != NULL)
- console.log('\tlpApplicationName: '+Memory.readUtf16String(args[1]));
- if (args[2] != NULL)
- console.log('\tlpCommandLine: '+Memory.readUtf16String(args[2]));
- this.lpProcessInformation = args[10];
- console.log('\tlpProcessInformation: '+this.lpProcessInformation);
- this.dwCreationFlags = args[6];
- console.log('\tdwCreationFlags: '+this.dwCreationFlags);
- args[6] = args[6].or(CREATE_SUSPENDED);
- },
- onLeave: function (retval) {
- if (retval.toInt32()) {
- var PID = Memory.readU32(this.lpProcessInformation.add(8))
- var hProcess = Memory.readU32(this.lpProcessInformation)
- console.log('CreateProcessInternalW SUCCEED: hProcess='+hProcess.toString(16)+' PID='+PID);
- send({'event': 'new process', 'PID': PID, 'creation_flags': this.dwCreationFlags.toInt32()})
- var sync_op = recv('continue_'+PID, function(value) {});
- sync_op.wait();
- console.log('Resuming process...');
- } else {
- console.log('CreateProcessInternalW FAILED!');
- }
- }
- });
-
-
- var just_cause = Process.findModuleByName('JustCause3.exe');
- if (just_cause !== null) {
- console.log('We are in JustCause3!');
- var OEPs = new Set();
- var just_cause_start = just_cause.base;
- var just_cause_end = just_cause.base.add(just_cause.size);
- Interceptor.attach(RtlQueryPerformanceCounter, {
- onEnter: function (args) {
- if ((just_cause_start.compare(this.returnAddress) <= 0) && (just_cause_end.compare(this.returnAddress) > 0) && (! OEPs.has(''+this.returnAddress))) {
- var OEP_VA = this.returnAddress.sub(just_cause.base);
- var continue_event = 'continue_'+this.threadId;
- console.log('Possible OEP: '+this.returnAddress+' (VA: '+OEP_VA+')');
- send({'event': 'possible OEP', 'OEP': this.returnAddress, 'OEP_VA': OEP_VA, 'continue_event': continue_event})
- var sync_op = recv(continue_event, function(value) {});
- sync_op.wait();
- OEPs.add(''+this.returnAddress);
- }
- }
- });
- }
复制代码
原文链接
|