看开水团与 ARM 花指令构造与静态Patch

新闻资讯   2023-07-02 18:03   93   0  

在工作中,多多少少会遇到花指令的情况,本系列文章我会慢慢更新,把我学习过程分享给大家,从简单的构造到如何结合手头的工具patch。


什么是花指令


洋文叫:Junk Code 脏指令 主要作用就是用来干扰,反汇编引擎(线形扫描&递归下降) 和 F5 增加逆向(PJ)难度的一种技术,能屏蔽大部分脚本小子,这些指令CPU也不认识,执行到这种指令CPU直接会报错,如何不影响CPU正常执行又能干扰反汇编引擎的花指令呢。


花指令种类与如何简单构造


花指令的种类有很多,这里只讨论移动端或者说ARM平台,哪些团队在用花指令呢我的知识范围内发现的的商业级样本中,魔兽世界怀旧服macOS版某泔水团等等等等........ 大家可以后续补充。


这里为什么没有研究x86,第一因为我不会第二个原因是x86是非定长指令集,这会导致x86有那种双字节的花指令不适合线形扫描,要校验的太多了无法通过定长4字节的方式进行线性扫描。


ARM64指令集天生就是定长4字节,这里可能有人会杠ADRL x0, unk_1023d55018字节你怎么说,其实这是两条指令adrp x0, #0xcadd x0, x0, #0x501。


第一章简单的花指令构造与修正


◆example1


