某视频类app 花指令、ollvm bcf、fla 处理

笔者的ida版本是ida7.7, 使用ida64 打开样本so, 看看 Jni_onload 函数

img_1.png

可以看到只有一个jumpout
看看 jni_onload 函数的起始部分
img_1.png

图中两个红色框框是一个对称的操作, 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 弹出.
img_1.png

点击 start 弹出如下窗口
img_1.png

点击 No, 然后会弹出如下窗口, 我们鼠标右键编辑寄存器的值. 将 x0 改为 0x12345, x1 改为 0x54321, sp改为 0x1000
img_1.png

然后点击下一步
img_1.png

执行完后我们看看堆栈的内容, 按如下方案操作即可查看堆栈内容
img_1.png

堆栈内容如下
img_1.png

可以看到x0 和 x1 在堆栈中的位置 正如我们上面画的图一样. 至此 uemu的使用就讲到这里. 还有一些好用的功能,比如修改寄存器值的时候可以批量 load和save, 这样就不用每次设置值了. 亲测好用
img_1.png

回过头来继续分析指令

中间指令的解析如下, 注意看备注

1
2
3
4
5
ADR X1, 0x4ABDC  注释: 这条指令实际是ida 优化过后的指令, 真实实际指令是 ADR X1, 0x20. 相当于 x1 = pc + 0x20, 结果就是 x1 = 0x4ABBC + 0x20 = 0x4abdc 这条指令执行完后 x1 = 0x4abdc
SUBS X1, X1, #4 注释: 相当于 x1 = x1 - 4 = 0x4abd8
MOV X0, X1 注释: x0 = x1 x0 = 0x4abd8
ADDS X0, X0, #0x34 注释: x0 = x0 + 0x34 = 0x4ac0c
STR X0, [SP,#24] 注释: 把x0 写入sp+24的位置. 相当于 x0 把 x30 覆盖了

接下来是恢复堆栈
LDP X2, X9, [SP,#16]
LDP X0, X1, [SP],#0x20

相当于把 X0 给了 X9

然后 br x9, 就相当于 branch register 0x4ac0c.
所以ida会显示 jumpout 0x4ac0c
其实图中的所有代码的作用只是 跳转到真实地址 0x4ac0c
我们ida 跳转到 0x4ac0c 看看

img_1.png

可以看到飘红, 并且ida没有正确识别出函数. 这时候需要手动帮助ida进行识别.
很明显 SUB SP, SP, #0xC0 这个是函数的序言(开始)部分, 一般函数的开头都长这样.

img_1.png

我们选中这几行, 然后按快捷键 p 创建函数. 然后再次 F5

img_1.png

嘿嘿嘿 大功告成!

不过看 jni_onload 有很明显的虚假控制流的混淆混迹

img_1.png

我们按照之前介绍的方法 create segment + only read + patch 试试
点击看看这个变量, 是在 bss段中, 并且按X交叉引用都是读操作 并没有写操作.

这时我们 shift + f7 查看所有的 segment, 右键新增一个 segment, 然后把这两个变量所在的内存区域放在新建的 segment中, 如下
img_1.png

要注意新增的段的 end_segment 是开区间, 是取不到的, 因为 0x77C28 这个地址属于 .prgend 段

然后编辑 my_segment 只勾选 只读
img_1.png

然后 F5看一下, 貌似并没有效果.

等等, 我们似乎忘记了 patch 0
执行如下 idc脚本 进行patch, 将 dword_77C20 和 dword_77C24 置为0

1
2
idc.patch_dword(0x77C20, 0)
idc.patch_dword(0x77C24, 0)

然后继续 F5 看看效果
img_1.png

嘿嘿嘿嘿嘿, 去 bcf 成功

在找 jni_onload 函数中的register native 方法的时候, 发现了fla的混淆

img_1.png

按照之前文章介绍的去fla的方法去混淆, 效果如下
img_1.png

发现样本中 不止一处花指令, 基本都是这样插花方式, 我们需要批量 patch

首先是要找出所有的花指令, 然后跳转到正确的地址, 然后nop掉下面的代码.

首先通过二进制搜索, 找到所有的
STP X0, X1, [SP,#var_20]!
STP X2, X30, [SP,#0x20+var_10] 对应的机器码

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
import ida_bytes
import idaapi
import idc


def binSearch(start, end, pattern):
matches = []
addr = start
if end == 0:
end = idc.BADADDR
if end != idc.BADADDR:
end = end + 1
while True:
addr = ida_bytes.bin_search(addr, end, bytes.fromhex(pattern), None, idaapi.BIN_SEARCH_FORWARD,
idaapi.BIN_SEARCH_NOCASE)
if addr == idc.BADADDR:
break
else:
matches.append(addr)
addr = addr + 1
return matches
matches = binSearch(0, 0, "E0 07 BE A9 E2 7B 01 A9")
print(len(matches)) # 119

for item in matches:
print(hex(item))

然后对这些地址进行反汇编, 将机器码转换为 汇编代码

1
2
3
4
5
def makeInsn(addr):
if idc.create_insn(addr) == 0:
idc.del_items(addr, idc.DELIT_EXPAND)
idc.create_insn(addr)
idc.auto_wait()

然后计算出要跳转的真实的地址

1
2
3
4
5
def getJumpAddress(addr):
A = idc.get_operand_value(addr + 8, 1) # 第三行的第2个操作数
B = idc.get_operand_value(addr + 12, 2) # 第四行的第3个操作数
C = idc.get_operand_value(addr + 20, 2)
return A - B + C

然后就是构造出跳转指令例如 b 0x123
然后 将 b 0x123 转换为 二进制. 可以通过 keystone 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import keystone
from keystone import *
import ida_bytes
import idaapi
import idc

def generate(code, addr):
ks = Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
# 参数2是地址,很多指令是地址相关的,比如 B 指令,如果地址无关直接传 0 即可,比如 nop。
encoding, _ = ks.asm(code, addr)
return encoding
res = generate("BR X9", 0)
print(res)
for byte in res:
print(hex(byte))
#0x20
#0x1
#0x1f
#0xd6

最后通过 ida_bytes.patch_bytes(addr, bytes(bCode)) 设置正确的跳转地址
然后通过 ida_bytes.patch_bytes(addr + 4, bytes(nopCode) * 9) 将剩下的9条指令 nop 掉

最后进行 patch

1
2
3
4
5
6
7
8
for addr in matches:
makeInsn(addr)
jump_addr = getJumpAddress(addr)
ins_string = f'b hex(jump_addr)'
bCode = generate(ins_string, addr)
nopCode = generate('nop', addr)
ida_bytes.patch_bytes(addr, bytes(bCode))
ida_bytes.patch_bytes(addr + 4, bytes(nopCode) * 9)

patch 后发现没有花指令的痕迹了. ida搜索一下 STP X0, X1, [SP,#var_20]!, 发现空空如也
img_1.png

来看看处理了 花指令后的 jni_onload

img_1.png

yyds