unicorn模拟执行在逆向中的妙用-以2024古剑山India Pale Ale为例
ATRI 发表于 江苏 移动安全 301浏览 · 2024-12-02 13:16

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}

附件

附件:
0 条评论
某人
表情
可输入 255