了解最新技术文章
从 JEB 4.2 开始,用户可以指示dexdec 1加载外部中间表示 (IR) 优化器插件。2
从非常高级的角度来看,安排进行反编译的 Dex 方法会经过以下处理管道:
Dalvik 方法转换为低级 IR
SSA 转换和打字
红外优化
最终的高级 IR 转换为 AST
AST 优化
最终干净的 AST 呈现为伪 Java 代码(注意:已经可以通过 JEB 的 Java AST API 访问 AST)
第 3 阶段包括重复调用 IR 处理器,该处理器实质上接受输入 IR 并将其转换为另一个更精细的 IR(该过程称为“提升”)。IR 处理器的范围从垃圾代码清理器到变量传播、立即传播、常量折叠、更高级别的构造重建、复合谓词重建、代码重组,再到各种混淆消除、可能涉及仿真、动态或符号执行的高级优化器,等等
通过在这个级别上工作,高级用户可以编写自定义反混淆器,我们可能无法将其作为 JEB 内置程序交付,原因有多种(例如,特定于单个文件组的混淆、对文件的自定义保护)根据 NDA 等)。
dexdec IR 插件是 JEB 后端插件(不是前端脚本)。因此,它们将被放置在coreplugins
文件夹中(或coreplugins/scripts
用于插件脚本)。它们可以写成:
预编译的 jar 文件:源语言可以是任何编译成 Java 字节码的语言;这些插件不能热插拔,因此不适合原型设计/实验;不过,它们非常适合成熟的插件。
Java 插件脚本:单个 Java 源文件。与 Javadoc 的强类型和 IDE 集成(例如与 Eclipse 或 IntelliJ)使其成为开发复杂插件的理想选择。支持热重载。(它们可以在 JEB 运行时无缝修改,非常适合原型设计。)
Python 插件脚本:用 2.7 语法编写。支持热重载。限制:与其他插件不同,一个 Python 脚本插件的实例可能被多个反编译线程共享。因此,它们必须是线程安全的并支持并发。
在本博客中,我们将展示如何编写 Python 插件脚本。熟悉 JEB 客户端脚本的用户将处于熟悉的领域。
重要的!请注意,在 JEB 中默认情况下不启用加载此类插件。将以下行添加到您的bin/jeb-engines.cfg
文件以启用加载 Python 插件:.LoadPythonPlugins = true
dexdec ir 插件必须实现该IDOptimizer
接口。在实践中,强烈建议扩展实现类AbstractDOptimizer
,如下所示:
1 2 3 4 5 6 7 8 9 | from com.pnfsoftware.jeb.core.units.code.android.ir import AbstractDOptimizer # sample IR plugin, does nothing but log the IR CFG class DOptSamplePython(AbstractDOptimizer): # perform() returns the number of optimizations performed def perform( self ): self .logger.info( 'MARKER - Input IR-CFG: %s' , self .cfg) return 0 |
重要的!所有dexdec IR 公共接口和类型都位于com.pnfsoftware.jeb.core.units.code.android.ir包中。开发 IR 插件时,请在此页面上打开一个选项卡!
上面的骨架:
必须与插件类具有相同的文件名,因此DOptSamplePython.py
必须放在coreplugins/scripts/
需要在引擎配置中启用 Python 脚本插件
如果您还没有这样做,请启动 JEB。您的插件应该出现在dexdec插件列表中。检查Android菜单,反编译器插件处理程序:
现在加载一个 dex/apk,并反编译任何类。您的插件最终将被调用。记录器视图应通过显示多条“MARKER – Input IR-CFG: ...”行来证明这一点。
dexdec的 IR 由IDElement
对象组成。每个 IR 语句都是一个IDInstruction
,本身就是一个IDElement
. (所有这些类型及其属性都在 API 文档中进行了深入描述。)当调用 IR 插件时,它“接收”一个IDMethodContext
(表示反编译的方法),存储在优化器的ctx 公共字段中。IR CFG 是一个由 IR 语句组成的控制流图,可以通过ctx.getCfg()
. 为方便起见,它也存储在cfg 公共字段中。格式化的 IR CFG 可能如下所示:
1 2 3 4 5 6 7 8 | 0000/2+ !onCreate(v4<com.pnfsoftware.raasta.AppHelp>, v5<android.os.Bundle>)<void> 0002/2: !requestWindowFeature(v4<com.pnfsoftware.raasta.AppHelp>, 1)<boolean> 0004/3: !setContentView(v4<com.pnfsoftware.raasta.AppHelp>, 7F030000)<void> 0007/5: !x4<android.webkit.WebView> = ((android.webkit.WebView)findViewById(v4<com.pnfsoftware.raasta.AppHelp>, 7F070000)<android.view.View>)<android.webkit.WebView> 000C/2: !loadData(x4<android.webkit.WebView>, getString(v4<com.pnfsoftware.raasta.AppHelp>, 7F05005B)<java.lang.String>, "text/html", "utf-8")<void> 000E/3: !setBackgroundColor(x4<android.webkit.WebView>, 0)<void> 0011/1: !setDefaultTextEncodingName(getSettings(x4<android.webkit.WebView>)<android.webkit.WebSettings>, "utf-8")<void> 0012/1: return |
语句 ( IDInstruction
) 可以具有以下任何操作码(请参阅DOpcodeType
):
– IR_NOP:无操作
– IR_ASSIGN:赋值
– IR_INVOKE:调用(包括新对象和新数组构造)
– IR_JUMP:无条件跳转
– IR_JCOND:有条件跳转
– IR_SWITCH: switch 语句
– IR_RETURN:return 语句
– IR_THROW:throw 语句
– IR_STORE_EXCEPTION:异常检索(特殊)
– IR_MONITOR_ENTER:VM 监视器获取
– IR_MONITOR_EXIT:VM 监视器释放
语句操作数本身就是IDElement
s,通常是IDExpression
s。示例:(IDImm
立即数)、IDVar
(变量)、IDOperation
(算术/按位/强制转换操作)、IDInvokeInfo
(方法调用细节)、IDArrayElt
(表示数组元素)、IDField
(表示静态或实例字段)等。IDElement
完整的参考层次结构列表。
IR 语句可以看作是递归的 IR 表达式树。它们可以很容易地被探索(visitXxx
method())和操作。它们可以被新创建的元素替换(参见IDMethodContext.createXxx
方法)。可以对 IR CFG 执行数据流分析,以检索 use-def 和 def-use 链,以及其他可变的活跃度和可达性信息(请参阅 参考资料cfg.doDataFlowAnalysis
)。
让我们将这个新的 API 用于实际的、真实的世界中使用。首先,一些背景知识:JEB 附带了模拟器支持的 IR 优化器,它们试图自动解密诸如字符串之类的立即数。虽然这个反混淆器通常在受保护的文件上表现良好,但最近,我们收到了字符串未解密的样本。原因很简单,看这个例子:
throw new java.lang.IllegalStateException(o.isUserRecoverableError.read(((char)android.text.TextUtils.getOffsetBefore("", 0)), 12 - java.lang.Long.compare(android.os.Process.getElapsedCpuTime(), 0L), (android.view.ViewConfiguration.getFadingEdgeLength() >> 16) + 798).intern());
在上面的代码中(从受保护的方法中提取),read
是一个字符串解密器。唉,呼叫的存在,例如:
TextUtils.getOffsetBefore(“”, 0))
Long.compare(Process.getElapsedCpuTime(), 0L)
ViewConfiguration.getFadingEdgeLength() >> 16
防止通用解密器启动。实际上,模拟器应该如何处理对外部 API 的调用,其结果可能与上下文相关?但在实践中,它们可以通过一些临时优化来解决:
getOffsetBefore() 算法(几乎)很简单
getElapsedCpuTime() 也返回严格肯定的结果,使 compare() 操作可预测
getFadingEdgeLength() 返回小于 0x10000 的小整数
我们将制作以下 IR 优化器:(文件RemoveDummyAndroidApiCalls.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | from com.pnfsoftware.jeb.core.units.code.android.ir import AbstractDOptimizer, IDVisitor class RemoveDummyAndroidApiCalls(AbstractDOptimizer): # note that we extend AbstractDOptimizer for convenience, instead of implementing IDOptimizer from scratch def perform( self ): # create our instruction visitor vis = AndroidUtilityVisitor( self .ctx) # visit all the instructions of the IR CFG for insn in self .cfg.instructions(): insn.visitInstruction(vis) # return the count of replacements return vis.cnt class AndroidUtilityVisitor(IDVisitor): def __init__( self , ctx): self .ctx = ctx self .cnt = 0 def process( self , e, parent, results): repl = None if e.isCallInfo(): sig = e.getMethodSignature() # TextUtils.getOffsetBefore("", 0) if sig = = 'Landroid/text/TextUtils;->getOffsetBefore(Ljava/lang/CharSequence;I)I' and e.getArgument( 0 ).isImm() and e.getArgument( 1 ).isImm(): buf = e.getArgument( 0 ).getStringValue( self .ctx.getGlobalContext()) val = e.getArgument( 1 ).toLong() if buf = = '' and val = = 0 : repl = self .ctx.getGlobalContext().createInt( 0 ) # Long.compare(xxx, 0) elif sig = = 'Ljava/lang/Long;->compare(JJ)I' and e.getArgument( 1 ).isImm() and e.getArgument( 1 ).asImm().isZeroEquivalent(): val0 = None arg0 = e.getArgument( 0 ) if arg0.isCallInfo(): sig2 = arg0.getMethodSignature() if sig2 = = 'Landroid/os/Process;->getElapsedCpuTime()J' : # elapsed time always >0, value does not matter since we are comparing against 0 val0 = 1 if val0 ! = None : if val0 > 0 : r = 1 elif val0 < 0 : r = - 1 else : r = 0 repl = self .ctx.getGlobalContext().createInt(r) # ViewConfiguration.getFadingEdgeLength() elif sig = = 'Landroid/view/ViewConfiguration;->getFadingEdgeLength()I' : # always a small positive integer, normally set to FADING_EDGE_LENGTH (12) repl = self .ctx.getGlobalContext().createInt( 12 ) if repl ! = None and parent.replaceSubExpression(e, repl): # success (this visitor is pre-order, we need to report the replaced node) results.setReplacedNode(repl) self .cnt + = 1 |
这段代码做了什么:
– 首先,它枚举并访问所有 CFG 指令。
– 访问者检查IDCallInfo
与上述各种 Android 框架 API 调用匹配的 IR 表达式:getOffsetBefore()、compare(getElapsedCpuTime(), 0)、getFadingEdgeLength()
– 它评估和计算结果,并将 IR 调用表达式 ( IDInvokeInfo
)替换为新创建的常量 ( IDImm
)。
插件可以打印的结果 IR 如下所示:
throw new java.lang.IllegalStateException(o.isUserRecoverableError.read(((char)0, 12 - 1, 0 + 798).intern());
随后,内置在dexdec中的其他优化器可以启动,进一步清理代码(例如折叠常量),并使 read() 调用成为字符串自动解密的候选者,产生以下结果: