技术文章

了解最新技术文章

当前位置:首页>技术文章>技术文章
全部 5 常见问题 0 技术文章 5

编写 dexdec IR 优化器插件

时间:2022-10-21   访问量:1085

JEB 4.2 开始,用户可以指示dexdec 1加载外部中间表示 (IR) 优化器插件。2

从非常高级的角度来看,安排进行反编译的 Dex 方法会经过以下处理管道:

  1. Dalvik 方法转换为低级 IR

  2. SSA 转换和打字

  3. 红外优化

  4. 最终的高级 IR 转换为 AST

  5. AST 优化

  6. 最终干净的 AST 呈现为伪 Java 代码(注意:已经可以通过 JEB 的 Java AST API 访问 AST)

第 3 阶段包括重复调用 IR 处理器,该处理器实质上接受输入 IR 并将其转换为另一个更精细的 IR(该过程称为“提升”)。IR 处理器的范围从垃圾代码清理器到变量传播、立即传播、常量折叠、更高级别的构造重建、复合谓词重建、代码重组,再到各种混淆消除、可能涉及仿真、动态或符号执行的高级优化器,等等

通过在这个级别上工作,高级用户可以编写自定义反混淆器,我们可能无法将其作为 JEB 内置程序交付,原因有多种(例如,特定于单个文件组的混淆、对文件的自定义保护)根据 NDA 等)。

示例dexdec IR 脚本插件应用自定义反混淆来恢复受保护样本上的字符串

一个示例 dexdec IR 插件

dexdec IR 插件是 JEB 后端插件(不是前端脚本)。因此,它们将被放置在coreplugins文件夹中(或coreplugins/scripts用于插件脚本)。它们可以写成:

在本博客中,我们将展示如何编写 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 插件时,请在此页面上打开一个选项卡!

上面的骨架:

如果您还没有这样做,请启动 JEB。您的插件应该出现在dexdec插件列表中。检查Android菜单,反编译器插件处理程序:

外部 Dex 反编译器插件列表

现在加载一个 dex/apk,并反编译任何类。您的插件最终将被调用。记录器视图应通过显示多条“MARKER – Input IR-CFG: ...”来证明这一点。

dexdec 中间表示

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 监视器释放

语句操作数本身就是IDElements,通常是IDExpressions。示例:(IDImm 立即数)、IDVar (变量)、IDOperation (算术/按位/强制转换操作)、IDInvokeInfo (方法调用细节)、IDArrayElt (表示数组元素)、IDField (表示静态或实例字段)等。IDElement 完整的参考层次结构列表。

IR 语句可以看作是递归的 IR 表达式树。它们可以很容易地被探索(visitXxxmethod())和操作。它们可以被新创建的元素替换(参见IDMethodContext.createXxx方法)。可以对 IR CFG 执行数据流分析,以检索 use-def 和 def-use 链,以及其他可变的活跃度和可达性信息(请参阅 参考资料cfg.doDataFlowAnalysis)。

用例:清理无用的 Android 调用

让我们将这个新的 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是一个字符串解密器。唉,呼叫的存在,例如:

防止通用解密器启动。实际上,模拟器应该如何处理对外部 API 的调用,其结果可能与上下文相关?但在实践中,它们可以通过一些临时优化来解决:

我们将制作以下 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() 调用成为字符串自动解密的候选者,产生以下结果:

我们的外部 IR 插件已启用。可以清洗 IR,进行自动解密。


上一篇:反转 Simatic S7 PLC 程序

下一篇:逆向 Android 应用程序保护器,第 3 部分 - 代码虚拟化

发表评论:

评论记录:

未查询到任何数据!

在线咨询

点击这里给我发消息 售前咨询专员

点击这里给我发消息 售后服务专员

在线咨询

免费通话

24小时免费咨询

请输入您的联系电话,座机请加区号

免费通话

微信扫一扫

微信联系
返回顶部