版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/
ARM64Emulator
ARM64Emulator 是基于 Unicorn 实现一个轻量级的 ARM64 模拟器,具备代码加载、内存映射、指令执行、反汇编、寄存器监控、Hook、Patch、字符串处理等功能
项目地址:github.com/CYRUS-STUDI...
这里主要使用 ARM64Emulator 模拟执行 so 中的汇编指令实现算法还原。
目标应用信息
app 中实现一个 CRC32 算法变形,具体实现在 so 中 modifiedCRC32 函数,现在要通过 unicorn 和 IDA Pro 逆向还原 so 中的算法。
项目地址:github.com/CYRUS-STUDI...
目标 so 文件地址:github.com/CYRUS-STUDI...
使用 ARM64Emulator 加载 so 并执行 modifiedCRC32 函数的汇编指令实现算法还原。
python
from unicorn.arm64_const import *
import struct
import re
from ARM64Emulator import ARM64Emulator
def modifiedCRC32(data):
emulator = ARM64Emulator("libcrc32.so")
mu = emulator.mu
# 字符串地址
str_addr = emulator.STACK_BASE + emulator.STACK_SIZE
emulator.mu.mem_map(str_addr, 0x1000) # 4KB
...
# 初始化传参
emulator.set_x0(0) # JNIEnv*
emulator.set_x1(0) # jobject
emulator.set_x2(str_addr) # input
# 运行
emulator.run(0x1C040, 0x1C2D8)
return hex(mu.reg_read(UC_ARM64_REG_X4))
if __name__ == "__main__":
result = modifiedCRC32("546NBypEyvgBt")
print(f"modifiedCRC32 result: '{result}'")
但汇编指令中有调用到一些 JNI 接口函数和系统函数,需要分析汇编代码并替换成对应的 Python 实现。
关键汇编代码分析
_ReadStatusReg
汇编代码如下:
arduino
.text:000000000001C05C 59 D0 3B D5 MRS X25, #3, c13, c0, #2
.text:000000000001C060 3A 01 00 D0 ADRP X26, #modified_crc32_table_ptr@PAGE
.text:000000000001C064 F4 03 02 AA MOV X20, X2
.text:000000000001C068 28 17 40 F9 LDR X8, [X25,#0x28]
.text:000000000001C06C F3 03 00 AA MOV X19, X0
.text:000000000001C070 A8 83 1F F8 STUR X8, [X29,#var_8]
MRS X25, #3, c13, c0, #2
-
MRS(Move from System Register)用于 读取系统寄存器。
-
#3, c13, c0, #2 对应 TPIDR_EL1(线程特定寄存器,常用于存储 TLS 线程本地存储指针)。
-
这行指令 将 TPIDR_EL1 读入 X25,可能是为了访问某个线程局部存储的数据。
ADRP X26, #modified_crc32_table_ptr@PAGE
-
ADRP(Address of Page)用于 获取 modified_crc32_table_ptr 所在的内存页地址,存入 X26。
-
这个指令不会提供完整地址,需要 结合 ADD 或 LDR 获取最终地址。
MOV X20, X2
- 保存 X2 到 X20
LDR X8, [X25,#0x28]
-
从 X25(即 TPIDR_EL1)的偏移 0x28 处读取一个 64-bit 值,存入 X8。
-
X25 指向 TLS(线程局部存储),所以 0x28 偏移量可能是线程相关的变量。
MOV X19, X0
- 备份 X0 到 X19,可能是 函数的第一个参数,用于后续计算。
STUR X8, [X29,#var_8]
-
把 X8 存入 X29(即 FP,帧指针)的 var_8 位置,通常是局部变量或栈空间的一部分。
-
STUR(Store Register Unscaled)是 STR 的变种,支持负偏移。
这段汇编代码中,主要通过 MRS 指令读取系统寄存器中内存访问异常相关状态信息信息,只需要把相关的两条指令 nop 掉就好了。
scss
# v49 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
emulator.patch_nop([0X1C05C, 0X1C068])
GetStringUTFChars
汇编代码如下:
makefile
.text:000000000001C160 68 02 40 F9 LDR X8, [X19]
.text:000000000001C164 E0 03 13 AA MOV X0, X19
.text:000000000001C168 E1 03 14 AA MOV X1, X20
.text:000000000001C16C E2 03 1F AA MOV X2, XZR
.text:000000000001C170 08 A5 42 F9 LDR X8, [X8,#0x548]
.text:000000000001C174 00 01 3F D6 BLR X8
.text:000000000001C174
.text:000000000001C178 F5 03 00 AA MOV X21, X0
逐行分析
arduino
.text:000000000001C160 68 02 40 F9 LDR X8, [X19]
-
LDR X8, [X19]
-
从 X19 指向的地址加载 X8。
-
X19 存放的是 JNIEnv 指针。
-
X8 现在包含了 X19 所指向的 第一个成员变量(通常是虚表指针)。
arduino
.text:000000000001C164 E0 03 13 AA MOV X0, X1
-
MOV X0, X19
-
将 X19 赋值给 X0。
-
函数调用的第一个参数(JNIEnv*)。
arduino
.text:000000000001C168 E1 03 14 AA MOV X1, X20
-
MOV X1, X20
-
将 X20 赋值给 X1。
-
X20 之前存的是 X2,可能是某个参数(比如 jstring 或者 字符串指针)。
arduino
.text:000000000001C16C E2 03 1F AA MOV X2, XZR
-
MOV X2, XZR
-
将 X2 设为 0(XZR 是零寄存器)。
-
可能表示 NULL 指针或默认参数。
arduino
.text:000000000001C170 08 A5 42 F9 LDR X8, [X8,#0x548]
-
LDR X8, [X8,#0x548]
-
从 X8 + 0x548 处加载函数指针到 X8。
-
X8 之前存的是 X19 的 虚表指针,所以这一步可能是 从虚表中读取函数地址。
-
0x548 可能是 JNI 虚函数表的偏移量,指向一个 函数指针。
arduino
.text:000000000001C174 00 01 3F D6 BLR X8
-
BLR X8
-
跳转到 X8 指向的函数,并执行。
-
这个 X8 是 从虚表偏移 0x548 处加载的函数指针。
-
这里是 JNI 方法 GetStringUTFChars
arduino
.text:000000000001C178 F5 03 00 AA MOV X21, X0
-
MOV X21, X0
-
把 X0 的返回值存入 X21
-
是 JNI 调用的返回值,这里是 char*(GetStringUTFChars)。
把 LDR X8, [X19] 和 BLR X8 nop 掉,再替换成自定义的 hook 函数 get_string_utf_chars,在函数中实现自定义的 GetStringUTFChars
scss
data = "546NBypEyvgBt"
# 字符串地址
str_addr = emulator.STACK_BASE + emulator.STACK_SIZE
emulator.mu.mem_map(str_addr, 0x1000) # 4KB
# v33 = (*env)->GetStringUTFChars(env, input, 0LL);
def get_string_utf_chars():
emulator.get_string_utf_chars(data, str_addr)
emulator.patch_nop_range(0X1C160, 0X1C174)
emulator.register_hook(0X1C174, get_string_utf_chars)
strlen
arduino
.text:000000000001C178 F5 03 00 AA MOV X21, X0
.text:000000000001C17C A1 85 00 94 BL .strlen
把调用 strlen 的地址 nop 掉再替换成 Python 中的 len 就好
scss
# v34 = strlen(v33);
def strlen():
emulator.set_x0(len(data))
emulator.patch_nop([0x1c17c])
emulator.register_hook(0x1c17c, strlen)
memmove(v36, v33, v35);
arduino
.text:000000000001C1D4 E0 03 17 AA MOV X0, X23 ; dest
.text:000000000001C1D8 E1 03 15 AA MOV X1, X21 ; src
.text:000000000001C1DC E2 03 16 AA MOV X2, X22 ; n
.text:000000000001C1E0 90 85 00 94 BL .memmove
memmove 函数用于在内存中复制数据,它的作用是安全地从源地址拷贝指定字节数到目标地址,即使源和目标内存区域重叠也能正确处理。
原型:
arduino
void *memmove(void *dest, const void *src, size_t n);
参数:
-
dest:目标内存地址。
-
src:源内存地址。
-
n:要复制的字节数。
在 Unicorn 中模拟 memmove 可以通过 mem_read 和 mem_write 实现,具体方式如下:
-
获取 X0 (dest)、X1 (src)、X2 (n) 寄存器的值
-
读取 src 地址的 n 字节数据
-
写入 dest 地址
-
设置 X0 返回 dest
ini
# memmove(v36, v33, v35);
def memmove():
dest = mu.reg_read(UC_ARM64_REG_X0)
src = mu.reg_read(UC_ARM64_REG_X1)
n = mu.reg_read(UC_ARM64_REG_X2)
if n == 0:
return # 不需要拷贝
print(f"memmove Hooked: Copying {n} bytes from {hex(src)} to {hex(dest)}")
# 读取 src 地址的数据,确保是 bytes
data = bytes(mu.mem_read(src, n))
# 如果 src 和 dest 有重叠,`memmove` 需要支持前后移动
if src < dest < src + n:
# `memmove` 需要从后往前复制以避免覆盖
for i in range(n - 1, -1, -1):
mu.mem_write(dest + i, data[i:i + 1])
else:
# 正常拷贝
mu.mem_write(dest, data)
# `memmove` 的返回值是 `dest`
mu.reg_write(UC_ARM64_REG_X0, dest)
emulator.patch_nop([0x1C1E0])
emulator.register_hook(0x1C1E0, memmove)
ReleaseStringUTFChars
arduino
.text:000000000001C1E4 FF 6A 36 38 STRB WZR, [X23,X22]
.text:000000000001C1E8 68 02 40 F9 LDR X8, [X19]
.text:000000000001C1EC 08 A9 42 F9 LDR X8, [X8,#0x550]
.text:000000000001C1F0 ; try {
.text:000000000001C1F0 E0 03 13 AA MOV X0, X19
.text:000000000001C1F4 E1 03 14 AA MOV X1, X20
.text:000000000001C1F8 E2 03 15 AA MOV X2, X21
.text:000000000001C1FC 00 01 3F D6 BLR X8
arduino
.text:000000000001C1E8 68 02 40 F9 LDR X8, [X19]
.text:000000000001C1EC 08 A9 42 F9 LDR X8, [X8,#0x550]
-
LDR X8, [X19] 取 X19 指向的地址的值,并存入 X8
-
LDR X8, [X8,#0x550] 从 X8 + 0x550 处加载值(可能是一个函数指针)
arduino
.text:000000000001C1FC 00 01 3F D6 BLR X8
-
BLR X8 间接跳转,调用 X8 指向的函数
-
这里是 ReleaseStringUTFChars
ReleaseStringUTFChars 是用于释放由 GetStringUTFChars 获取的 UTF-8 编码的字符串。
由于这里的 GetStringUTFChars 替换成了 Python 的实现所有不需要了。直接 nop 掉就好。
scss
# (*env)->ReleaseStringUTFChars(env, input, v33);
emulator.patch_nop([0x1C1FC, 0x1c1ec])
vsnprintf
arduino
.text:000000000001C3A4 2B 85 00 94 BL .vsnprintf
vsnprintf 函数用于格式化字符串并将结果写入缓冲区。
vsnprintf 函数原型
arduino
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
-
str:输出缓冲区
-
size:缓冲区大小
-
format:格式化字符串
-
ap:变长参数
在 Unicorn 中 Hook vsnprintf
-
读取 format 字符串
-
解析 va_list 参数
-
使用 Python 格式化字符串
-
写入 str 缓冲区
-
返回格式化后的字符串长度
python
import struct
import re
# vsnprintf(a1, 9u, "%08x", arg)
def vsnprintf():
X0 = mu.reg_read(UC_ARM64_REG_X0) # char *str
X1 = mu.reg_read(UC_ARM64_REG_X1) # size_t size
X2 = mu.reg_read(UC_ARM64_REG_X2) # const char *format
X3 = mu.reg_read(UC_ARM64_REG_X3) # va_list ap
# 读取 format 字符串
fmt_bytes = mu.mem_read(X2, 100) # 读取格式字符串(假设最长 100 字节)
fmt_str = fmt_bytes.split(b'\x00')[0].decode('utf-8')
print(f"vsnprintf Hooked: format = '{fmt_str}', buffer = {hex(X0)}, size = {X1}, va_list = {hex(X3)}")
# 解析 va_list 参数
args = []
ap = X3
format_specifiers = re.findall(r"%[#0-9]*[dxslu]", fmt_str) # 解析格式
for spec in format_specifiers:
if spec[-1] in 'di': # 解析 %d, %i (整数)
val = struct.unpack("<i", mu.mem_read(ap, 4))[0]
args.append(val) # 确保是整数
ap += 8
elif spec[-1] in 'xX': # 解析 %x, %X (十六进制)
val = struct.unpack("<I", mu.mem_read(ap, 4))[0]
args.append(int(val)) # **关键修正:存储整数**
ap += 8
elif spec[-1] in 'lu': # 解析 %lu (long unsigned)
val = struct.unpack("<Q", mu.mem_read(ap, 8))[0]
args.append(int(val)) # 确保是整数
ap += 8
elif spec[-1] in 's': # 解析 %s (字符串)
ptr = struct.unpack("<Q", mu.mem_read(ap, 8))[0]
str_bytes = mu.mem_read(ptr, 100).split(b'\x00')[0] # 读取字符串
args.append(str_bytes.decode('utf-8'))
ap += 8
elif spec[-1] in 'c': # 解析 %c (字符)
val = struct.unpack("<B", mu.mem_read(ap, 1))[0]
args.append(chr(val))
ap += 8
# 使用 Python 进行格式化
try:
formatted_str = fmt_str % tuple(args)
except TypeError as e:
print(f"Format error: {e}, fmt_str: '{fmt_str}', args: {args}")
return
print(f"vsnprintf result: '{formatted_str}'")
# 写入目标缓冲区
output_bytes = formatted_str.encode('utf-8')[:X1 - 1] # 不能超过 size
mu.mem_write(X0, output_bytes + b'\x00')
# 返回字符串长度
mu.reg_write(UC_ARM64_REG_X0, len(output_bytes))
emulator.patch_nop([0x1C3A4])
emulator.register_hook(0x1C3A4, vsnprintf)
NewStringUTF
vbnet
.text:000000000001C284 68 02 40 F9 LDR X8, [X19]
.text:000000000001C288 08 9D 42 F9 LDR X8, [X8,#0x538]
.text:000000000001C28C ; try {
.text:000000000001C28C A1 47 00 D1 SUB X1, X29, #-var_11
.text:000000000001C290 E0 03 13 AA MOV X0, X19
.text:000000000001C294 00 01 3F D6 BLR X8
逐条指令解析
- LDR X8, [X19]
-
从 X19 指向的地址加载一个值到 X8,这个值通常是一个指针。
-
X19 很可能是某个对象的 基址 (比如 this 指针,或者一个全局结构体)。
- LDR X8, [X8,#0x538]
-
在 X8 存储的地址上 偏移 0x538 处 再次读取值,并存入 X8。
-
X8 现在 存储了一个函数指针,通常是一个 虚函数表 (vtable) 的偏移,或者 全局函数指针。
- SUB X1, X29, #-var_11
-
X29 是 FP (Frame Pointer),用来管理当前栈帧。
-
var_11 是栈上的一个局部变量,SUB 操作实际上是 计算局部变量的地址,然后传递给 X1。
-
X1 可能是一个 指向结构体或缓冲区的指针,用于存储结果。
- MOV X0, X19
- 把 X19 (对象或结构体的基址) 传递给 X0,作为第一个参数。
- BLR X8
-
调用 X8 指向的函数。
-
(*env)->NewStringUTF(env, v48)
python 中模拟 NewStringUTF
scss
# result = (*env)->NewStringUTF(env, v48);
def new_string_utf():
# 获取 X1 = UTF-8 字符串地址
utf8_addr = mu.reg_read(UC_ARM64_REG_X1)
# 读取字符串内容
utf8_string = emulator.read_c_string(utf8_addr)
print(f"NewStringUTF Hooked: Creating Java String for '{utf8_string}'")
# 返回字符串地址
mu.reg_write(UC_ARM64_REG_X0, utf8_addr)
emulator.patch_nop([0x1c288, 0x1c294])
emulator.register_hook(0x1c294, new_string_utf)
stack_chk_fail
__stack_chk_fail 是一个用于检测 栈溢出 的安全函数,通常由编译器自动插入到程序中。
ini
.text:000000000001C314 28 17 40 F9 LDR X8, [X25,#0x28] ; 读取 canary 值
.text:000000000001C318 A9 83 5F F8 LDUR X9, [X29,#var_8] ; 读取局部变量存储的 canary 值
.text:000000000001C31C 1F 01 09 EB CMP X8, X9 ; 比较 canary
.text:000000000001C320 61 00 00 54 B.NE loc_1C32C ; 如果不匹配,跳转到 __stack_chk_fail
.text:000000000001C324 E0 03 13 AA MOV X0, X19 ; 准备参数
.text:000000000001C328 7D 75 00 94 BL sub_3991C ; 调用 sub_3991C
.text:000000000001C32C 45 85 00 94 BL .__stack_chk_fail ; 触发栈溢出保护机制
这个代码片段是标准的 Stack Canary 保护机制:
-
检测栈溢出,防止 buffer overflow 攻击。
-
如果检测失败,调用 __stack_chk_fail,触发崩溃。
找到所有调用 __stack_chk_fail 的地方并 nop 掉就好了。
arduino
.text:000000000001C32C loc_1C32C ; CODE XREF: Java_com_cyrus_example_crc32_CRC32Utils_modifiedCRC32+27C↑j
.text:000000000001C32C ; Java_com_cyrus_example_crc32_CRC32Utils_modifiedCRC32+2A8↑j
.text:000000000001C32C ; Java_com_cyrus_example_crc32_CRC32Utils_modifiedCRC32+2E0↑j
.text:000000000001C32C 45 85 00 94 BL .__stack_chk_fail
在 loc_1C32C 上按 X 找到所有跳转到 __stack_chk_fail 的地址
nop掉
ini
# __stack_chk_fail
emulator.patch_nop([0x1C2E8, 0x1C320, 0x1C2BC])
这样就可以让 Unicorn 继续执行而不会因为 canary 校验失败而崩溃。
完整代码
python
from unicorn.arm64_const import *
import struct
import re
from ARM64Emulator import ARM64Emulator
def modifiedCRC32(data):
emulator = ARM64Emulator("libcrc32.so")
mu = emulator.mu
# 字符串地址
str_addr = emulator.STACK_BASE + emulator.STACK_SIZE
emulator.mu.mem_map(str_addr, 0x1000) # 4KB
# v49 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
emulator.patch_nop([0X1C05C, 0X1C068])
# v33 = (*env)->GetStringUTFChars(env, input, 0LL);
def get_string_utf_chars():
emulator.get_string_utf_chars(data, str_addr)
emulator.patch_nop_range(0X1C160, 0X1C174)
emulator.register_hook(0X1C174, get_string_utf_chars)
# v34 = strlen(v33);
def strlen():
emulator.set_x0(len(data))
emulator.patch_nop([0x1c17c])
emulator.register_hook(0x1c17c, strlen)
# memmove(v36, v33, v35);
def memmove():
dest = mu.reg_read(UC_ARM64_REG_X0)
src = mu.reg_read(UC_ARM64_REG_X1)
n = mu.reg_read(UC_ARM64_REG_X2)
if n == 0:
return # 不需要拷贝
print(f"memmove Hooked: Copying {n} bytes from {hex(src)} to {hex(dest)}")
# 读取 src 地址的数据,确保是 bytes
data = bytes(mu.mem_read(src, n))
# 如果 src 和 dest 有重叠,`memmove` 需要支持前后移动
if src < dest < src + n:
# `memmove` 需要从后往前复制以避免覆盖
for i in range(n - 1, -1, -1):
mu.mem_write(dest + i, data[i:i + 1])
else:
# 正常拷贝
mu.mem_write(dest, data)
# `memmove` 的返回值是 `dest`
mu.reg_write(UC_ARM64_REG_X0, dest)
emulator.patch_nop([0x1C1E0])
emulator.register_hook(0x1C1E0, memmove)
# (*env)->ReleaseStringUTFChars(env, input, v33);
emulator.patch_nop([0x1C1FC, 0x1c1ec])
# vsnprintf(a1, 9u, "%08x", arg)
def vsnprintf():
X0 = mu.reg_read(UC_ARM64_REG_X0) # char *str
X1 = mu.reg_read(UC_ARM64_REG_X1) # size_t size
X2 = mu.reg_read(UC_ARM64_REG_X2) # const char *format
X3 = mu.reg_read(UC_ARM64_REG_X3) # va_list ap
# 读取 format 字符串
fmt_bytes = mu.mem_read(X2, 100) # 读取格式字符串(假设最长 100 字节)
fmt_str = fmt_bytes.split(b'\x00')[0].decode('utf-8')
print(f"vsnprintf Hooked: format = '{fmt_str}', buffer = {hex(X0)}, size = {X1}, va_list = {hex(X3)}")
# 解析 va_list 参数
args = []
ap = X3
format_specifiers = re.findall(r"%[#0-9]*[dxslu]", fmt_str) # 解析格式
for spec in format_specifiers:
if spec[-1] in 'di': # 解析 %d, %i (整数)
val = struct.unpack("<i", mu.mem_read(ap, 4))[0]
args.append(val) # 确保是整数
ap += 8
elif spec[-1] in 'xX': # 解析 %x, %X (十六进制)
val = struct.unpack("<I", mu.mem_read(ap, 4))[0]
args.append(int(val)) # **关键修正:存储整数**
ap += 8
elif spec[-1] in 'lu': # 解析 %lu (long unsigned)
val = struct.unpack("<Q", mu.mem_read(ap, 8))[0]
args.append(int(val)) # 确保是整数
ap += 8
elif spec[-1] in 's': # 解析 %s (字符串)
ptr = struct.unpack("<Q", mu.mem_read(ap, 8))[0]
str_bytes = mu.mem_read(ptr, 100).split(b'\x00')[0] # 读取字符串
args.append(str_bytes.decode('utf-8'))
ap += 8
elif spec[-1] in 'c': # 解析 %c (字符)
val = struct.unpack("<B", mu.mem_read(ap, 1))[0]
args.append(chr(val))
ap += 8
# 使用 Python 进行格式化
try:
formatted_str = fmt_str % tuple(args)
except TypeError as e:
print(f"Format error: {e}, fmt_str: '{fmt_str}', args: {args}")
return
print(f"vsnprintf result: '{formatted_str}'")
# 写入目标缓冲区
output_bytes = formatted_str.encode('utf-8')[:X1 - 1] # 不能超过 size
mu.mem_write(X0, output_bytes + b'\x00')
# 返回字符串长度
mu.reg_write(UC_ARM64_REG_X0, len(output_bytes))
emulator.patch_nop([0x1C3A4])
emulator.register_hook(0x1C3A4, vsnprintf)
# result = (*env)->NewStringUTF(env, v48);
def new_string_utf():
# 获取 X1 = UTF-8 字符串地址
utf8_addr = mu.reg_read(UC_ARM64_REG_X1)
# 读取字符串内容
utf8_string = emulator.read_c_string(utf8_addr)
print(f"NewStringUTF Hooked: Creating Java String for '{utf8_string}'")
# 返回字符串地址
mu.reg_write(UC_ARM64_REG_X0, utf8_addr)
emulator.patch_nop([0x1c288, 0x1c294])
emulator.register_hook(0x1c294, new_string_utf)
# __stack_chk_fail
emulator.patch_nop([0x1C2E8, 0x1C320, 0x1C2BC])
# 初始化传参
emulator.set_x0(0) # JNIEnv*
emulator.set_x1(0) # jobject
emulator.set_x2(str_addr) # input
# 监控寄存器X4的变化
emulator.watch_registers("X4")
# 运行
emulator.run(0x1C040, 0x1C2D8)
return hex(mu.reg_read(UC_ARM64_REG_X4))
if __name__ == "__main__":
result = modifiedCRC32("546NBypEyvgBt")
print(f"modifiedCRC32 result: '{result}'")
运行输出如下:
可以看到结果和 app 中的是一样的。
完整源码地址:github.com/CYRUS-STUDI...