_main:
0x0000000100003c84 FF8300D1 sub sp, sp, #0x20
0x0000000100003c88 FD7B01A9 stp fp, lr, [sp, #0x10]
0x0000000100003c8c FD430091 add fp, sp, #0x10
0x0000000100003c90 08008052 mov w8, #0x0
0x0000000100003c94 FF0B00B9 str wzr, [sp, #0x8]
0x0000000100003c98 db 0xb1 ; '.'
0x0000000100003c99 db 0x7f ; '.'
0x0000000100003c9a db 0x39 ; '9'
0x0000000100003c9b db 0x05 ; '.'
0x0000000100003c9c db 0x4e ; 'N'
0x0000000100003c9d db 0x61 ; 'a'
0x0000000100003c9e db 0xbc ; '.'
0x0000000100003c9f db 0x00 ; '.'
0x0000000100003ca0 29048052 mov w9, #0x21


上边的样本是我自己构造的简单花指令样本,可以看到 0x100003c98-0x100003c9f 无法被反汇编引擎正常解析并标记为数据db,这个样本是在Hopper中解析的,在ida pro呈现的是:DCB/DCW/DWD/DCQ 这里简单理解他们都是数据只是大小不同 B = byte,W= word (2bytes),D = dword(4bytes),Q = qword(8bytes)


ARM文档(https://developer.arm.com/documentation/dui0473/m/directives-reference/dcb?lang=en)。


QA:那问题来了数据不是有数据段么,怎么出现在代码段了?


DCB/DCW/DWD/DCQ 在ARM中表示定义数据的指令,正常的代码经过编译器怎么能犯这种错误呢,问题就出在二进制工程师通过某种手段构造出来故意干扰反汇编引擎脚本小子下边公布代码:


◆main.cpp

 

/****
*@author 周樟寿
*/
#include <iostream>

/****
* __attribute__((always_inline)) 内联函数
* @return
*/
static __attribute__((always_inline)) int case1() {

#if __arm64__
__asm__(
"udf #0x5397FB1\n"
".long 87654321\n"
".long 12345678\n"
);
#endif
int b = 1 * 3 + (5 * 6);

return b;
}
int main() {
int c = case1();
std::cout << (c) << std::endl;
return 0;
}


这个例子是无法正常运行执行的,因为cpu执行到了我们通过内联汇编构造的非法指令,这里使用了两种方式来构造非法指令。


◆.long 表示声明是数据即我们看到DCQ

◆udf 字面意思就是未定义 具体请看文档官方文档-udf(https://developer.arm.com/documentation/dui0801/h/A32-and-T32-Instructions/UDF)


有了基础知识,我们先想办法修正这类这里使用ida python进行修正。


◆fix_example_junk_code.py

    

""
@author 周樟寿
""
import ida_bytes
import idautils
import idc
arm64_nop = b"\x1f\x20\x30\xd5"

if __name__ == '___main__':
print("fix_example_junk_code.py begin.....")
segments = idautils.Segments() #获取所有段
for set in segments:
#只处理 text 段 这个脚本支持 so 和 match-o
if idc.get_segm_name(seg) in ['__text','.text']
#开始
seg_start_ea = idc.get_segm_start(seg)
#结束
seg_end_ea = idc.get_segm_end(seg)-4

#临时变量
current_ea = seg_start_ea
while current_ea <= seg_end_ea:
#指令大小对于非指令无效
item_size = idc.get_item_size(current_ea)
#是否是asm 指令判断
is_asm_code = ida_bytes.is_code(ida_bytes.get_flags(current_ea))
if not is_asm_code:
# patch nop
ida_bytes.patch_bytes(current_ea,arm64_nop)
current_ea = current_ea + 4
else:
current_ea = current_ea + item_size

print("fix script end success ........")


◆第一章遗留下了一个问题就是,如何构造不影响cpu执行指令的花指令。


第二章构造可用的花指令: B+junkCode


这个小节需要解决的是让CPU正常执行并且还能干扰反汇编引擎&F5,先看例子讲解原理。


◆case2


/****
*@author 周樟寿
*/
#include <iostream>

/****
* __attribute__((always_inline)) 内联函数
* @return
*/
static __attribute__((always_inline)) int case2() {

#if __arm64__
__asm__(
"b #0x10\n"
"udf #0x5397FB1\n"
".long 87654321\n"
".long 12345678\n"
);
#endif
int b = 1 * 3 + (5 * 6);

return b;
}
int main() {
int c = case2();
std::cout << (c) << std::endl;
return 0;
}


这个例子与第一个例子最大的不同就是,增加了一个B指令来让CPU正常执行跳过我们精心构造的花指令,下面聊聊为什么用B指令和 为什么后边的立即数是#0x10。


◆为什么用B指令 文档(https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/B--Branch-?lang=en)


使用B指令是因为,B指令的寻址方式是相对寻址,相对于PC寄存器进行的,其次B是无条件跳转无需关心LR寄存器。


◆B 指令后边的立即数


立即数为什么是0x10这里先把转成10进制等于16这里要回顾一下PC寄存器的知识,PC寄存器永远存储的是需要执行下一条指令的地址,这里就会衍生出来一个公式来计算具体跳多远,公式: 立即数=(填充指令数量+1) * 4,套用上边例子最终结果 16=(3+1) * 4。


◆结语


这种简单类型的修正思路就是,发现B指令,先计算出立即数,然后判断判断一条是否为指令集,然后轮训判断下去保存总指令大小,如果发现立即数=我们扫描的区域大小直接全nop掉,
下一章我直接根据实际样本进行学习以及修正。


◆修正脚本


fix_case2.py


""
@author 周樟寿
""
import ida_bytes
import idautils
import idc

arm64_nop = b"\x1f\x20\x30\xd5"


def get_range():
segments = idautils.Segments() # 获取所有段
for seg in segments:
# 只处理 text 段 这个脚本支持 so 和 match-o
if idc.get_segm_name(seg) in ['__text', '.text']:
# 开始
seg_start_ea = idc.get_segm_start(seg)
# 结束
seg_end_ea = idc.get_segm_end(seg) - 4
break
return seg_start_ea, seg_end_ea


if __name__ == '___main__':
print("fix_example_junk_code.py begin.....")

# 开始
seg_start_ea, seg_end_ea = get_range()

# 临时变量
current_ea = seg_start_ea
while current_ea <= seg_end_ea:
# 指令大小对于非指令无效
item_size = idc.get_item_size(current_ea)
# 是否是asm 指令判断
is_asm_code = ida_bytes.is_code(ida_bytes.get_flags(current_ea))
if not is_asm_code:
current_ea = current_ea + 4
else:

# 判断是否是指令B
if idc.print_insn_mnem(current_ea) in ['b', 'B']:

# 获取B 后边的目标地址 立即数=目标地址-当前地址
b_imm = idc.get_operand_value(current_ea, 0) - current_ea

# 判断指令B下边是否是指令不是指令开始非指令区域大小
if not ida_bytes.is_code(ida_bytes.get_flags(current_ea + 4)):
data_size = 0
begin_ea = current_ea + 4
while True:
if not ida_bytes.is_code(ida_bytes.get_flags(begin_ea)):
data_size = data_size + 4
begin_ea = begin_ea + 4
else:
break
if data_size == b_imm:
# todo not
print("nop fix")

current_ea = current_ea + item_size

print("fix script end success ........")


花指令的剩余套路简介与基本构造


看了这么多代码我相信老鸟都看烦了,不拖拉直接把基础剩余的花指令套路大概过一遍直接上真实样本分享我的修正思路,大家有更好的思路也可以交流。


花指令的类型还有以下几种:

1.虚假控制流+ DCQ
2.虚假控制流 + B + 栈不平
3.B + 栈不平
4.利用x30寄存器进行 RET 跳转


还有很多精心构造的下边我给出具体构造的例子,不会再写具体修正脚本大家可以自己试着去修正一下。


◆虚假控制流+B 样本.cpp

 

/****
*@author 周樟寿
*/
int bcf(int a) {
return (a + a);
}
static __attribute__((always_inline)) int case3() {
int a = 3;
if (bcf(a) != 0) {
#if __arm64__
__asm__(
"b 0xc\n"
"udf #0x5397FB1\n"
".long 87654321\n"
".long 12345678\n"
);
#endif
}
int b = 1 * 3 + (5 * 6);
return b;
}


这只是简单的例子虚假控制流按我这个写法会被编译器优化掉实际情况远比这复杂的多,仅供参考。


◆B + 栈不平


static __attribute__((always_inline)) int case4() {

int a = 3;
if (bcf(a) != 0) {
#if __arm64__
__asm__(
"b 0xc\n"
"add sp,sp,#0x100\n"
"add sp,sp,#0x100\n"
);
#endif
}
int b = 1 * 3 + (5 * 6);
return b;
}


例子和构造样本就先简单到这里我们直接结合,真实样本样本学习其中的套路。


开水团花指令学习与修正 xxxx.so & xxxx


看开水团系列的应用我是纯抱着学习态度去的,因为平时也用但是觉得卡卡的偶尔还非常热抱着好奇心的态度去学习,你别说有点东西下边开始分享我的学习过程,尽量分享的细致一点和我的思路,这个学习记录没有参考别人的修正方案按自己的理解进行修正,称不上完美但是可以f5了看起来清爽一点,如果哪里有脚本上的优化大家可以交流集思广益。


◆样本总结

包含的种类非常多我只修正了我关心的部分会讲解修正过程以及他们之间的关系,开水团的花指令是有关联的属于嵌套关系而且还夹杂了DCQ这种数据,其中BR x8这种动态跳转我没有仔细去看不在我的修正范围。


◆搜索方向

安卓从ida pro的export 窗口看 JNI_onLoad 开始看起就能发现花指令的存在了,一直跟着向下即可,同Group产品我也都看了双平台的花指令套路是一致的,大家不要去看iOS端修起来很费时间,如果学习尽量看安卓下边正式开始吧。


样本1之 利用 x30 (LR) 寄存器 + RET 强制 停止函数


在点进去JNI_onLoad就会看到如下:


.text:14580 E0 7B 3E A9 STP X0, X30,[SP,#var_20]
.text:14584 01 00 00 94 BL sub_14588
.text:14584 ; End of function JNI_OnLoad
sub_14588
.text:14588 60 00 00 10 ADR X0, loc_14594
.text:1458C FE 03 00 AA MOV X30, X0
.text:14590 C0 03 5F D6 RET


这个样本很有意思,第一利用了ADR地址无关性把目标地址放进了X0寄存器,第二利用 X30寄存器也就是LR寄存器直接跳了过去,RET其实等同于mov pc,lr那如何修正呢,先人工修正一下看看。


修正.asm


.text:14580 E0 7B 3E A9 STP X0, X30,[SP,#var_20]
.text:14584 01 00 00 94 BL sub_14588
.text:14584 ; End of function JNI_OnLoad
sub_14588
.text:14588 60 00 00 10 nop
.text:1458C FE 03 00 AA nop
.text:14590 C0 03 5F D6 B loc_14594


这里手工修正,修正了3条指令核心就在RET修正B 目标地址,这里其实有朋友会说为什么不在BL sub_14588这里修正B loc_14594因为实际情况远比想象的复杂得多,这个ret中间的位置可能会出现 sp栈相关操作要不要保留?不清楚干嘛的都要保留,我直接放出修正脚本写的比较low大家别喷有些没找到函数,我就直接手工操作的。


◆修正.py


/****
*@author 周樟寿
*/
import ida_bytes
import idautils
import idc
import re

from keystone import *

if __name__ == '__main__':

"""
初始化汇编 引擎 keystone
"""

ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)

arm64_nop = b"\x1f\x20\x03\xd5"

segments = idautils.Segments()

for seg in segments:
"""这里只处理 代码段"""
x_30_ret_total = 0

if idc.get_segm_name(seg) in ["__text", ".text"]:
"""获取段信息"""
seg_name = idc.get_segm_name(seg)
seg_start_addr = idc.get_segm_start(seg)
seg_end_addr = idc.get_segm_end(seg) - 4
seg_size = seg_end_addr - seg_start_addr

current_ea = seg_start_addr

while current_ea <= seg_end_addr:
item_size = idc.get_item_size(current_ea)
is_asm_code = ida_bytes.is_code(ida_bytes.get_flags(current_ea))

if is_asm_code:
current_mnem = idc.print_insn_mnem(current_ea)

"""
优化修复逻辑 里边有很多地方都是 通过地址-4 这种计算方式来判断的,遇到了特殊情况就无法搜索
需要向上逐4 字节搜索

"""
new_fix = True
if new_fix:

"""
这是原始模板,但是存在中间穿插 栈平衡相关得指令无法使用 精确 -4 判断 第一个操作数是 x30
和 -8 判断 mnem 是 adr
adr x0 ,#0xdc
mov x30 x0
ret

这里优化搜索规则 当前是RET 指令向上搜索, 如果向上搜索 4-8 字节 发现有操作 X30 (LR) 寄存器 就标
标记好了,x30 的 操作位置,继续向上搜索 查找ADR 指令 找到要跳转 得目标
"""

if current_mnem == 'RET':
if ((idc.print_operand(current_ea - 4, 0) == 'X30') and (
idc.print_insn_mnem(current_ea - 4) == 'MOV')) \
or \
(idc.print_operand(current_ea - 8, 0) == 'X30' and idc.print_insn_mnem(
current_ea - 8) == 'MOV'

):
# (
# (idc.print_operand(current_ea - 4, 0) == 'X30') and (
# idc.print_insn_mnem(current_ea - 4) == 'MOV')
# ):

if idc.print_insn_mnem(current_ea - 4) in ['ADD', 'MOV']:
print(
f'current {hex(current_ea)} x30_ret sub_4={idc.print_insn_mnem(current_ea - 4)}')
"""统计"""

x_30_mov = 0
x_30_adr = 0

"""
nop ret
"""
# ida_bytes.patch_bytes(current_ea, arm64_nop)

location_addr = 0

if idc.print_insn_mnem(current_ea - 4) == "MOV":
"""
nop mov
"""
# ida_bytes.patch_bytes(current_ea, arm64_nop)
# ida_bytes.patch_bytes(current_ea - 4, arm64_nop)

"""
用于确定具体跳转目标得指令位置
"""
location_addr = current_ea - 8

elif idc.print_insn_mnem(current_ea - 4) == "ADD":

# ida_bytes.patch_bytes(current_ea - 8, arm64_nop)

location_addr = current_ea - 12
else:
print(f'未知:{hex(current_ea)}')

if location_addr != 0:

try:
b_number = int(
re.search(r'[0-9A-F]+', idc.print_operand(location_addr, 1)).group(),
16)

path_asm = f'b #{hex(b_number - current_ea)}'
print(
f' address:{hex(location_addr)} operand:{hex(b_number - current_ea)} asm:{path_asm}')

encoding, count = ks.asm(path_asm)
patch_byte = bytes([int(i) for i in encoding])
print(f"------------------->{path_asm} = [", end='')
for i in encoding:
print("%02x " % i, end='')
print(']')

"""
修正跳转
"""
ida_bytes.patch_bytes(location_addr, arm64_nop)

"""
修正ret
"""
ida_bytes.patch_bytes(current_ea, patch_byte)

if idc.print_insn_mnem(current_ea - 4) == "MOV":
"""
nop mov
"""
# ida_bytes.patch_bytes(current_ea, arm64_nop)
ida_bytes.patch_bytes(current_ea - 4, arm64_nop)
elif idc.print_insn_mnem(current_ea - 4) == "ADD":
ida_bytes.patch_bytes(current_ea - 8, arm64_nop)


except Exception as e:
print(f'出错地址:{hex(location_addr)}')
#
# asm_number = b_number - location_addr
#
# path_asm = f'b #{b_number}'

# encoding, count = ks.asm(path_asm)
# patch_byte = bytes([int(i) for i in encoding])
# print(f"------------------->{path_asm} = [", end='')
# for i in encoding:
# print("%02x " % i, end='')
# print(']')
#
# ida_bytes.patch_bytes(location_addr, patch_byte)

x_30_ret_total = x_30_ret_total + 1

if not new_fix:
"""如果之 这种花指令跳转规则 """
if (current_mnem == 'RET') and (idc.print_operand(current_ea - 4, 0) == 'X30') and (
idc.print_operand(current_ea - 4, 1) in ['X0', 'W0']):
"""当前指令-8"""
sub_8_mnem = idc.print_insn_mnem(current_ea - 8)

sub_8_imm = 0
b_imm = 0

if sub_8_mnem == "ADR":

has_patch = False

sub_8_imm = idc.print_operand(current_ea - 8, 1)
sub_8_imm = int(re.search(r'[0-9A-F]+', sub_8_imm).group(), 16)

"""
计算地址
立即数 = 目标地址-当前地址

"""

b_imm = sub_8_imm - (current_ea - 8)

if has_patch:
ida_bytes.patch_bytes(current_ea, arm64_nop)
ida_bytes.patch_bytes(current_ea - 4, arm64_nop)

path_asm = f'bl #{b_imm}'

encoding, count = ks.asm(path_asm)
patch_byte = bytes([int(i) for i in encoding])
print(f"------------------->{path_asm} = [", end='')
for i in encoding:
print("%02x " % i, end='')
print(']')

if has_patch:
ida_bytes.patch_bytes(current_ea - 8, patch_byte)

"""统计"""
x_30_ret_total = x_30_ret_total + 1
print(
f'address:{hex(current_ea)} sub_8_mnem[{sub_8_mnem}] sub_8_imm={hex(sub_8_imm)} b_offset:{hex(b_imm)}')
"""skip"""
current_ea = current_ea + item_size

print(f"total all ret30 total:{x_30_ret_total}")


整体脚本实现相对来说比较啰嗦,我讲究的是能用就行剩下的交给GPT帮我优化,脚本包含我刚开始的修正代码,当时修正的是错误的大家看看就好了。


第二种花指令,是隐藏真实跳转地址,并交给中央转发器统一分发


以前没见过就是觉得非常厉害大概的跳转思路设计的很精妙,这也导致了我刚开始无脑
NOP陷入了痛苦深渊,直到写这篇文章的时候有开源项目实现过这种隐藏真实跳转的LLVM项目(https://github.com/amimo/goron),写这篇文章的时候说实话没有看过他的核心思想就是根据样本学习和动态调试,一步一步扣出来的,下边结合样本和具体修正思路来看比较好,样本种类很多大家慢慢看别急。


◆样本1


.text:14594 E0 7B 7E A9 LDP X0, X30, [SP,#-0x20]
.text:14598 E0 7B 3B A9 STP X0, X30, [SP,#-0x50]
.text:1459C 20 00 80 D2 MOV X0, #1
.text:145A0 02 00 00 14 B loc_145A8
.text:145A4 06 00 00 00 DCD 6
.text:145A8 15 02 00 94 BL sub_14DFC
.text:145AC 14 00 00 00 DCD 0x14
.text:145B0 30 00 00 00 DCD 0x30
.text:145B4 8C 00 00 00 DCD 0x8C
.text:145B8 B8 00 00 00 DCD 0xB8
.text:145BC 1F 20 03 D5 DCD 0xD503201F


◆中央转发器


.text:14DFC E0 07 BF A9 STP X0, X1, [SP,#var_10]!
.text:14E00 C0 5B 60 B8 LDR W0, [X30,W0,UXTW#2]
.text:14E04 DE 43 20 8B ADD X30, X30, W0,UXTW
.text:14E08 E0 07 C1 A8 LDP X0, X1, [SP+0x10+var_10],#0x10
.text:14E0C C0 03 5F D6 RET


这要结合中央转发器来看,先说中央转发器都干了什么里边通篇就只做了一个运算操作,最终跳出到目标地址依然利用了 RET X30 的特性这里不过多介绍,核心突出的就是计算和跳转不要关心STPLDP。


◆计算就干了一件事情 公式如下 x30=x30+load(x30+(x0<<2))


公式是这样,读懂需要看上下文刚开始我也很懵,最后调试几遍就发现了规律,首先看x30x0的值是这么来的计算一遍看看。


先看X30(LR)已知条件是LR寄存器永远指向下一条要执行的指令地址拿样本1中来看,当前
X30=0x145AC,为什么因为调用中央转发器用的是BL有返回的跳转。


再看x0的值,向上溯源调用中央转发器前就有一条MOV X0, #1这个#1是什么,这里可以理解为跳转编号,大家可以找到中央转发器看一下XREF引用每个调用前都会有 X0或者W0的立即数赋值操作。


◆tips


大家要注意具有迷惑性的0x145A4 DCD 0x6实际样本中还有一种 x0 赋值方式就是
LDR w0,0xc这并不是直接把0xc赋值给 w0,LDR是基于 PC寄存器的一种地址无关性的读取操作,说人话的意思就是加载 pc寄存器地址+0xc 存放的内容放到 w0寄存器中具体会在脚本中体现因为要考虑到。


◆根据样本我们实际算一遍就明白了


X30 = 0x145AC +Load(0x145AC+(1<<2))

#这一步只是地址计算
0x145AC+0x4 = 0x145B0
#加载这个地址的值
Load(0x145B0)=0x30

#实际得出的地址
0x145AC+0x30 = 0x145DC


◆核心思想与总结

利用中央转发起进行x30 条转,配合花指令存储偏移传递不同编号进行跳转,干扰编译器。
利用花指令保存真实跳转偏移植,屏蔽脚本小子无脑NOP,让花指令数据变得有意义。
样本都嵌套的一层嵌套一层。


具体修正与脚本


实际修正脚本也是写了非常多,还有很多情况需要考虑还好我都写了中文注释。


find_中央转发器.py


/****
*@author 周樟寿
*/
import ida_bytes
import idautils
import idc

if __name__ == '__main__':
segments = idautils.Segments()
for seg in segments:
"""这里只处理 代码段"""
if idc.get_segm_name(seg) in ["__text", ".text"]:
seg_name = idc.get_segm_name(seg)
seg_start_addr = idc.get_segm_start(seg)
seg_end_addr = idc.get_segm_end(seg) - 4
seg_size = seg_end_addr - seg_start_addr
print(
f's_name:{seg_name} s_start:{hex(seg_start_addr)} s_end:{hex(seg_end_addr)} s_size:{seg_size} has_align:{seg_size % 4 == 0}')

current_ea = seg_start_addr
while current_ea <= seg_end_addr:
"""判断是不是 ASM"""
is_asm_code = ida_bytes.is_code(ida_bytes.get_flags(current_ea))
""" 获取长度 """
item_size = idc.get_item_size(current_ea)
if not is_asm_code:
create_result = ida_bytes.create_dword(current_ea, 4, True)
current_ea = current_ea + 4
else:
c_asm = idc.GetDisasm(current_ea)
if c_asm == "ADD X30, X30, W0,UXTW":
print(f'全局跳转.....address:{hex(current_ea)} asm:{c_asm}')
current_ea = current_ea + item_size

print('format script success ........')


修正中央跳转.py

这个脚本的修正思路,通过中央转发器的 xref 引用进行向上修复,前提是通过脚本找到,我贴心的为大家打印了日志和修正开关,还有对跳转位置写了备注。


import struct

import ida_bytes
import idautils
import idc

import re

from keystone import *


def op_convert(x):
if x.startswith("0x"):
return int(x, 16)
else:
return int(x)


"""
此脚本应用于 特殊花指令跳转........
"""

if __name__ == '__main__':

arm64_nop = b"\x1f\x20\x03\xd5"

"""
初始化汇编 引擎 keystone
"""

ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)

"""
这里是经过分析 找到得全局分发器入口地址
"""
# redirects = [0x1065FEBD4, 0x106D959F0]
redirects = [0x14DFC]

for redirect in redirects:

for xref in idautils.XrefsTo(redirect, 0):
print('============================================================================')
print(xref.type, idautils.XrefTypeName(xref.type), 'from', hex(xref.frm), 'to', hex(xref.to))

redirect_base = xref.frm + 4
print(f'redirect base:{hex(redirect_base)}')

for a_xref in idautils.XrefsTo(xref.frm, 0):
"""获得 x0 后边得立即数"""
op1 = idc.print_operand(a_xref.frm - 4, 1).replace("=", "").replace("#", "")
"""转换 数字"""
xx_number = op_convert(op1)

"""计算偏移"""
offset = xx_number << 2

f_bytes = ida_bytes.get_bytes(redirect_base + offset, 4)

n_tuple = struct.unpack('<I', f_bytes)
hex_data = [hex(num) for num in n_tuple][0]
index_arr = redirect_base + int(hex_data, 16)

"""添加备注 """
ida_bytes.set_cmt(a_xref.frm, f"current redirect:{hex(index_arr)}", False)

print(
f"--[{idc.print_insn_mnem(a_xref.frm - 4)}-{idc.print_insn_mnem(a_xref.frm - 8)}]---------[{xx_number}-{offset}]-----to:[{hex(index_arr)}]-------imm[{hex(index_arr - a_xref.frm)}]---->",
a_xref.type, idautils.XrefTypeName(a_xref.type), 'from', hex(a_xref.frm), 'to', hex(a_xref.to))

"""
开始修正这些指令

计算方式 = 实际目标地址-当前地址 = 立即数
b 所得立即数
"""
has_patch = True
if has_patch:

patch_imm = hex(index_arr - a_xref.frm)
path_asm = f'b #{patch_imm}'

encoding, count = ks.asm(path_asm)
patch_byte = bytes([int(i) for i in encoding])
print(f"------------------->{path_asm} = [", end='')
for i in encoding:
print("%02x " % i, end='')
print(']')
ida_bytes.patch_bytes(a_xref.frm, patch_byte)

"""
patch 完 这些指令 要把 操作 x0 和 w0 这种指令也 nop掉 向上 搜索
"""
if has_patch:

sub_4 = a_xref.frm - 4
sub_8 = a_xref.frm - 8
if (idc.print_insn_mnem(sub_4) in ["LDR", "MOV"]) and idc.print_operand(sub_4, 0) in ["X0", "W0"]:
ida_bytes.patch_bytes(sub_4, arm64_nop)
if (idc.print_insn_mnem(sub_8) in ["LDR", "MOV"]) and idc.print_operand(sub_8, 0) in ["X0", "W0"]:
ida_bytes.patch_bytes(sub_8, arm64_nop)

print('============================================================================\n')

print('xref fix script sucess.......')


后续的畅想&吐槽


我只是分享出来我的学习过程,顺便吐槽一下开水团 Group的APP很卡,还有就是我在不同意隐私条款的情况下依然在抽样我的设备信息,具体条款我也看了简直太流氓了。其次就是有那么点不单纯窥探,这些脚本并不能够完全修正但F5但是可以不致于抓瞎操作,当前这个脚本适用于 安卓和iOS端开水团group的产品,后边我会把修正字符串加密的脚本也放出,兄弟团大概率会把他们的编译器升级,把具体跳转地址也加密了,大家有一起学习iOS&Android逆向的朋友可以一起交流。


连载后续计划


todo 因为本次教程只是抛砖引玉,并不是完美修复与完美f5,还要修正是样本中间接二次跳转还是会干扰反汇编引擎,中间有很多栈相关的操作其实在修正后都是无用的,我还在慢慢手动Patch去二级跳转带来的反汇编引擎JUMPOUT,如果要让IDA完美解析需要把中间二级跳转与无用栈指令全都干掉。





看雪ID:mb_fssslkzs

https://bbs.kanxue.com/user-home-904969.htm

*本文为看雪论坛优秀文章,由 mb_fssslkzs 原创,转载请注明来自看雪社区


# 往期推荐

1、在 Windows下搭建LLVM 使用环境

2、深入学习smali语法

3、安卓加固脱壳分享

4、Flutter 逆向初探

5、一个简单实践理解栈空间转移

6、记一次某盾手游加固的脱壳与修复




球分享

球点赞

球在看

文章引用微信公众号"看雪学苑",如有侵权,请联系管理员删除!

博客评论
还没有人评论,赶紧抢个沙发~
发表评论
说明:请文明发言,共建和谐网络,您的个人信息不会被公开显示。