前言
在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注释)