自动化SO字符串解密

使用IDA Python C/Frida 完成自动化SO字符串解密

Posted by BC on April 11, 2019

前言

在SO文件逆向过程中,经常会遇到字符串动态解密,不便于静态分析。
本文提出以下解决方案来调用解密函数
1.低难度解密函数 翻译为python
2.中等难度解密函数 copy出来 编译为DLL 用Ctypes调用DLL
3.★用Frida+flask框架(Frida+Burp Suite)动态调用解密函数
为了完成这个功能,我学习了久违的IDA API,完成了自动寻找解密函数参数,自动注释的功能

目标解密函数

const char *__fastcall decryptString(int encryptedPtr)
{
  int encryptedPtr_1; // r4
  pthread_t pthread_self; // r0
  int self_index_of_table; // r6
  unsigned int table_i; // r1
  signed int self_index_of_wtftable; // r7
  char *wtfbaseArr; // r9
  const char *v7; // r5
  char real_encrypted_char_not_iter; // r0
  signed int xorkey; // r2
  signed int v10; // r1
  char *decryptedPtr; // r3
  int v12; // r7

  encryptedPtr_1 = encryptedPtr;
  pthread_mutex_lock((pthread_mutex_t *)&mutexLock);
  pthread_self = ::pthread_self();              // 0x4006F154
  self_index_of_table = 0;
  table_i = TABLE_I;                            // 初次为0
  if ( TABLE_I >= 1 )
  {
    do
    {
      if ( pthreadTable[self_index_of_table] == pthread_self )
        break;
      ++self_index_of_table;
    }
    while ( self_index_of_table < TABLE_I );
  }
  if ( self_index_of_table == TABLE_I )
  {
    pthreadTable[TABLE_I] = pthread_self;       // 第一次 table[0]=self
    TABLE_I = ++table_i;
  }
  if ( table_i >= 5 )
    TABLE_I = 0;                                // table_i和锁相关 不管他
  self_index_of_wtftable = WTFTABLE[self_index_of_table];
  wtfbaseArr = (char *)&WTFBASE + 0x10000 * self_index_of_table;// WTFBASE开始 0x10000一段
  if ( self_index_of_wtftable > 255 )           // WTFTABLE[256] 且可能循环写入
    self_index_of_wtftable = 0;
  v7 = &wtfbaseArr[0x100 * self_index_of_wtftable];
  _aeabi_memclr(&wtfbaseArr[0x100 * self_index_of_wtftable], 0x100);// wtfbaseArr[256 * self_index_of_wtftable] 申请256空间
  WTFTABLE[self_index_of_table] = self_index_of_wtftable + 1;// 表2[index1]=index2+1
  real_encrypted_char_not_iter = *(_BYTE *)(encryptedPtr_1 + 1);// 00开头 +1 取出加密bytes[]第一个字节
  wtfbaseArr[0x100 * self_index_of_wtftable] = real_encrypted_char_not_iter;// wtfbaseArr[256 * self_index_of_wtftable] 密文首地址
  if ( *(_BYTE *)(encryptedPtr_1 + 1) )
  {
    LOBYTE(xorkey) = 0;                         // 低8
    v10 = 3;
    decryptedPtr = &wtfbaseArr[0x100 * self_index_of_wtftable];// 明文首地址 在下面解密
    do
    {
      *decryptedPtr = (real_encrypted_char_not_iter - 1) ^ xorkey;// charIter = (charIter-1)^xorkey
      xorkey = v10 >> 1;
      real_encrypted_char_not_iter = *(_BYTE *)(encryptedPtr_1 + v10);// 密文字符串 +v10 取iter的后面第v10-1个密文byte
      v7[v10 >> 1] = real_encrypted_char_not_iter;
      decryptedPtr = (char *)&v7[v10 >> 1];
      v12 = *(unsigned __int8 *)(encryptedPtr_1 + v10);
      v10 += 2;
    }
    while ( v12 );
  }
  pthread_mutex_unlock((pthread_mutex_t *)&mutexLock);
  return v7;
}

调用DLL

可以看到这是一个固定key的xor加解密。 笔者首选是把伪代码复制到VS中,输入字符串的char[],执行解密函数,在调试过程中修改IDA F5代码错误的细节。 跑通函数后,将其作为导出函数,编译为DLL。

#define EXPORTFUNC extern "C" __declspec(dllexport)
EXPORTFUNC char *__fastcall maybeDecryptString2(unsigned char *encryptedPtr);

