安卓逆向-快手去花指令

下雨天
10月14日发布 /正在检测是否收录...

快手去花指令

1. 简述

  • 学习过程的记录,仅代表个人浅显理解,有错误烦请纠正;

Ⅰ 什么是花指令?

  • 花指令是由开发者设计,通过某种方式插入程序,用于阻碍反汇编器和代码分析者对程序控制流的认识的汇编代码片段;它是由开发者设计的,希望使反汇编出错,让破解者无法清楚正确的理解程序逻辑的一种技术;

2. 确认是否有花

  • 一般来说,插花都会在比较重要的函数做,或者说重要的函数被插花的概率更大,所以我们在做这一步的时候可以优先查看JNI_OnLoad这种函数来看是否被插花;

image-20250502140617754

  • 这算是比较明显的特征,或者看上面的导航条,出现了大段的红色,以及对应的机器码部分也是红色,这就属于未被识别为函数的特征,所以可以确认这个样本存在花指令;

image-20250502140841707

3. 修复JNI_OnLoad

2.1 手动修复

  • 样本存在大量的花指令,所以不可能一个一个单独修复,肯定是需要借助ida脚本来做这件事,但在批量处理之前我们可以先看一下单个的处理,先看看对应的汇编代码;
.text:00000000000462C4                 STP             X0, X1, [SP,#var_20]!
.text:00000000000462C8                 STP             X2, X30, [SP,#0x20+var_10]
.text:00000000000462CC                 ADR             X1, dword_462EC
.text:00000000000462D0                 SUBS            X1, X1, #0xC
.text:00000000000462D4                 MOV             X0, X1
.text:00000000000462D8                 ADDS            X0, X0, #0x3C ; '<'
.text:00000000000462DC                 STR             X0, [SP,#0x20+var_8]
.text:00000000000462E0                 LDP             X2, X9, [SP,#0x20+var_10]
.text:00000000000462E4                 LDP             X0, X1, [SP+0x20+var_20],#0x20
.text:00000000000462E8                 BR              X9
  • 可以看到最后它是跳转到了X9寄存器的地址,而ida其实已经识别到这个地址了,或者说他计算出了这个地址,就是JUMPOUT位置的值,0x4631,那么这个值在哪里?

image-20250502141201562

  • 可以看到,它们隔的其实其实不远,只是中间有一些数据,我们按下D可以将他们转换一下;

image-20250502141409538

  • 这里其实ida已经将该跳转的地方识别为了函数,只是它没有创建,因为它处在数据里面,ida无法百分百确定它是代码段,所以选择了较为稳妥的方案,将选择权交给用户,那我们可以尝试按下P将它创建为一个函数,这里注意光标位置放置准确;

image-20250502141616058

  • 可以发现成功被创建成了一个函数,按下tab即可看伪c;

image-20250502141722986

  • 此时点击JNI_OnLoad函数里的地址,它也会顺利的跳到这个新创建的sub_4631C函数处;

2.2 keypatch修复

  • 接下来需要去分析这些汇编代码具体都有什么含义,依次来看,以jni_onload这个函数为例,这里可以按下h将一些数据转换成十六进制,方便我们分析,先看完整的汇编代码:
.text:00000000000462C4             ; jint JNI_OnLoad(JavaVM *vm, void *reserved)
.text:00000000000462C4                             EXPORT JNI_OnLoad
.text:00000000000462C4             JNI_OnLoad                              ; DATA XREF: LOAD:0000000000003AD0↑o
.text:00000000000462C4
.text:00000000000462C4             var_20          = -0x20
.text:00000000000462C4             var_18          = -0x18
.text:00000000000462C4             var_10          = -0x10
.text:00000000000462C4             var_8           = -8
.text:00000000000462C4
.text:00000000000462C4 E0 07 BE A9                 STP             X0, X1, [SP,#-0x20]!
.text:00000000000462C8 E2 7B 01 A9                 STP             X2, X30, [SP,#0x10]
.text:00000000000462CC 01 01 00 10                 ADR             X1, 0x462EC
.text:00000000000462D0 21 30 00 F1                 SUBS            X1, X1, #0xC
.text:00000000000462D4 E0 03 01 AA                 MOV             X0, X1
.text:00000000000462D8 00 F0 00 B1                 ADDS            X0, X0, #0x3C ; '<'
.text:00000000000462DC E0 0F 00 F9                 STR             X0, [SP,#0x18]
.text:00000000000462E0 E2 27 41 A9                 LDP             X2, X9, [SP,#0x10]
.text:00000000000462E4 E0 07 C2 A8                 LDP             X0, X1, [SP],#0x20
.text:00000000000462E8 20 01 1F D6                 BR              X9
.text:00000000000462E8             ; End of function JNI_OnLoad
  • 先看第一条指令;这里可以按下h将一些数据转换成十六进制或者十进制都行,后续注意进制即可;
E0 07 BE A9                 STP             X0, X1, [SP,#-0x20]!
@ 感叹号代表前缀索引模式 这里的作用是将X0、X1这两个寄存器依次存入栈顶 此时的栈顶应当是SP - 32的位置 
@ 并更新栈指针为 SP - 32 大概就是做了这两件事情
  • 再看第二条指令:
E2 7B 01 A9                 STP             X2, X30, [SP,#0x10]
@ 这里是偏移寻址 直接sp加上16即可 将寄存器 X2 和 X30(LR)的值依次存储到栈顶偏移 16 字节的位置
  • 第三条指令:
01 01 00 10                 ADR             X1, 0x462EC
@ 这里可以简单的理解为 X1 = 0x462EC adr其实是一个地址加载伪指令 
  • 第四条指令:
21 30 00 F1                 SUBS            X1, X1, #0xC
@ 这是一个减法指令 作用:X1 = X1 - 0xC = 0x462E0 根据上条代码得到
  • 第五条指令,后续就接着分析到结束了;
E0 03 01 AA                 MOV             X0, X1
@ 将x1的值赋值给x0 X0 = X1 = 0x462E0
00 D0 00 B1                 ADDS            X0, X0, #0x3C ; '<'
@ 加法指令 X0 = X0 + 0X34 = 0x462E0 + 0x3C = 0x4631C
E0 0F 00 F9                 STR             X0, [SP,#0x18]
@ 这里也是偏移寻址 将X0放在SP+24的位置 并且这里是经过重新赋值的新X0
E2 27 41 A9                 LDP             X2, X9, [SP,#0x10]
@ 依旧是偏移寻址 作用是从内存中连续加载两个寄存器的值 它的加载顺序如下:
@ X2 ← [SP + 16]
@ X9 ← [SP + 24](每个寄存器占 8 字节)
@ 这条指令得到的结果就是X2不变 依靠上面的逻辑可以知道SP + 16的寄存器正是X2 所以X2是不变的
@ 而X9的结果就是上面的新X0
E0 07 C2 A8                 LDP             X0, X1, [SP],#0x20
@ 后索引寻址 LDP是 ARM64 架构中的双寄存器加载指令 作用是从内存中连续加载两个寄存器的值 并更新栈指针
@ 该指令的核心功能是 恢复栈空间 加载完成后 SP 向高地址方向移动 32 字节 释放之前分配的栈空间
@ (例如函数调用时通过 STP 指令分配的栈空间) 
STP X0, X1, [SP, #-32]!  ; 保存 X0/X1 到栈顶,SP 减少 32 字节  
...  
LDP X0, X1, [SP], #32    ; 恢复 X0/X1,SP 增加 32 字节 
@ 这里实际上就是将栈顶存着的X0、X1恢复到原先的值
20 01 1F D6                 BR              X9
@ 跳转到X9的位置 也就是 BR 0x4631C
  • 关于上述的分析其实也是有借助于ai的,所以并不是标准答案,仅仅只是我的拙见;指令一共也就这几条,不会有什么难度,而且重要的指令也就两条运算而已,不熟悉的朋友可以多多借助ai来帮助分析;
  • 将这十条汇编分析完之后我们来总结一下寄存器与栈的使用情况,首先是寄存器,一共用到了X0X1X2X30X9 这五个寄存器,而真正参与计算的只有X1、X2、X9这三个寄存器,其余两个的值是没有变过的,这里主要起到一个混淆的作用,X0和X1寄存器经过计算得到0x4631C这个值,而X9只是用来承接这个值而已;再就是栈,第一个作用就是为花指令打掩护,前面我们提到过,花指令是人为的,目标是干扰分析者或者反汇编工具,这里借助栈来实现这种隐晦的赋值方案来反击分析者,否则会很轻易的知道具体是跳转到哪里去了,也就失去了花指令的意义;第二个作用就是保护上下文,我们知道,有些寄存器是易失的,有些寄存器是非易失的,区别就在于,在函数调用时,是否需要保存它的值,在用完之后再将它恢复回去,这里就很显然,X0、X1就是易失的,所以最后才需要将栈上的值恢复回去,也就是这个作用;
  • 通过手动推演的方式我们已经了解了这个花指令,然后需要去进行patch,来抵消花指令对我们分析的困扰;他有这几个要求,首先将实际的功能patch到整个程序的首行,在这里也就是B 0x4631C,这个跳转也就是它的实际作用,再就是将其余行全部nop掉,防止他干扰我们的分析以及对反汇编工具的干扰;
  • 这里可以选择自带的patch也可以选择keypatch工具,我这里选择使用keypacth,他比较方便;在首行代码处右键选择keypatch;这个工具没有的话需要自行安装,相关的依赖也请百度后安装;
  • 选择keypatch后,随后根据出现的选项选择第一个patcher,将assembly这里改成B 0x4631C,随后点击patch,后续的框可以取消掉;这里需要注意位置是首行,不用patch错了;随后同样的操作,选择剩余的所有代码,改为nop;

image-20250502151353708

  • 此时反编译依旧是JUMPOUT,此时将光标放在第一行汇编上,选择左上角的edit,找到Functions选项,然后选择delete function,此时这段代码会变红,代表ida无法识别这段代码,然后继续将光标放在首行,按下p键或者去delete的地方找create function将其转换为函数;然后再次按下tab即可发现反编译没问题了;

image-20250502151445695

  • 跳转一下;

image-20250502151512512

  • 这样JNI_OnLoad就修复完成了,其实手动更加的灵活,只是第二种方式可以明白它的原理且更加适应复杂场景,后续批量处理的时候也需要依靠刚刚的分析;

4. 批量修复

4.1 idapython代码提示

  • 首先我们需要一个开发脚本的环境,ida里显然是不太舒适的,我这里选择pycharm来进行脚本开发,如下配置即可;

image-20250502160832108

4.2 修复

  • 首先我们需要明确一点的是,究竟有多少处花指令,这里我们去ida搜索一下特征即可确定,那么哪一个算是特征?我们任意的去找一些红色的位置,看看它们的汇编代码分别是什么样的;

image-20250502161126708

  • 大部分都是这种结构,那么我们就可以以他为基准来搜索特征,这里需要注意,尽量不要依靠反汇编的汇编字符串来做特征,这样会有未被反汇编器识别的结果被错过,这样会干扰分析,所以最好是通过机器码来匹配;
  • 我们选择搜索机器码,且建议搜索两条,一条的话可能会存在误判;

image-20250502161256516

  • 搜索的条件是:E0 07 BE A9 E2 7B 01 A9,结果有123条;

image-20250502161330879

  • 在开始编写之前我们需要明确一件事,我们应该如何批量修改;首先需要跟我们进行的操作一样,找到匹配的特征项,其次再是统一处理;
  • 接下来开始编写ida脚本,这个我也不是特别熟悉,参考了很多大佬的脚本,在结尾会进行感谢;首先看看特征匹配的代码;
from keystone import *
import ida_bytes
import idaapi
import idc
import ida_nalt


def binSearch(start, patternStr):
    matches = []

    pattern = ida_bytes.compiled_binpat_vec_t()
    ida_bytes.parse_binpat_str(pattern, 0x0, patternStr, 16, ida_nalt.BPU_2B) # ida_nalt.BPU_2B 表示使用2字节的搜索单位
    while True:
        # 从 start 地址到 idc.BADADDR(表示无效地址)的范围内搜索 pattern模式 1 表示只搜索第一个匹配项
        addr, _ = ida_bytes.bin_search3(start, idc.BADADDR, pattern, 1)
        if addr == idc.BADADDR:  # bad address
            break
        else:
            matches.append(addr)
            start = addr + 1
    return matches

# 特征
matches = binSearch(0, "E0 07 BE A9 E2 7B 01 A9")
print(matches)
print(f"共有{len(matches)}个匹配项")
  • 这个当然不是在ida里运行的,需要去ida里跑,选择file 下的 Script command即可运行;

image-20250502163551541

  • 将脚本粘贴上去后即可运行,这里提一句我用的是ida 9.0,不同的版本ida的api会不太一样,所以有报错的话需要自行摸索;

image-20250502163736666

  • 个数和我们在ida搜索匹配到的数量是对的上的,接下来就需要确认修复方案,我们手动用keypatch的时候是在找它的最终跳转地址吧,那我们对每一条都需要这么做,找到他最终跳转的地址后patch即可;回到之前的汇编分析,我们发现了实际上它最终的地址计算依靠的就是三条指令,一条获取到一个给定的地址,然后减去一个值,再次加上一个值,就能得到它最终需要跳转的地址了;
def getJumpAddress(addr):
    A = idc.get_operand_value(addr + 8, 1)  # get_operand_value 需要地址识别为代码
    B = idc.get_operand_value(addr + 12, 2)
    C = idc.get_operand_value(addr + 20, 2)
    return A - B + C   # 获取跳转的地址
  • 关于这段代码解释如下:addr是当前地址,+8则是第三行,也就是获取第一个操作数,这里我暂且这么说,后面的1可以理解为它的位置吧,后续两行代码是一样的道理,能理解就行,最终计算出跳转的实际地址;
  • 随后需要的步骤就是上面提及的nop,这个已经在 2.2 位置提及过,脚本如下:
import keystone  # pip install keystone-engine
from keystone import *

import ida_bytes
import idc
import ida_nalt

# https://python.docs.hex-rays.com/
# https://python.docs.hex-rays.com/ida_bytes

def binSearch(start, patternStr):
    matches = []

    pattern = ida_bytes.compiled_binpat_vec_t()
    ida_bytes.parse_binpat_str(pattern, 0x0, patternStr, 16, ida_nalt.BPU_2B) 
    # ida_nalt.BPU_2B 表示使用2字节的搜索单位
    while True:
        # 从 start 地址到 idc.BADADDR(表示无效地址)的范围内搜索 pattern模式 1 表示只搜索第一个匹配项
        addr, _ = ida_bytes.bin_search3(start, idc.BADADDR, pattern, 1)
        if addr == idc.BADADDR:  # bad address
            break
        else:
            matches.append(addr)
            start = addr + 1
    return matches



def getJumpAddress(addr):
    A = idc.get_operand_value(addr + 8, 1)  # get_operand_value 需要地址识别为代码
    B = idc.get_operand_value(addr + 12, 2)
    C = idc.get_operand_value(addr + 20, 2)
    return A - B + C   # 获取跳转的地址


def makeInsn(addr):
    # 尝试在给定的地址 addr 上创建一条指令
    if idc.create_insn(addr) == 0:
        # 如果在地址 addr 上没有成功创建指令,则删除该地址上的所有项(扩展项)
        idc.del_items(addr, idc.DELIT_EXPAND)
        # 重新尝试在地址 addr 上创建一条指令
        idc.create_insn(addr)
    # 等待自动操作完成
    idc.auto_wait()


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


# 1 搜索匹配
matches = binSearch(0, "E0 07 BE A9 E2 7B 01 A9")

for addr in matches:
    # 2 计算跳转地址
    makeInsn(addr) # 给定的地址 addr 上创建一条指令
    targetAddr = getJumpAddress(addr)
    print(hex(targetAddr))

    # 3 patch
    code = f"B {hex(targetAddr)}"
    bCode = generate(code, addr)
    nopCode = generate("nop", 0)
    
    ida_bytes.patch_bytes(addr, bytes(bCode))
    ida_bytes.patch_bytes(addr + 4, bytes(nopCode) * 9)
print("patch compelete")
  • patch完成之后记得选择edit下的 patch program应用到整个文件;

image-20250502170131089

  • 完成后退出ida,此时要选择不保存数据库;

image-20250502170316825

  • 随后重新打开so文件,加载后即可发现花指令被去除了;

image-20250502170401278

  • 导航条已经没有红色的部分了,JNI_OnLoad函数也是正确的反编译了;至此去除花指令结束;

5. 总结

  • 感谢龙哥、季冬、如画等大佬的文章,受益良多,ida脚本主要也来源于此;
  • By:下雨天 2025.5.2
  • 逆向交流+vx:HeiYuKuaiDou23
© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 收藏
评论 抢沙发
OωO
取消