什么是去除花指令的最高境界

VSole2023-06-30 09:28:37

什么是去花指令的最高境界?莫过于去花之后替换进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都是业务无关代码,冤冤相报何时了,不抗了不抗了。

函数调用指令寄存器
本作品采用《CC 协议》,转载必须注明作者和本文链接
软件漏洞分析简述
2022-07-18 07:08:06
然后电脑坏了,借了一台win11的,凑合着用吧。第一处我们直接看一下他写的waf. 逻辑比较简单,利用正则,所有通过 GET 传参得到的参数经过verify_str函数调用inject_check_sql函数进行参数检查过滤,如果匹配黑名单,就退出。但是又有test_input函数进行限制。可以看到$web_urls会被放入数据库语句执行,由于$web_urls获取没有经过过滤函数,所以可以
莫过于去花之后替换进apk中,依然正常运行,这对汇编功底无疑是一种挑战。今天就献丑拿某流量第一的APK样本做一下IDA脚本一键去花指令分析。◆上IDA脚本,此代码为片段代码ARM64是ARM架构的64位版本。ARM64的调用约定定义了函数如何传递参数和返回结果。前八个浮点型参数通过寄存器V0至V7传递。这意味着如果函数修改了这些寄存器的值,那么它需要在返回前恢复它们的原始值。
概述在windows系统上,涉及到内核对象的功能函数,都需要从应用层权限转换到内核层权限,然后再执行想要的内核函数,最终将函数结果返回给应用层。本文就是用OpenProcess函数来观察函数从应用层到内核层的整体调用流程。OpenProcess函数,根据指定的进程ID,返回进程句柄。NTSTATUS Status; //保存函数执行状态。OBJECT_ATTRIBUTES Obja; //待打开对象的对象属性。HANDLE Handle; //存储打开的句柄。CLIENT_ID ClientId; //进程、线程ID. dwDesiredAccess, //预打开进程并获取对应的权限。ObjectNamePresent = ARGUMENT_PRESENT ; //判断对象名称是否为空
关于堆栈ShellCode操作:基础理论002-利用fs寄存器寻找当前程序dll的入口:从动态运行的程序中定位所需dll003-寻找大兵LoadLibraryA:从定位到的dll中寻找所需函数地址004-被截断的shellCode:加解密,解决shellCode的零字截断问题
反射式DLL注入实现
2022-05-13 15:59:21
反射式dll注入与常规dll注入类似,而不同的地方在于反射式dll注入技术自己实现了一个reflective loader()函数来代替LoadLibaryA()函数去加载dll,示意图如下图所示。蓝色的线表示与用常规dll注入相同的步骤,红框中的是reflective loader()函数行为,也是下面重点描述的地方。
该漏洞发生的位置是在驱动文件Win32k.sys中的xxxHandleMenuMessage函数,产生的原因是没有对该函数中调用的xxxMNFindWindowFromPoint函数的返回值进行合法性验证,直接将其作为参数传递给后面的xxxSendMessage函数调用,从而造成了提权漏洞。
Win32k组件最初的设计和编写是完全建立的用户层上的,但是微软在 Windows NT 4.0 的改变中将 Win32k.sys 作为改变的一部分而引入,用以提升图形绘制性能并减少 Windows 应用程序的内存需求。窗口管理器(User)和图形设备接口(GDI)在极大程度上被移出客户端/服务端运行时子系统(CSRSS)并被落实在它自身的一个内核模块中。
结构&拷贝与引用
2023-05-10 11:27:04
结构&拷贝与引用开始之前,我们约定数据块也叫插槽,也就是storage。storage是永久存储在区块链上的地方。Stack 的最大深度为 1024 个元素,支持 256 位的字长。结构当定义局部变量时,它存储在内存中,然后压入堆栈以执行。1024栈深简介EVM不是寄存器机而是堆栈机,所以所有的计算都在称为堆栈的数据区域上进行。1024 是一个非常保守的值,以尽可能安全EVM 的设计方式往往会使更大的堆栈变得无用。EVM 只能访问堆栈中前16个slot。
可是当我们开启了smap保护之后,内核态就没有办法访问用户态的数据,此时当我们再hijack tty_operation到我们的用户态时,我们的kernel就会panic,更别说劫持执行流到用户态上执行rop了。当我们调用msgsnd时,在linux内核中会调用do_msgsnd。
本篇针对该JS中的字符串混淆进行还原。字符串是如何混淆的解密方式想要对字符串反混淆就要先分析该样本是如何对字符串进行混淆的。而处于全局作用域的_0x1f1a68实际上也是对另一个函数的调用。
VSole
网络安全专家