前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 前几天研究某APP未果，中间稍微研究了一些Frida的RPC机制，做一下笔记。 Frida RPC简介 Frida RPC可以将APP中的函数暴露给加载器，方便在外部调用，相比于recv和send，使用起来更加方便。 示例hook脚本 rpc.exports = { add(a, b) { //同步函数 return a + b; }, sub(a, b) { //异步函数 return new Promise(resolve => { setTimeout(() => { resolve(a - b); }, 100); }); } }; 示例Python加载器 import codecs import frida def on_message(message, data): if message['type'] == 'send': print(message['payload']) elif message['type'] == 'error': print(message['stack']) session = frida.attach('iTunes') with codecs.open('./agent.js', 'r', 'utf-8') as f: source = f.read() script = session.create_script(source) script.on('message', on_message) script.load() #调用rpc暴露出来的add和sub方法 print(script.exports.add(2, 3)) print(script.exports.sub(5, 3)) session.detach() Frida RPC使用演示 Java层 Java函数的Hook又分为静态方法和实例方法，静态类直接用Java.use('类名')获取以后直接就可以调用，而实例方法需要先用Java.choose('类名')，在堆中查找到类的实例以后获取该实例，才能调用。 静态方法 实例方法 So层 SO层如果是静态方法也跟Java层的类似，只不过需要获取上下文对象，并作为第一个参数传入。 JNI方法 演示代码 Hook代码 console.log("脚本载入成功"); Java.perform(function () { rpc.exports = { staticmethod() { var n1 = Java.use('com.max.xiaoheihe.utils.n1'); return n1.E(); }, classmethod() { var result = undefined; Java.choose('com.max.xiaoheihe.bean.account.AccountDetailObj', { onMatch: function (instance) { result = instance.getUserid(); }, onComplete: function () { if (result === undefined) { return 'not found'; } } }); return result; }, jnimethod(urlpath, timestamp, nonce) { var currentApp = Java.use('android.app.ActivityThread').currentApplication(); var context = currentApp.getApplicationContext(); var NDKTools = Java.use('com.max.xiaoheihe.utils.NDKTools'); return NDKTools.encode(context, urlpath, timestamp, nonce); }, }; }); 加载器 import frida device = frida.get_usb_device() process = device.attach('小黑盒') with open('demo.js', 'r', encoding='utf-8') as f: script = process.create_script(js.read()) script.load() print('static method: ', script.exports.staticmethod()) print('class method: ', script.exports.classmethod()) print('jni method: ', script.exports.jnimethod( '/app/link/tree/', '1637135684', '00000000000000000000000000000000')) 运行结果如下：
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] [post cid="1365"/] 前情提要 就在我写完上一篇文章后没过一个礼拜，小黑盒就又改 hkey 计算方法了，断断续续花了一礼拜终于搞清楚它的算法。 在这里分享一下逆向的过程。 @> 本文适用于小黑盒 1.3.121 ~ 1.3.124 ，小黑盒历史版本参考我的仓库 Smali 分析 使用Jadx打开安装包，找到hkey的计算函数 可以看到两个版本的 hkey 计算方式略有区别，最明显的就是替换的字符改了 修改以后还是提示参数错误，肯定是 encode 函数也改了，接下来换用IDA So 分析 先贴一下IDA生成的反汇编代码，也不用看的很仔细 int __fastcall encode(int *a1, int a2, int a3, int a4, int a5) { int *v5; // r9 int v6; // r11 _DWORD *v7; // r4 char *v8; // r5 char *v9; // r0 size_t v10; // r0 int v11; // r10 int v12; // r0 int v13; // r0 _DWORD *v14; // r2 const char *v15; // r1 char *v16; // r10 const char *v17; // r11 size_t v18; // r4 size_t v19; // r4 signed int v20; // r8 char *v21; // r6 char *v22; // r0 int v23; // r12 int i; // lr int v25; // r5 int v26; // r0 unsigned int v27; // r4 int v28; // r3 unsigned int v29; // r1 int v30; // r8 const char *v31; // r4 int j; // r1 int k; // r0 int v34; // r0 size_t v35; // r5 size_t v36; // r6 char *v37; // r5 char *v38; // r0 int v39; // r1 int v40; // r12 int l; // lr int v42; // r6 int v43; // r0 unsigned int v44; // r4 int v45; // r3 unsigned int v46; // r2 int result; // r0 int v48; // [sp+0h] [bp-48h] int v49; // [sp+4h] [bp-44h] int *v50; // [sp+8h] [bp-40h] char *nptr; // [sp+Ch] [bp-3Ch] int v52; // [sp+10h] [bp-38h] _DWORD *v53; // [sp+14h] [bp-34h] char s; // [sp+19h] [bp-2Fh] int v55; // [sp+24h] [bp-24h] v5 = a1; v6 = a4; v7 = &_stack_chk_guard; if ( j_check_signature(a1) != 1 ) goto LABEL_28; v53 = &_stack_chk_guard; v8 = (char *)&v48 - ((strlen(KEYTWO[0]) + 62) & 0xFFFFFFF8); v9 = strcpy(v8, KEYTWO[0]); v10 = strlen(v9); _aeabi_memcpy(&v8[v10], "/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=", 56); v7 = 0; v11 = (*(int (__fastcall **)(int *, int, _DWORD))(*v5 + 676))(v5, v6, 0); v12 = *v5; v52 = a5; v13 = (*(int (__fastcall **)(int *, int, _DWORD))(v12 + 676))(v5, a5, 0); if ( v11 ) { v14 = v53; if ( !v13 ) goto LABEL_26; v49 = v6; v50 = &v48; v15 = (const char *)v11; v16 = (char *)v13; v17 = v15; v18 = strlen(v15); v19 = v18 + strlen(v8); v20 = v19 + strlen(v16); v21 = (char *)&v48 - ((v20 + 7) & 0xFFFFFFF8); strcpy((char *)&v48 - ((v20 + 7) & 0xFFFFFFF8), v17); v22 = strcat(v21, v8); nptr = v16; strcat(v22, v16); v23 = v20 - 1; for ( i = 0; v23 > i; ++i ) { v25 = 0; while ( v25 < v23 - i ) { v26 = (int)&v21[v25]; v27 = (unsigned __int8)v21[v25]; v28 = v25; v29 = (unsigned __int8)v21[v25++ + 1]; if ( v27 > v29 ) { v21[v28] = v29; *(_BYTE *)(v26 + 1) = v27; } } } v30 = v20 / 3; v31 = (const char *)malloc(v30 + 2); for ( j = 0; j <= v30 + 1; ++j ) v31[j] = 0; for ( k = 0; k <= v30; ++k ) v31[k] = v21[3 * k]; v34 = atoi(nptr); sprintf(&s, "%#x", v34); v35 = strlen(v31); v36 = strlen(&s) + v35; v37 = (char *)&v48 - ((v36 + 7) & 0xFFFFFFF8); v38 = strcpy(v37, v31); strcat(v38, &s); v39 = v49; v40 = v36 - 1; for ( l = 0; v40 > l; ++l ) { v42 = 0; while ( v42 < v40 - l ) { v43 = (int)&v37[v42]; v44 = (unsigned __int8)v37[v42]; v45 = v42; v46 = (unsigned __int8)v37[v42++ + 1]; if ( v44 > v46 ) { v37[v45] = v46; *(_BYTE *)(v43 + 1) = v44; } } } (*(void (__fastcall **)(int *, int, const char *))(*v5 + 680))(v5, v39, v17); (*(void (__fastcall **)(int *, int, char *))(*v5 + 680))(v5, v52, nptr); v7 = (_DWORD *)j_charToJstring(v5, v37); } v14 = v53; LABEL_26: if ( *v14 == v55 ) return (int)v7; LABEL_28: result = *v7 - v55; if ( *v7 == v55 ) result = j_j_charToJstring(v5, UNSIGNATURE[0]); return result; } 撇开变量部分，稍微整理了一下变量名，以_p结尾的都是指针，具体变量类型参考原始代码 首先可以看到调用了_stack_chk_guard，看名字应该是栈溢出保护，不管他 第58~61行看起来复杂实际上是新建了一个数组，然后把字符串KWYTWO和/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=拼起来存进数组buffer1中 最后有两个__fastcall，根据变量名称，猜测跟传入的参数有关 首先检查了是否传入了urlpath和timestamp这两个变量，如果传入值为空就跳出 第77~84行看起来挺复杂，实际上就是计算出urlpath+buffer1+timestamp的长度(用于开辟内存空间)，然后把这三个字符串拼起来，存进buffer2中 最后是一个循环，看上去蛮复杂，实际上就是冒泡排序，让buffer2中的字符按照Ascii顺序排序 首先取buffer2长度的 1/3+2，创建数组buffer3，然后清空数组，然后是一个循环，buffer3[k] = buffer2[k*3]，数组长度要加2是因为字符串结尾有个\0，以及避免特殊情况数组越界 第109~116行，先把时间戳timestamp转换成十六进制，然后跟buffer3拼接起来，存进buffer4 第118~134行跟刚刚很像，是对buffer4的冒泡排序 最后就是调用charToJstring然后返回 Frida Hook 这里就以验证为主，Hook了3个函数charToJstring,strcpy,strcat，hook.js如下： console.log("脚本载入成功"); Java.perform(function() { var charToJstringAddr = Module.findExportByName("libnative-lib.so", "charToJstring"); console.log(charToJstringAddr); if (charToJstringAddr != null) { Interceptor.attach(charToJstringAddr, { onEnter: function(args) { var arg0 = args[0]; var arg1 = args[1]; console.log('charToJstring - Enter'); console.log(Memory.readCString(arg0)); console.log(Memory.readCString(arg1)) }, onLeave: function(retval) { console.log('======') } }) } var strcat = Module.findExportByName("libc.so", "strcat"); console.log(strcat); if (strcat != null) { Interceptor.attach(strcat, { onEnter: function(args) { var arg0 = args[0]; var arg1 = args[1]; console.log('strcat - Enter'); console.log(Memory.readCString(arg0)); console.log(Memory.readCString(arg1)) }, onLeave: function(retval) { console.log('======') } }) } var strcpy = Module.findExportByName("libc.so", "strcpy"); console.log(strcpy); if (strcpy != null) { Interceptor.attach(strcpy, { onEnter: function(args) { var arg0 = args[0]; var arg1 = args[1]; console.log('strcpy - Enter'); console.log(Memory.readCString(arg0)); console.log(Memory.readCString(arg1)) }, onLeave: function(retval) { console.log('======') } }) } }); 可以配合上面的分析一起看，还是比较好理解的 算法实现 C语言实现 半抄半改写出来的，buffer4存放的是计算结果 #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { // 输入1：/bbs/app/link/view/time // 输入2：1597893810 // 输出1：$()++-/////01245789=CGKMPRUZ_aaabeefhiijklmnopqrtuvwxz （中间结果） // 输出2：$()++-/////001223455789=CGKMPRUZ_aaabbcdeeeffhiijklmnopqrtuvwxxz char buffer1[200] = "//Z1q/Gb/R///+9xZ561TtoHjPrv2ew0Ln8vZnI5oObw+++oa3zw++1yd7wMqU/eNKahfmji5/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time="; //密钥字符串 char buffer2[200] = "\0"; char buffer3[200] = "\0"; char buffer4[200] = "\0"; char buffer5[200] = "\0"; int timestamp = 1597893810; char timestamp_s[] = "1597893810"; char urlpath[] = "/bbs/app/link/view/time"; int i, m = 0; int count = 0; int t1, t2 = 0; int buffer2_len; int buffer3_len; strcpy(buffer2, urlpath); strcat(buffer2, buffer1); strcat(buffer2, timestamp_s); // buffer2内容是 url路径+buffer1+时间戳 buffer2_len = strlen(buffer2); count = buffer2_len - 1; for (i = 0; i < count; ++i) { m = 0; while (m < count - i) { t1 = buffer2[m]; t2 = buffer2[m + 1]; if (t1 > t2) { buffer2[m] = t2; buffer2[m + 1] = t1; } m++; } } buffer3_len = buffer2_len / 3; for (i = 0; i <= buffer3_len; ++i) buffer3[i] = buffer2[3 * i]; sprintf(buffer5, "%#x", timestamp); strcpy(buffer4, buffer3); strcat(buffer4, buffer5); count = strlen(buffer4) - 1; for (i = 0; i < count; i++) { m = 0; while (m < count - i) { t1 = buffer4[m]; t2 = buffer4[m + 1]; if (t1 > t2) { buffer4[m] = t2; buffer4[m + 1] = t1; } m++; } } puts(buffer4); } Python实现 import hashlib # 密钥字符串 ENC_STATIC = '//Z1q/Gb/R///+9xZ561TtoHjPrv2ew0Ln8vZnI5oObw+++oa3zw++1yd7wMqU/eNKahfmji5/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=' # 测试样例 test = [ ('/account/data_report', 1597919951, '$()++-////0112235555789=CGKMPRUZ__aaaccdeefffhijkmnooqrrtuvwxxz'), ('/game/get_game_name', 1597920084, '$()++-////001233445555679=CGKMPRUZ__aaabeeeffghhjkmmnnortuvwxxz'), ('/bbs/app/feeds/news', 1597920132, '$()++-/////011233345556789@DHKNQTXZ_aabbeeeffhhijlnnoprstuwwxxz'), ('/bbs/app/profile/subscribed/events', 1597920180, '$()++-/////00113334555679=CGKMPRUZ_aaabbbdeeeffhhijkmnnopqrsstuvwxxz'), ('/bbs/app/link/view/time', 1597920216, '$()++-/////00123335556789=CGKMPRUZ_aaabdeeeffhiijklmnopqrtuvwxxz') ] # 计算MD5 def md5_calc(data: str) -> str: md5 = hashlib.md5() md5.update(data.encode('utf-8')) result = md5.hexdigest() return(result) # encode实现 def encode(url: str, t: int) -> str: enc = list(f'{url}{ENC_STATIC}{t}') count = len(enc) - 1 for i in range(0, count): for j in range(0, count-i): if (enc[j] > enc[j+1]): enc[j], enc[j+1] = enc[j+1], enc[j] l = len(enc) // 3 + 1 enc += '\0' enc = [enc[3*i] for i in range(0, l)] if enc[-1] == '\0': enc = enc[:-1] enc += list(hex(t)) count = len(enc)-1 for i in range(0, count): for j in range(0, count-i): if (enc[j] > enc[j+1]): enc[j], enc[j+1] = enc[j+1], enc[j] result = ''.join(enc) return(result) # 算法验证 for i, j, k in test: s = encode(i, j) if (s == k): print(i, j, '测试通过') print(k) else: print(i, j, '测试失败') print(k) print(s) 后记 @> 全文完，完整项目在GitHub上开源：链接 (求Star)