unicorn一般用法
unicorn支持多架构(ARM, ARM64 (ARMv8), m68k, MIPS, PowerPC, RISC-V, S390x (SystemZ), SPARC, TriCore & x86 (include x86_64))
uc=Uc(UC_ARCH_ARM64, UC_MODE_ARM)根据不同架构设置
uc_mem_map(address, size)方法,在执行前映射一部分虚拟内存
uc_mem_read(address, size) 读取内存
uc_mem_write(addreee, code) 写入内存
uc.hook_add(UC_HOOK_CODE, code_hook) hook_add函数可添加一个Hook,这个hook每次都会执行而不是只执行一次,可以很方便的访问寄存器和内存,也可以修改PC寄存器来改变流程
India Pale Ale解法
这是一道IOS逆向,直接给了ipa文件,所以我们直接解压ipa后IDA进行分析即可
看了一下其他大佬都是动调就可以直接秒了,但是我没有ios的调试环境,所以借助另一个神器unicorn来解题
这道题我们需要关注init函数,有一些题就会通过init函数修改程序的内容,这里通过3个init函数修改了base64表,rc4的key还有密文,其中key和密文都是简单的异或
base64换表部分
base64部分比较复杂,如果你想直接硬解也行,但是这里讲一下通过模拟执行直接拿到变表的方法,用unicorn写的IDA插件uEmu直接模拟执行这段代码,观察汇编
STP Q0, Q1, [X8] ; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm"...
LDP Q0, Q1, [SP,#0x90+var_70]
STP Q0, Q1, [X8,#(aAbcdefghijklmn+0x20 - 0x10000D848)] ; "ghijklmnopqrstuvwxyz0123456789+/"
可得知魔改的表被写在X8寄存器的地址,直接查看内存就可获得被魔改的表
这个插件是unicorn较为基础的实现,在开始执行前要设置寄存器的初始值,他的好处是比较方便灵活,但是对于内存的操作非常不方便,实现的功能也比较少,所以对于后面的rc4将直接写python脚本进行模拟执行
rc4部分
首先观察这个rc4
void __usercall sub_1000057E0(__int64 *a1@<X0>, __int64 *a2@<X1>, std::string *a3@<X8>)
{
_BYTE *v6; // x0
unsigned __int64 v7; // x10
_BYTE *v8; // x8
unsigned __int64 v9; // x9
unsigned __int64 v10; // x10
unsigned __int64 v11; // x11
__int64 v12; // x8
__int64 v13; // x11
unsigned __int64 v14; // x12
int v15; // w13
__int64 *v16; // x13
__int64 v17; // x9
__int64 v18; // x12
unsigned __int64 v19; // x23
unsigned __int64 v20; // x22
__int64 i; // x21
unsigned __int64 v22; // x8
__int64 v23; // x9
void *__p; // [xsp+8h] [xbp-48h] BYREF
_BYTE *v25; // [xsp+10h] [xbp-40h]
sub_10000601C((int)&__p);
a3->__r_.__value_.__r.__words[0] = 0LL;
a3->__r_.__value_.__l.__size_ = 0LL;
a3->__r_.__value_.__l.__cap_ = 0LL;
v6 = v25;
if ( v25 == __p )
{
v8 = v25;
}
else
{
v7 = 0LL;
v6 = __p;
do
{
v6[v7] = v7;
++v7;
v6 = __p;
v8 = v25;
v9 = v25 - (_BYTE *)__p;
}
while ( v7 < v25 - (_BYTE *)__p );
if ( v9 )
{
v10 = 0LL;
v11 = 0LL;
do
{
v12 = (unsigned __int8)v6[v10];
v13 = v11 + v12;
v14 = *((unsigned __int8 *)a2 + 23);
v15 = (char)v14;
if ( (v14 & 0x80u) != 0LL )
v14 = a2[1];
if ( v15 >= 0 )
v16 = a2;
else
v16 = (__int64 *)*a2;
v11 = (v13 + *((char *)v16 + v10 % v14)) % v9;
v6[v10] = v6[v11];
v6[v11] = v12;
++v10;
v6 = __p;
v8 = v25;
v9 = v25 - (_BYTE *)__p;
}
while ( v10 < v25 - (_BYTE *)__p );
}
}
v17 = *((unsigned __int8 *)a1 + 23);
v18 = a1[1];
if ( (v17 & 0x80u) != 0LL )
{
a1 = (__int64 *)*a1;
v17 = v18;
}
if ( v17 )
{
v19 = 0LL;
v20 = 0LL;
for ( i = v17 - 1; ; --i )
{
v22 = v8 - v6;
v19 = (v19 + 1) % v22;
v23 = (unsigned __int8)v6[v19];
v20 = (v20 + v23) % v22;
v6[v19] = v6[v20];
v6[v20] = v23;
std::string::push_back(
a3,
*(_BYTE *)a1 ^ *((_BYTE *)__p
+ (*((unsigned __int8 *)__p + v20) + (unsigned __int64)*((unsigned __int8 *)__p + v19))
% (v25 - (_BYTE *)__p)));
if ( !i )
break;
a1 = (__int64 *)((char *)a1 + 1);
v6 = __p;
v8 = v25;
}
v6 = __p;
}
if ( v6 )
{
v25 = v6;
operator delete(v6);
}
}
这里的rc4魔改了轮数为0xFA,rc4是对称加密,所以我们直接把密文再用rc4执行一遍就可以解密,对于这种对称加密就非常适合用unicorn来模拟执行了
首先设置cpu架构为arm64
from unicorn import *
from unicorn.arm64_const import *
uc=Uc(UC_ARCH_ARM64, UC_MODE_ARM)
然后扒下我们需要模拟执行的机器码
text=bytes.fromhex("0A0080D2E00308AA0A682A384A050091E0A340A9090100CB5F0109EB63FFFF54690300B40A0080D20B0080D208686A386B01088BAC5E40398D1D0013AF3A40A9BF010071CCB18C9AEDB1959A4E09CC9ACCA90C9BAC69AC386B010C8B6C09C99A8BAD099B09686B3809682A3808682B384A050091E0A340A9090100CB5F0109EB63FDFF5402000014E80300AA895E40392A1D00138B3240A95F01007174B1949A89B1899A490400B4170080D2160080D2350500D1E9060091080100CB2A09C89A57A5089B09687738CA02098B4B09C89A76A9089B086876380868373809683638E8A740A90A6977380B6976386A010A8B290108CB4B09C99A69A9099B08696938890240392801084A011D0013E00313AA1F2003D5B50000B494060091E0A340A9B50600D1E4FFFF17E00740F9")
对于机器码有一些需要注意的就是避免模拟执行到程序的API,所以我在选择机器码的时候直接nop了这段汇编
BL ZNSt3112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE9push_backEc ; std::string::push_back(char)
把机器码写入与IDA对应的内存地址,写入key ,密文
uc.mem_write(0x10000582C, text)
uc.mem_write(0x10001F000,bytes.fromhex('f6ccc8d5c9c0eec0dcedc0d7c0'))#key
uc.mem_write(0x10001F017, b'\x0D') #key_length
uc.mem_write(0x100020000,bytes.fromhex('f10a192a76f635cf0d87480d4749d8a42701821d331d0d66973b6658c3f5e2c6f6'))#密文
设置栈指针,栈初始值
uc.reg_write(UC_ARM64_REG_SP, 0x10000E000)
stack_ptr = 0x10000E000
uc.mem_write(stack_ptr + 0x8, b'\x00\x00\x00\x00\x00\x00\x00\x00') # x0
uc.mem_write(stack_ptr + 0x10, b'\xfa\x00\x00\x00\x00\x00\x00\x00') # x8
设置寄存器初始值
uc.reg_write(UC_ARM64_REG_X20, che)
uc.reg_write(UC_ARM64_REG_X21, key)
uc.reg_write(UC_ARM64_REG_X19, input)
这里有几个坑点就是key和密文长度的设置,因为在源码中是根据字串的长度动态分配内存,而密文的长度大于0x17,程序会将密文重新分配到另一个内存地址,可以看下面源码
_QWORD *__fastcall sub_100005E10(_QWORD *a1, char *__s)
{
size_t v4; // x0
size_t v5; // x20
void *v6; // x22
v4 = strlen(__s);
if ( v4 >= 0xFFFFFFFFFFFFFFF0LL )
sub_1000060E4(a1);
v5 = v4;
if ( v4 >= 0x17 )
{
v6 = operator new((v4 + 16) & 0xFFFFFFFFFFFFFFF0LL);
a1[1] = v5;
a1[2] = (v5 + 16) & 0x7FFFFFFFFFFFFFF0LL | 0x8000000000000000LL;
*a1 = v6;
}
else
{
*((_BYTE *)a1 + 23) = v4;
v6 = a1;
if ( !v4 )
goto LABEL_7;
}
memcpy(v6, __s, v5);
LABEL_7:
*((_BYTE *)v6 + v5) = 0;
return a1;
}
关键点,构造hook函数,所以在hook的时候手动将密文长度赋值给W9寄存器(LDRB W9, [X20,#0x17])
然后由于我nop了std::string::push_back,所以我直接在0x100005938处hook异或之后的结果,(EOR W8, W9, W8)
连在一起即为flag
def code_hook(mu:Uc, addr, size, userdata):
global flag
if addr == 0x1000058B8: #给密文长度赋值 LDRB W9, [X20,#0x17]
mu.reg_write(UC_ARM64_REG_W9, 0x21)
mu.reg_write(UC_ARM64_REG_PC, addr + size)
if addr == 0x100005938:
x0 = mu.reg_read(UC_ARM64_REG_W1)
x8 = mu.reg_read(UC_ARM64_REG_W8)
flag+=chr(x8)
#print(f"x0: {x0:x}, x8: {x8:x}")
uc.hook_add(UC_HOOK_CODE, code_hook)
总结
unicorn是一个强大的工具,在没有调试环境的时候非常适合使用,但是开始模拟执行前要正确对内存,栈,寄存器进行初始化和赋值,还有灵活处理无法模拟执行的情况,这需要清晰得理解程序的汇编。后续我也会尝试配置IOS环境,使用另一个神器unidbg等等
最后完整代码
from unicorn import *
from unicorn.arm64_const import *
text=bytes.fromhex("0A0080D2E00308AA0A682A384A050091E0A340A9090100CB5F0109EB63FFFF54690300B40A0080D20B0080D208686A386B01088BAC5E40398D1D0013AF3A40A9BF010071CCB18C9AEDB1959A4E09CC9ACCA90C9BAC69AC386B010C8B6C09C99A8BAD099B09686B3809682A3808682B384A050091E0A340A9090100CB5F0109EB63FDFF5402000014E80300AA895E40392A1D00138B3240A95F01007174B1949A89B1899A490400B4170080D2160080D2350500D1E9060091080100CB2A09C89A57A5089B09687738CA02098B4B09C89A76A9089B086876380868373809683638E8A740A90A6977380B6976386A010A8B290108CB4B09C99A69A9099B08696938890240392801084A011D0013E00313AA1F2003D5B50000B494060091E0A340A9B50600D1E4FFFF17E00740F9")
uc=Uc(UC_ARCH_ARM64, UC_MODE_ARM)
uc.mem_map(0x0, 0x1000)
uc.mem_map(0x100005000, 0x9000)
uc.mem_map(0x10000E000, 0x10000)
uc.mem_map(0x10001E000, 0x20000)
uc.mem_write(0x10000582C, text)
uc.mem_write(0x10001E000,b'flag{123456789012345678901234567890}')
uc.mem_write(0x10001F000,bytes.fromhex('f6ccc8d5c9c0eec0dcedc0d7c0'))#key
uc.mem_write(0x10001F017, b'\x0D') #key_length
uc.mem_write(0x100020000,bytes.fromhex('f10a192a76f635cf0d87480d4749d8a42701821d331d0d66973b6658c3f5e2c6f6'))#密文
uc.reg_write(UC_ARM64_REG_SP, 0x10000E000)
stack_ptr = 0x10000E000
uc.mem_write(stack_ptr + 0x8, b'\x00\x00\x00\x00\x00\x00\x00\x00') # x0
uc.mem_write(stack_ptr + 0x10, b'\xfa\x00\x00\x00\x00\x00\x00\x00') # x8
input=0x10001E000
key=0x10001F000
che=0x100020000
uc.reg_write(UC_ARM64_REG_X20, che)
uc.reg_write(UC_ARM64_REG_X21, key)
uc.reg_write(UC_ARM64_REG_X19, input)
flag=''
def code_hook(mu:Uc, addr, size, userdata):
global flag
if addr == 0x1000058B8: #给密文赋值
mu.reg_write(UC_ARM64_REG_W9, 0x21)
mu.reg_write(UC_ARM64_REG_PC, addr + size)
if addr == 0x100005938:
w1 = mu.reg_read(UC_ARM64_REG_W1)
w8 = mu.reg_read(UC_ARM64_REG_W8)
flag+=chr(w8)
#print(f"W1: {w1:x}, x8: {w8:x}")
uc.hook_add(UC_HOOK_CODE, code_hook)
#uc.emu_start(0x10000582c, 0x1000058B0)//打印S盒
#print(uc.mem_read(0x0, 0xFa))
uc.emu_start(0x10000582c, 0x100005954)
print(flag)
#flag{45_4_105_r3v3r51n6_b361nn3r}
附件
- ipa.zip 下载