找回密码
 立即注册
搜索
查看: 141|回复: 0

[翻译] 查找OEP

[复制链接]

251

主题

0

回帖

2345

积分

管理员

积分
2345
发表于 2025-11-4 11:44:28 | 显示全部楼层 |阅读模式
    今天,我们开始小心翼翼地着手对《正当防卫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.png

    现在,我们已经找到了原始入口点(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账号,但我才不在乎呢)):
  1. # coding: utf-8
  2. import frida
  3. import threading
  4. import sys
  5. import os
  6. from winappdbg import System, Process, HexDump
  7. from pprint import pprint
  8. from time import sleep
  9. sessions = []
  10. PIDs = []
  11. threads = []
  12. continue_events = []
  13. system = System()
  14. RtlUserThreadStart = {}
  15. def get_RtlUserThreadStart_addr(path, ntdll_path):
  16.     pid = frida.spawn((path,))
  17.     session = frida.attach(pid)
  18.     for m in session.enumerate_modules() :
  19.         if m.path.lower().endswith(ntdll_path) :
  20.             for x in m.enumerate_exports() :
  21.                 if x.name == 'RtlUserThreadStart' :
  22.                     break
  23.             else :
  24.                 session.detach()
  25.                 frida.kill(pid)
  26.                 raise Exception('unable to find RtlUserThreadStart')
  27.             break
  28.     else :
  29.         session.detach()
  30.         frida.kill(pid)
  31.         raise Exception('unable to find ntdll')
  32.     session.detach()
  33.     frida.kill(pid)
  34.     return x.relative_address + m.base_address
  35. RtlUserThreadStart[32] =get_RtlUserThreadStart_addr('%s\\SysWow64\\notepad.exe'%os.environ['WINDIR'], 'syswow64\\ntdll.dll')
  36. RtlUserThreadStart[64] =get_RtlUserThreadStart_addr('%s\\notepad.exe'%os.environ['WINDIR'], 'system32\\ntdll.dll')
  37. def follow_proc_callback(script):
  38.     def follow_proc_callback_priv(message, data) :
  39.         if message['type'] == 'error' :
  40.             pprint(message)
  41.             print message['stack']
  42.             return
  43.         elif message['type'] == 'send' :
  44.             payload = message['payload']
  45.             event = payload.get('event', '')
  46.             if event == 'new process' :
  47.                 print 'New process!'
  48.                 pid = payload['PID']
  49.                 creation_flags = payload['creation_flags']
  50.                 p = Process(pid)
  51.                 bps = {}
  52.                 for bp_address in RtlUserThreadStart.values() :
  53.                     if p.is_address_readable(bp_address) :
  54.                         bps[bp_address] = p.read(bp_address, 2)
  55.                         p.write(bp_address, '\xEB\xFE')
  56.                 t = p.get_thread(p.get_thread_ids()[0])
  57.                 p.resume()
  58.                 while not bps.has_key(t.get_pc()) :
  59.                     sleep(0.1)
  60.                 p.suspend()
  61.                 for addr, v in bps.items() :
  62.                     p.write(addr, v)
  63.                 follow_proc(pid, js, follow_proc_callback)
  64.                 if creation_flags & 0x4 == 0 :
  65.                     p.resume()
  66.                 script.post({'type': 'continue_%d'%pid})
  67.                 return
  68.             elif event == 'possible OEP' :
  69.                 continue_events.append((script, payload['continue_event']))
  70.                 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'])
  71.                 return
  72.         raise Exception('unknown message')
  73.     return follow_proc_callback_priv
  74. def follow_proc(pid, js, callback) :
  75.     PIDs.append(pid)
  76.     session = frida.attach(pid)
  77.     session.disable_jit()
  78.     sessions.append(session)
  79.     script = session.create_script(js)
  80.     script.on('message', callback(script))
  81.     script.load()
  82. with open("find_oep.js") as f :
  83.     js = f.read()
  84. pid = frida.spawn(("C:\\Program Files (x86)\\Steam\\Steam.exe",))
  85. follow_proc(pid, js, follow_proc_callback)
  86. frida.resume(pid)
  87. try :
  88.     cmd = ''
  89.     while cmd != 'q' :
  90.         cmd = sys.stdin.readline().strip().lower()
  91.         if cmd == 'r' :
  92.             for script, code in continue_events :
  93.                 script.post({'type': code})
  94. except :
  95.     pass
  96. map(lambda s: s.detach(), sessions)
  97. for pid in PIDs :
  98.     try :
  99.         Process(pid).kill()
  100.     except :
  101.         pass
