什么是去花指令的最高境界?莫过于去花之后替换进apk中,依然正常运行,这对汇编功底无疑是一种挑战。
今天就献丑拿某流量第一的APK样本做一下IDA脚本一键去花指令分析。
献上对比图:
当然还有替换进去的运行图,替换进去apk运行不成功那算什么去花。
讲完效果就开干!
简单分析
◆异常B指令 -寄存器间接跳转
分析一下,使用了寄存器间接跳,干扰反汇编引擎让其无法正确函数尾部,也不能什么都指望IDA。
◆上IDA脚本,此代码为片段代码
ARM64(也被称为AArch64)是ARM架构的64位版本。ARM64的调用约定定义了函数如何传递参数和返回结果。在ARM64的调用约定中,参数是通过寄存器和堆栈传递的,具体如下:
参数传递:前八个整型或指针类型的参数通过寄存器X0至X7传递。前八个浮点型参数通过寄存器V0至V7传递。如果函数有更多的参数,那么超出的参数将通过堆栈传递。
返回值:函数的返回值通过寄存器X0(和X1,如果需要)或者V0(浮点数和SIMD类型)返回。
保留寄存器:某些寄存器在函数调用中需要被保留。这意味着如果函数修改了这些寄存器的值,那么它需要在返回前恢复它们的原始值。这些寄存器包括:X19至X28,以及栈指针SP。
调用者保存寄存器:一些寄存器在函数调用前后的值可以不同,如果调用者函数希望保留这些寄存器的值,那么它需要在调用其他函数前保存这些寄存器的值。这些寄存器包括:X0至X18,和所有的V寄存器(V0至V31)。
由上文可知,函数在开头必然要分配函数所需的栈空间以及保存调用者保存寄存器,并且在函数尾部恢复保存寄存器以及栈空间,这就是下图脚本原理。
ins_map = {"sub": "add", "str": "ldr", "stp": "ldp"}#因为入栈出栈是对应的 所以我们只需要吧 基础的三个对应opcode替换下就可以
def find_func_end(self):
encodings = [0xC0, 0x03, 0x5F, 0xD6] # ret 的指令编码
for i in md.disasm(ida_bytes.get_bytes(self.func_start, 12), 0): #arm64前三条指令分配栈空间保存寄存器
if i.op_str.find('!') == -1:
encodings = ks_disasm(self.get_inv_opcode(i)) + encodings
else:
s_new = i.op_str.replace("]!", "")
s_new = s_new.replace(", #-", "],")
dis_str = '{} {}'.format(self.ins_map.get(i.mnemonic), s_new) # 调用约定的栈平衡指令
encodings = ks_disasm(dis_str) + encodings
opcodelist = list(ida_bytes.get_bytes(self.func_start, 0x8000))
index = find_sublist(opcodelist, encodings)
self.func_end = self.func_start + index # 函数结尾的地址
print("func end addr : ", hex(self.func_end))
由图中我们也能看出这是标准的入栈出栈格式,运行后得到正确的函数尾部地址 0x32ea8。
但是当我们强行把JNI_OnLoad的函数结尾地址改成0X32EA8是不行的,因为IDA还是有点倔脾气的,必须让他心服口服的认为才行!
◆读汇编
现在找到了正确的函数尾部地址,那么经过上上图我们看到0X32804明显是个不应该在函数中出现的函数,但是他确实有完整的栈平衡格式,这个有完美起跳动作的狼人我们应该怎么处理呢?
SP+8???经过分析这个函数返回X0竟然是LR寄存器,哦!明白了,小狼一头!
手动计算下LR的地址明显是0x327FC + 0x34 =0x32830那么BR X1的位置就是0x32830。
看下0x32830地址的OPCDE OMG嵌套狼-
SVC 0
DCD 0x477001DE
****
BL sub_32804
ADD X6, X0, #8
BR X6
****
B.NE loc_32858
CLREX
BRK #3
明显的花指令喽,CPU要是执行了SVC不得崩溃才怪,但是花指令花就花在它执行不到,这这这!那就处理呗,反正能算出他的真实执行地址,把能NOP的都NOP呗。
◆方案1 Unicorn
其实我是写了两个版本的,最后Unicorn arm32版本被我PASS掉了,因为都搞64了,但是放个图纪念一下。
◆方案2 大胆干,早点散
经过分析所有的基础花都是基于那个获取LR的函数来的,那就通过他定位花的位置,直接nop其位置就得了。如果在函数中找到特征码存在的地方,会自动遍历其上下N个地址,直到找到花特征进行NOP。
这样就可以了吗?
经过处理并且删除掉内部函数以及无用分支流我们确实得到了一个能够F5的函数,但是这对我们的要求来说远远不够,我们的要求可以替换进真机正常运行!
现在F5正常,但是为什么我们替换到真机里就闪退呢?
无非两个问题,第一就是花指令去除到了真实代码块,二就是检测了代码块是否被更改!
◆动态排查
那就调试呗,首先找到崩溃位置,跟就完了。
根据笔者跟了百条指令,发现崩溃点位在:
竟然把0给了SP寄存器,并且循环清空栈空间,这不崩才怪!
◆原理分析
其实如果安全人员想让它崩溃有更简单的办法,但是为什么用这种方法呢,大家可以看到,这一系列的操作是为了清空原本的栈空间,那么栈空间有什么不可告人的秘密呢?如果熟悉汇编代码,画过堆栈图的大佬们应该知道,栈空间可是保存着完整的调用链条的,这也是frida之类打印调用堆栈的原理,当我们调试的时候真机崩溃了,打印出的调用堆栈确是1-2-3-4-5-6那你懵不懵呢?
放个对比图-上面的是正常的崩溃截图,frida可以准确的打印出崩溃地址!可以更清晰的供调试人员分析是因为那里崩溃,方便排查!同时,这也方便了逆向人员定位检测代码!
下面的就是清空栈空间后的崩溃截图,什么都打印不出来就直接Crash掉了。
◆向上溯源
继续排查发现,样本对代码块进行了移位亦或操作。
因为v14指向的是本函数的代码位置,所以当我们开心的NOP掉时,必然触发了它的检测机制,这才是隐藏在最后的狼人,让我们干掉它把。
直接让他RET就好,pathch一个函数也不好玩,把整个so patch了把,RegisterNatives成功得到 !一个1000kb多的so,500kb都是业务无关代码,冤冤相报何时了,不抗了不抗了。
看雪ID:至尊小仙侠
https://bbs.kanxue.com/user-home-873999.htm
# 往期推荐
3、安卓加固脱壳分享
球分享
球点赞
球在看
原文始发于微信公众号(看雪学苑):什么是去除花指令的最高境界