笔者的ida版本是ida7.7, 使用ida64 打开样本so, 看看 Jni_onload 函数
可以看到只有一个jumpout
看看 jni_onload 函数的起始部分
图中两个红色框框是一个对称的操作, stp 相当于 push to stack, 而 ldp 相当于 pop
STP X0, X1, [SP,#-32]!
STP X2, X30, [SP,#16]
这两行汇编是开栈操作. 执行后寄存器在堆栈中的分布如下:
地址 | 寄存器 |
---|---|
低地址 | x0 |
x1 | |
x2 | |
高地址 | x30 |
这里介绍一个插件 可以帮助理解一些指令. 可以帮助验证猜想.
如果对于指令不熟悉的话,可以下载一个ida的模拟执行的插件, 执行完后,你可以看到很清晰的堆栈以及各个寄存器的值.
github地址 https://github.com/alexhude/uEmu
1 | git clone https://github.com/alexhude/uEmu |
然后 ida 左上角 File -> Script file -> 选择仓库中的 uEmu.py 文件. 或者也可以直接当插件使用. 放到 plugin目录中
然后我们在 jni_onload 的第一行汇编代码处右键, 可以看到 uemu 弹出.
点击 start 弹出如下窗口
点击 No, 然后会弹出如下窗口, 我们鼠标右键编辑寄存器的值. 将 x0 改为 0x12345, x1 改为 0x54321, sp改为 0x1000
然后点击下一步
执行完后我们看看堆栈的内容, 按如下方案操作即可查看堆栈内容
堆栈内容如下
可以看到x0 和 x1 在堆栈中的位置 正如我们上面画的图一样. 至此 uemu的使用就讲到这里. 还有一些好用的功能,比如修改寄存器值的时候可以批量 load和save, 这样就不用每次设置值了. 亲测好用
回过头来继续分析指令
中间指令的解析如下, 注意看备注
1 | ADR X1, 0x4ABDC 注释: 这条指令实际是ida 优化过后的指令, 真实实际指令是 ADR X1, 0x20. 相当于 x1 = pc + 0x20, 结果就是 x1 = 0x4ABBC + 0x20 = 0x4abdc 这条指令执行完后 x1 = 0x4abdc |
接下来是恢复堆栈
LDP X2, X9, [SP,#16]
LDP X0, X1, [SP],#0x20
相当于把 X0 给了 X9
然后 br x9, 就相当于 branch register 0x4ac0c.
所以ida会显示 jumpout 0x4ac0c
其实图中的所有代码的作用只是 跳转到真实地址 0x4ac0c
我们ida 跳转到 0x4ac0c 看看
可以看到飘红, 并且ida没有正确识别出函数. 这时候需要手动帮助ida进行识别.
很明显 SUB SP, SP, #0xC0 这个是函数的序言(开始)部分, 一般函数的开头都长这样.
我们选中这几行, 然后按快捷键 p 创建函数. 然后再次 F5
嘿嘿嘿 大功告成!
不过看 jni_onload 有很明显的虚假控制流的混淆混迹
我们按照之前介绍的方法 create segment + only read + patch 试试
点击看看这个变量, 是在 bss段中, 并且按X交叉引用都是读操作 并没有写操作.
这时我们 shift + f7 查看所有的 segment, 右键新增一个 segment, 然后把这两个变量所在的内存区域放在新建的 segment中, 如下
要注意新增的段的 end_segment 是开区间, 是取不到的, 因为 0x77C28 这个地址属于 .prgend 段
然后编辑 my_segment 只勾选 只读
然后 F5看一下, 貌似并没有效果.
等等, 我们似乎忘记了 patch 0
执行如下 idc脚本 进行patch, 将 dword_77C20 和 dword_77C24 置为0
1 | idc.patch_dword(0x77C20, 0) |
然后继续 F5 看看效果
嘿嘿嘿嘿嘿, 去 bcf 成功
在找 jni_onload 函数中的register native 方法的时候, 发现了fla的混淆
按照之前文章介绍的去fla的方法去混淆, 效果如下
发现样本中 不止一处花指令, 基本都是这样插花方式, 我们需要批量 patch
首先是要找出所有的花指令, 然后跳转到正确的地址, 然后nop掉下面的代码.
首先通过二进制搜索, 找到所有的
STP X0, X1, [SP,#var_20]!
STP X2, X30, [SP,#0x20+var_10] 对应的机器码
1 | import ida_bytes |
然后对这些地址进行反汇编, 将机器码转换为 汇编代码
1 | def makeInsn(addr): |
然后计算出要跳转的真实的地址
1 | def getJumpAddress(addr): |
然后就是构造出跳转指令例如 b 0x123
然后 将 b 0x123 转换为 二进制. 可以通过 keystone 实现
1 | import keystone |
最后通过 ida_bytes.patch_bytes(addr, bytes(bCode)) 设置正确的跳转地址
然后通过 ida_bytes.patch_bytes(addr + 4, bytes(nopCode) * 9) 将剩下的9条指令 nop 掉
最后进行 patch
1 | for addr in matches: |
patch 后发现没有花指令的痕迹了. ida搜索一下 STP X0, X1, [SP,#var_20]!, 发现空空如也
来看看处理了 花指令后的 jni_onload
yyds