复制代码
  1. "use strict";
  2. var QueryPerformanceCounter = Module.findExportByName('kernel32', 'QueryPerformanceCounter')
  3. var CreateProcessInternalW = Module.findExportByName('kernel32', 'CreateProcessInternalW')
  4. var SetTokenInformation = Module.findExportByName('kernel32', 'SetTokenInformation')
  5. if (SetTokenInformation == null)
  6.     SetTokenInformation = Module.findExportByName('kernelbase', 'SetTokenInformation')
  7. var RtlQueryPerformanceCounter = Module.findExportByName('ntdll', 'RtlQueryPerformanceCounter')
  8. var CREATE_SUSPENDED = 0x00000004
  9. Interceptor.attach(CreateProcessInternalW, {
  10.     onEnter: function (args) {
  11.         console.log('new process created!');
  12.         if (args[1] != NULL)
  13.             console.log('\tlpApplicationName: '+Memory.readUtf16String(args[1]));
  14.         if (args[2] != NULL)
  15.             console.log('\tlpCommandLine: '+Memory.readUtf16String(args[2]));
  16.         this.lpProcessInformation = args[10];
  17.         console.log('\tlpProcessInformation: '+this.lpProcessInformation);
  18.         this.dwCreationFlags = args[6];
  19.         console.log('\tdwCreationFlags: '+this.dwCreationFlags);
  20.         args[6] = args[6].or(CREATE_SUSPENDED);
  21.     },
  22.     onLeave: function (retval) {
  23.             if (retval.toInt32()) {
  24.                 var PID = Memory.readU32(this.lpProcessInformation.add(8))
  25.                 var hProcess = Memory.readU32(this.lpProcessInformation)
  26.                 console.log('CreateProcessInternalW SUCCEED: hProcess='+hProcess.toString(16)+' PID='+PID);
  27.                 send({'event': 'new process', 'PID': PID, 'creation_flags': this.dwCreationFlags.toInt32()})
  28.                 var sync_op = recv('continue_'+PID, function(value) {});
  29.                 sync_op.wait();
  30.                 console.log('Resuming process...');
  31.             } else {
  32.                 console.log('CreateProcessInternalW FAILED!');
  33.             }
  34.     }
  35. });
  36. var just_cause = Process.findModuleByName('JustCause3.exe');
  37. if (just_cause !== null) {
  38.     console.log('We are in JustCause3!');
  39.     var OEPs = new Set();
  40.     var just_cause_start = just_cause.base;
  41.     var just_cause_end = just_cause.base.add(just_cause.size);
  42.     Interceptor.attach(RtlQueryPerformanceCounter, {
  43.         onEnter: function (args) {
  44.             if ((just_cause_start.compare(this.returnAddress) <= 0) && (just_cause_end.compare(this.returnAddress) > 0) && (! OEPs.has(''+this.returnAddress))) {
  45.                 var OEP_VA = this.returnAddress.sub(just_cause.base);
  46.                 var continue_event = 'continue_'+this.threadId;
  47.                 console.log('Possible OEP: '+this.returnAddress+' (VA: '+OEP_VA+')');
  48.                 send({'event': 'possible OEP', 'OEP': this.returnAddress, 'OEP_VA': OEP_VA, 'continue_event': continue_event})
  49.                 var sync_op = recv(continue_event, function(value) {});
  50.                 sync_op.wait();
  51.                 OEPs.add(''+this.returnAddress);
  52.             }
  53.         }
  54.     });
  55. }
复制代码

原文链接










您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表