然后在python中,使用Ctypes库,调用DLL的导出函数

import ctypes
from ctypes import *
import idaapi

dll = cdll.LoadLibrary(r'decryptdll.dll')
decryptFunc = dll.maybeDecryptString2 #导出函数名
decryptFunc.restype = c_char_p  #导出函数返回值
#......
pyarray = getBytes(realarg,200) # IDA API
carray = (ctypes.c_uint8 * (255))(*pyarray)
cstr = carray
result = decryptFunc(cstr)  #此处已返回py object

至此,已完成python调用dll的部分。

IDA 寻找参数 自动注释

首先,要使用IDA的API找到函数调用者,在B指令的上几行,找到参数 举一个简单的调用例子

.text&.ARM.extab:78EF3FA4                 LDR             R0, =(off_78F097B8 - 0x78EF3FAA) ;ARM汇编中,R0为第一个参数,也作为返回值。此处参数为0x78F097B8这个地址
.text&.ARM.extab:78EF3FA6                 ADD             R0, PC  ; off_78F097B8
.text&.ARM.extab:78EF3FA8                 LDR             R0, [R0] ;LDR指定,读R0地址指向的数据到R0,此处参数为0x78F097B8指针指向内容
.text&.ARM.extab:78EF3FAA                 BIC.W           R0, R0, #3 
.text&.ARM.extab:78EF3FAE                 BL              decryptString ; apkID 自动注释解密内容为'apkID' 
def find_function_arg(addr,reg='R0'):
while True:
    addr = idc.PrevHead(addr)   #从B指令向上找
    if GetMnem(addr) == "LDR" and reg in GetOpnd(addr, 0) and "=(off_" in GetOpnd(addr, 1): #指令为LDR 操作数0为R0 操作数1为off_开头的加密字符串首地址
    return '0x'+GetOpnd(addr, 1).split('off_')[1].split(' -')[0]
return ""

def CommentAll():
     for x in XrefsTo(DECRYPTFUNC ,flags = 0):
        ref = find_function_arg(x.frm)
        arg = idc.get_wide_dword(int(ref,16))-1
        realarg = arg+finalOffset #finalOffset为 dump so过程中错误重定位的修正 一般例子可以不需要
        pyarray = getBytes(realarg,200) #从realArg读200 byte 在解密函数读到00 00时会自动截断字符串  这里偷懒直接设为200
        carray = (ctypes.c_uint8 * (255))(*pyarray)
        presult = decryptFunc(carray)
        print presult
        MakeComm(x.frm, presult)    #IDA 注释API
        

至此,已完成基本功能。但是方法的通用性还不够高。 如果解密函数需要从某块内存动态读取不同的解密key,或者算法过于复杂,OLLVM等,不能还原成C语言的情况下,只能调用该函数。 于是有了如下方案 Frida+flask

Frida + flask 动态调用函数

下文思路同 HermesAgent,基于xposed的API包装暴露框架 只是把Xposed无法hook的native用Frida实现,把Http服务器用flask实现

关于Frida调用native函数的语法

var decryptFunc = new NativeFunction(ptr(base.add(decryptFuncAddr)),
                            'pointer', #返回值类型
                            ['pointer'] #参数列表
                        );
                        
console.log(decryptFunc(encryptedAddr));


flask用法略

Frida python到js的bridge函数

#.....

def on_message(message, data):
    global myResponse
    #.....
    myResponse = data


script = session.create_script(js_code)

@app.route('/test', methods=['POST'])
def test():
    myResponse = {}
    script.post({'type': 'invoke', 'data': postJson})#postjson为flask接收到的参数
    while myResponse == {}: #轮询response response在onMessage函数中被修改
        time.sleep(0.1)
    return myResponse

Frida js到python的bridge函数

recv('invoke', handleMessage);

function handleMessage(message) {
    recv(handleMessage); #异步首递归 不写的话handleMessage只会被调用一次 这个地方坑了我半天时间
    var myMessage = message['data'];
    console.log('传入参数 ' + myMessage);
    #....
    send({"test":"123"})    #发送到python的onMessage函数

在idaPython脚本中 就可以通过访问flask的http接口 调用解密函数了

第一弹 自动注释完

to be continued 还可以patch so 把调用解密函数的地方直接patch为明文字符串 而不是汇编VIEW里的注释(甚至不能给伪代码VIEW注释)