某程xxmain.so花指令去除记录
前言
最近想要练习练习脚本去花,去混淆,解密字符串
但是感觉自己加花/混淆出来的样本太简单了
在龙哥星球看到xccccccc师傅发了一篇某程xxmain.so的去花和算法还原的文章
记一次xxmain.so从去花到魔改算法还原
so里的花指令强度还算可以,模式比较固定,非常适合练习
xccccccc师傅文章里App版本是8.0.65,可以说是很老,笔者在豌豆荚也下不到,于是用的8.74.6版本
好在目标方法仍被保留,其在so中的偏移是0xc2794
Frida检测绕过
绕过思路和之前某书Frida检测绕过记录一模一样,这里不再赘述
花指令分析
函数开头一眼花指令,不是正常函数开头的序言
STP压X0和X30
LDR给W0一个立即数
BL调用sub_C2C64,同时设置X30/LR为返回地址即0xC27A0
sub_C2C64前两条指令通过X0和立即数计算新的X0
(单独说明是因为其他类似sub_C2C64的函数前两条指令的运算方式会不同,还有存在第三条运算指令的情况)
第三条指令X0加1,第四条指令X0先左移2位,再作为偏移,以X30为基址,去tb读个新的DWORD赋给W0
第五条指令X30加X0,改返回地址,最后RET返回,也就是说sub_C2C64根据X0进行动态跳转
上面最后会跳转到0xC2CA0(unidbg模拟执行+traceCode可以很方便地找到跳转地址)
LDP和开头的STP对应,也会平衡堆栈
后面两条STP就是正常函数开头的序言了
然后又是STP+LDR+BL的组合,只是W0的值不同
而且这里BL并不是直接调用sub_C2C64,而是先跳转到loc_C279C再通过BL调用sub_C2C64
这就和前面不一样了,区别在于STP+LDR+BL后的第四条指令仍是BL还是运算指令EOR/SUB等等
总结一下,函数开头的STP+LDR+BL会跳转到一个类似sub_C2C64的通过X0和X30计算返回地址的函数calcfunc
但是这个计算返回地址的函数calcfunc有很多,指令条数和运算方式都不太一样
比如这里sub_31C4C前三条指令都是X0和立即数的运算,从第四条指令开始才符合上面的模式
笔者的解决思路是每当函数开头进入一个新calcfunc的时候(calcfunc作为键),对calcfunc的指令进行反汇编并保存
然后实现一个汇编指令解释器去模拟每条指令,以计算最终的跳转地址
对于STP+LDR+BL后第四条指令仍是BL的情况,通过loc_xxxxx确定唯一的calcfunc,得到跳转地址后加入值列表
去花
这里需要说明的一点是,跳转地址的第一条指令是LDP,要和STP对应,所以patch的地址得是LDR的地址
完整脚本如下
python
import idautils
import ida_bytes
import idc
import struct
from capstone import *
from keystone import *
# 反汇编引擎
cs = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
# 汇编引擎
ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
# 字典
# 条目结构
# {"通过w0和x30计算目标跳转地址的函数名称":[x30, [函数汇编指令], (目标patch地址,目标跳转地址), (,), ...]}
amap = dict()
def mydisasm(codebin, codeaddr):
# 反汇编
asm = []
for insn in cs.disasm(codebin, codeaddr):
if insn.mnemonic != "":
# 助记符
asm.append(insn.mnemonic)
# 操作数
asm.append(insn.op_str)
return asm
def getfuncinsns(startaddr):
# 获取函数汇编指令
funcinsns = []
addr = startaddr
while True:
addrcode = idc.get_wide_dword(addr)
addrcode = struct.pack("<I", addrcode)
ins = mydisasm(addrcode, addr)
funcinsns.append(ins)
addr += 4
if ins[0] == "ret":
break
return funcinsns
def execfuncinsns(w0, x30, insns: list[list[str]]):
# 解释执行函数汇编指令
# 返回目标跳转地址
maybewrong = False
# 处理第一条指令
insn0 = insns[0]
assert insn0[1].startswith("x0, x0, #")
insn0oprand = insn0[1].replace("x0, x0, #", "")
insn0oprand = int(insn0oprand, 16)
if insn0[0] == "add":
w0 += insn0oprand
elif insn0[0] == "sub":
w0 -= insn0oprand
elif insn0[0] == "eor":
w0 ^= insn0oprand
elif insn0[0] == "lsr":
w0 >>= insn0oprand
else:
maybewrong = True
# 处理第二条指令
insn1 = insns[1]
assert insn1[1].startswith("x0, x0, #")
insn1oprand = insn1[1].replace("x0, x0, #", "")
insn1oprand = int(insn1oprand, 16)
if insn1[0] == "add":
w0 += insn1oprand
elif insn1[0] == "sub":
w0 -= insn1oprand
elif insn1[0] == "eor":
w0 ^= insn1oprand
elif insn1[0] == "lsr":
w0 >>= insn1oprand
else:
maybewrong = True
if len(insns) == 6:
# 处理第三条指令
assert insns[2][0] == "add"
assert insns[2][1] == "x0, x0, #1"
w0 += 1
# 处理第四条指令
assert insns[3][0] == "ldr"
assert insns[3][1] == "w0, [x30, x0, sxtx #2]"
w0 <<= 2
w0 = idc.get_wide_dword(x30+w0)
# 处理第五条指令
assert insns[4][0] == "add"
assert insns[4][1] == "x30, x30, x0"
w0 = x30+w0
# 处理第六条指令
assert insns[5][0] == "ret"
elif len(insns) == 7:
# 处理第三条指令
insn2 = insns[2]
assert insn2[1].startswith("x0, x0, #")
insn2oprand = insn2[1].replace("x0, x0, #", "")
insn2oprand = int(insn2oprand, 16)
if insn2[0] == "add":
w0 += insn2oprand
elif insn2[0] == "sub":
w0 -= insn2oprand
elif insn2[0] == "eor":
w0 ^= insn2oprand
elif insn2[0] == "lsr":
w0 >>= insn2oprand
else:
maybewrong = True
# 处理第四条指令
assert insns[3][0] == "add"
assert insns[3][1] == "x0, x0, #1"
w0 += 1
# 处理第五条指令
assert insns[4][0] == "ldr"
assert insns[4][1] == "w0, [x30, x0, sxtx #2]"
w0 <<= 2
w0 = idc.get_wide_dword(x30+w0)
# 处理第六条指令
assert insns[5][0] == "add"
assert insns[5][1] == "x30, x30, x0"
w0 = x30+w0
# 处理第七条指令
assert insns[6][0] == "ret"
else:
maybewrong = True
if maybewrong:
w0 = -1
return w0
def search(addr):
global amap
# 第一条指令
# STP X0, X30, [SP,#-0x10]!
firstinsaddr = addr
if idc.get_wide_dword(firstinsaddr) == 0xA9BF7BE0:
# 第二条指令
# LDR W0, =
secondinsaddr = addr + 4
if idc.get_wide_dword(secondinsaddr) == 0x18000040:
# 第三条指令
thirdinsaddr = addr + 8
thirdinscode = idc.get_wide_dword(thirdinsaddr)
thirdinscode = struct.pack("<I", thirdinscode)
thirdinsasm = mydisasm(thirdinscode, thirdinsaddr)
# 第三条指令一定是bl
if len(thirdinsasm) != 0 and thirdinsasm[0] == "bl":
# 第四条指令
fourthinsaddr = int(thirdinsasm[1][1:], 16)
fourthinscode = idc.get_wide_dword(fourthinsaddr)
fourthinscode = struct.pack("<I", fourthinscode)
fourthinsasm = mydisasm(fourthinscode, fourthinsaddr)
# 第四条指令不是bl,说明直接进入计算函数,向amap插入条目
if len(fourthinsasm) != 0 and fourthinsasm[0] != "bl":
calcfunc = f"sub_{hex(fourthinsaddr)[2:]}"
calcbasex30 = thirdinsaddr+4
if calcfunc not in amap.keys():
calcfuncinsns = getfuncinsns(fourthinsaddr)
amap[calcfunc] = [calcbasex30, calcfuncinsns]
# 第四条指令仍是bl,说明间接进入计算函数,验证计算函数是否存在
elif len(fourthinsasm) != 0 and fourthinsasm[0] == "bl":
calcfunc = f"sub_{fourthinsasm[1][3:]}"
if calcfunc not in amap.keys():
print(f"wrong at {hex(firstinsaddr)}")
# 第二条指令
secondinscode = idc.get_wide_dword(secondinsaddr)
secondinscode = struct.pack("<I", secondinscode)
secondinsasm = mydisasm(secondinscode, secondinsaddr)
# 第二条指令一定是ldr
if len(secondinsasm) != 0 and secondinsasm[0] == "ldr":
# 获取动态的w0
w0addr = secondinsasm[1].replace("w0, #", "")
w0 = idc.get_wide_dword(int(w0addr, 16))
# 获取相对计算函数静态的x30和汇编指令
calcfunclist = amap[calcfunc]
x30 = calcfunclist[0]
calcfuncinsns = calcfunclist[1]
# 计算目标跳转地址
w0jmpaddr = execfuncinsns(w0, x30, calcfuncinsns)
if w0jmpaddr != -1:
calcfunclist.append((secondinsaddr, w0jmpaddr))
else:
print(f"bad func at {calcfunc}")
exit()
def gencode(srcaddr, jmpcode):
# 生成机器码
code, _ = ks.asm(jmpcode, srcaddr)
return code
# 遍历段,找.text段
textsegstart = -1
textsegend = -1
for segea in idautils.Segments():
if ".text" in idc.get_segm_name(segea):
textsegstart = segea
textsegend = idc.get_segm_end(segea)
break
if textsegstart != -1 and textsegend != -1:
while textsegstart <= textsegend:
search(textsegstart)
textsegstart += 4
for value in amap.values():
# [(目标patch地址,目标跳转地址)]
patchlist = value[2:]
for srcaddr, jmpaddr in patchlist:
jmpcode = f"B {hex(jmpaddr)}"
code = gencode(srcaddr, jmpcode)
if code != None:
ida_bytes.patch_bytes(srcaddr, bytes(code))
print("Done")
最终patch了231616字节,去花后反编译,堆栈还是有点问题
还有点混淆,d810能去个大概
后记
后续的算法还原就没做了,xccccccc师傅文章里分析出来是魔改的MD5和SHA256
感觉再跟一遍有点折磨(懒狗本狗了属于是
去花脚本有待改进,就像上面说的堆栈还有点问题
还有就是去混淆,不透明谓词的话,理论上改成const类型即可,但是量太大,脚本改的话还得学习学习