0X00 内容概述 这篇文章也同步发布在我的公众号上:
f41K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7q4)9J5k6i4N6W2K9i4S2A6L8W2)9J5k6i4q4I4i4K6u0W2j5$3!0E0i4K6u0r3M7#2)9J5c8W2m8Z5f1o6S2x3L8g2)9#2k6W2q4W2c8f1!0g2j5%4y4J5N6i4N6Y4z5g2q4a6N6H3`.`.
libtiny.so 是小某书中一个 核心 native 层业务/基础能力混合库 ,通常承担设备指纹收集、风控等逻辑。
本文将以 小某书 (版本号9.35.0) 的 libtiny.so 为分析对象,从 JNI_OnLoad 作为入口,系统性梳理其 native 初始化流程与 JNI 方法注册机制。 重点会放在如何识别并去除其中的间接跳转与最后还原出可读性强的伪代码。
0X01 初探混淆 首先我们打开libtiny.so,可以看到直接跳转到寄存器X9,审计发现整个so文件有6万多个函数以及非常多的跳转。
同时,可以看到导出表只有一个函数JNI_OnLoad,打开之后伪代码如下
jint JNI_OnLoad (JavaVM * vm , void * reserved ){ jint result ; // w0 _ReadStatusReg (TPIDR_EL0 ); __asm { BR X9 } return result ; } 汇编代码如下
; __unwind { STP X29 , X30, [SP ,#-0x60+var_s0]! STP X28 , X27, [SP ,#var_s10] STP X26 , X25, [SP ,#var_s20] STP X24 , X23, [SP ,#var_s30] STP X22 , X21, [SP ,#var_s40] STP X20 , X19, [SP ,#var_s50] MOV X29 , SP SUB SP , SP , #0x150 //栈初始化 MOV X19 , SP MRS X11 , TPIDR_EL0STR X11 , [X19,#0x150+var_140] LDR X11 , [X11,#0x28] //本地TLS SUB X8 , X29, #-var_38 ADRP X23 , #off_7C6E10@PAGE MOV X24 , #0xFFFFFFFFFFFFBE78 STUR X11 , [X29,#var_10] STP X8 , X8, [X19,#0x150+var_138] LDR X11 , [X19,#0x150+var_138] LDR X11 , [X23,#off_7C6E10@PAGEOFF] LDR X12 , [X0]MOV W9 , #0x3708 ADRP X10 , #sub_1B056C@PAGE MOVK X24 , #0xFC69,LSL#16 MOVK W9 , #0x31E,LSL#16 ADD X10 , X10, #sub_1B056C@PAGEOFF ADRP X21 , #off_7C6DC0@PAGE ADD X8 , X10, X9ADD X9 , X11, X24ADD X10 , X12, #0x30 ; '0' ADRP X25 , #0x7C6000 ADRP X27 , #0x7C6000 ADRP X22 , #0x7C6000 ADD X21 , X21, #off_7C6DC0@PAGEOFF ADRP X26 , #0x7C6000 ADRP X28 , #0x7C6000 STR X0 , [X19,#0x150+var_148] ADRP X20 , #0x7C6000 STR X10 , [X19,#0x150+var_128] BR X9 简化一下变成下面这样(最后可以理解成 goto *off_7C6E10+0xFFFFFFFFFC69BE78。)
ADRP X23 , #off_7C6E10@PAGE MOV X24 , #0xFFFFFFFFFFFFBE78 LDR X11 , [X23,#off_7C6E10@PAGEOFF] MOV W9 , #0x3708 MOVK X24 , #0xFC69,LSL#16 ADD X9 , X11, X24BR X9 但是查看 off_7C6E10 的值是 DCQ 0x3B1453C 发现对应的值和0xFFFFFFFFFC69BE78,相加溢出到0x1B03B4,接下来就该想想怎么恢复了,因此我们可以先看一下0x1B03B4的汇编代码,实现如下,我们暂且将其命名为Block1。
sub_1B03B4 MOV X9 , #0xFFFFFFFFFCE1C8F8 ADD X8 , X8, X9BR X8 我们可以看到,它使用X8作为下一跳的目标值,而X8的部分值是由于之前的函数JNI_OnLoad决定,接下来发现X8跳转的值是0x1B056C,汇编代码如下,因与解密逻辑相关,此处命名为Block 2。
LDR X8 , [X19,#0x28] MOV W2 , #0x10006 LDR X8 , [X8]LDR X1 , [X19,#0x20] LDR X0 , [X19,#8] BLR X8 MOV W8 , #0x7B64 ADRP X10 , #sub_1AFABC@PAGE LDR X9 , [X22,#0xDD8] MOVK W8 , #0x702,LSL#16 ADD X10 , X10, #sub_1AFABC@PAGEOFF ADD X8 , X10, X8MOV W11 , #0xE62C SUB W10 , W10, W8MOVK W11 , #0x30E,LSL#16 ADD W10 , W10, W11ADD X9 , X9, W10,SXTWSTR W0 , [X19,#0x30] BR X9 其中,W10的值依赖于初始值W10_final = W10_old - 0x040E90F4,另外地,X9的值是X22(前为0x7c6000)加上偏移,即7c6dd8。
下一跳是0x1afb88,这里的最终跳转地址则依赖于最开始X25的赋值(Dispatched table)和0xFFFFFFFFFC894D84,此处命名为Block3。
sub_1AFB88 LDR X9 , [X25,#0xDD0] MOV X11 , #0xFFFFFFFFFFFF4D84 MOV X10 , #0xFFFFFFFFFFFF3B28 MOVK X11 , #0xFC89,LSL#16 MOVK X10 , #0xFCCA,LSL#16 ADD X9 , X9, X11ADD X8 , X8, X10BR X9 下一跳又是X8相关,在上一条赋值,这一个块和Block1很像,不是吗?
sub_1AF9C4 MOV X9 , #0xFFFFFFFFFC334974 ADD X8 , X8, X9BR X8 譬如下一块,就很像Block2。因此,在libtiny.so文件中,因为官方将一个函数拆分为非常多个小的Block,而每个Block通过在Dispatch table读取并且通过加减运算进行混淆进行间接跳转,进而迷惑反编译器隐藏自身逻辑,我们可以总结出这样一个跳转结构:
真实业务块(Business Block) :
执行实际的算法或 JNI 调用(例如 sub_1B056C 中的 BLR X8)。执行完毕后,混淆器在块尾通过特定的数学公式更新状态寄存器,并产生下一个目的地的计算线索。
纯计算跳板块(Block 1 - Calculation Trampoline) :
特征 :体积极小,不包含业务逻辑。
职责 :专门用于承接上一块传过来的基准寄存器(如 X8),对其加上一个 64 位的大常量硬编码偏移,随即立刻通过 BR 间接跳转。
状态计算主控块(Block 2 - Dispatcher Logic) :
特征 :包含密集型常量拼接( MOV+ MOVK)与复杂的“加$\rightarrow$减$\rightarrow$加”数学混淆链。
职责 :负责从全局分发虚表(Dispatch Table)中读取基础指针,并配合前序寄存器或状态变量(如 [X19, #0x37] 的条件判断),计算出长距离跨越的下一跳绝对目标。
状态中转过渡块(Block 3 - State Transition) :
特征 :为后续的 Block 1 提前铺路。
职责 :同时拉起多个大常量的拼接,分别通过不同的寄存器(如 X11 计算 X9 的基址, X10 计算 X8 的基址),确保跳转到下一个 Block 1 后,Block 1 拥有正确的输入寄存器值。
当然,上述规则并不固定,在实际分析中这三种Block大体遵循这个规律,而且每个Block的汇编代码不尽相同。
0X2 间接跳转 在用unicorn跑了之后发现普遍存在下面两种状况:
1、在某些Block里面跳转地址依赖于调用者(caller Bolck)的某个寄存器,因此可能同一个Block在运行时会跳转到不同的地址。
2、同一个Block可能是多个Block跳转的地址。
也就是说,Block和Block之前并不是一一对应的,那怎么办呢?
我的想法是,首先先创建一个segment,然后把每个业务函数A识别出来,然后通过unidbg依次执行函数,把所有机器码都写进新segment对应的函数B去创建一个完整的函数。
如何优化?
经过观察,类似于如下的Block2,一般稳定在3-4条指令的大小的基本块,且绝对不包含业务代码,实际观察中,通常后面的基本块会对这里面相关寄存器重新赋值,可以直接去掉。
因此,上面的部分的汇编代码我们可以直接忽略不记录,我们不妨把剩余的执行的机器码DUMP到一个bin文件里。但是直接打开bin文件肯定是不可行的,因为ARM64很多指令都是偏移寻址(如ADRP、B、BL、B.cond),IDA识别会出错,如下图所示。
我们可以增加一个Relocation的函数,帮助我们处理代码内部的偏移寻址问题。
private byte [] relocateInstructionARM64 (byte [] origBytes , long origPC , long newPC , Backend backend , Map < Long , Long > blrMap ) { int ins = ((origBytes [3 ] & 0xFF ) << 24 ) | ((origBytes [2 ] & 0xFF ) << 16 ) | ((origBytes [1 ] & 0xFF ) << 8 ) | (origBytes [0 ] & 0xFF ); long moduleBase = module .base ; if ((ins & 0x9F000000 ) == 0x90000000 ) { int immlo = (ins >> 29 ) & 0x3 ; int immhi = (ins >> 5 ) & 0x7FFFF ; int imm = (immhi << 2 ) | immlo ; if ((imm & 0x100000 ) != 0 ) imm |= 0xFFF00000 ; long origPage = origPC & ~0xFFFL ; long targetPageAbsolute = origPage + ((long ) imm << 12 ); long idaTargetPage = targetPageAbsolute - (moduleBase & ~0xFFFL ); long newPage = newPC & ~0xFFFL ; long newImm = (idaTargetPage - newPage ) >> 12 ; ins &= 0x9F00001F ; ins |= ((newImm & 0x3 ) << 29 ); ins |= (((newImm >> 2 ) & 0x7FFFF ) << 5 ); } else if ((ins & 0x7C000000 ) == 0x14000000 ) { int imm26 = ins & 0x3FFFFFF ; if ((imm26 & 0x2000000 ) != 0 ) { imm26 |= 0xFC000000 ; } long targetAddrAbsolute = origPC + ((long ) imm26 << 2 ); long idaTargetAddr = targetAddrAbsolute - moduleBase ; long newImm26 = (idaTargetAddr - newPC ) >> 2 ; ins &= 0xFC000000 ; ins |= (newImm26 & 0x3FFFFFF ); } else if ((ins & 0xFFFFFC1F ) == 0xD63F0000 ) { if (blrMap .containsKey (origPC )) { long targetAbsAddr = blrMap .get (origPC ); if (targetAbsAddr >= moduleBase && targetAbsAddr < (moduleBase + module .size )) { long idaTargetAddr = targetAbsAddr - moduleBase ; long newImm26 = (idaTargetAddr - newPC ) >> 2 ; ins = 0x94000000 | ((int ) newImm26 & 0x3FFFFFF ); } } } else if ((ins & 0xFF00001F ) == 0x54000000 ) { int imm19 = (ins >> 5 ) & 0x7FFFF ; if ((imm19 & 0x40000 ) != 0 ) imm19 |= 0xFFF80000 ; long targetOffset = (origPC + ((long ) imm19 << 2 )) - moduleBase ; long newTargetAbsolute = TARGET_NEW_BASE + targetOffset ; long newImm19 = (newTargetAbsolute - newPC ) >> 2 ; ins &= 0xFF00001F ; ins |= ((newImm19 & 0x7FFFF ) << 5 ); } else if ((ins & 0x9F000000 ) == 0x30000000 ) { int immlo = (ins >> 29 ) & 0x3 ; int immhi = (ins >> 5 ) & 0x7FFFF ; int imm = (immhi << 2 ) | immlo ; if ((imm & 0x100000 ) != 0 ) imm |= 0xFFE00000 ; long targetAddrAbsolute = origPC + imm ; long idaTargetAddr = targetAddrAbsolute - moduleBase ; long newImm = idaTargetAddr - newPC ; ins &= 0x9F00001F ; ins |= ((newImm & 0x3 ) << 29 ); ins |= (((newImm >> 2 ) & 0x7FFFF ) << 5 ); } byte [] res = new byte [4 ]; res [0 ] = (byte ) (ins & 0xFF ); res [1 ] = (byte ) ((ins >> 8 ) & 0xFF ); res [2 ] = (byte ) ((ins >> 16 ) & 0xFF ); res [3 ] = (byte ) ((ins >> 24 ) & 0xFF ); return res ; } 然后在Relocation函数中指定目标加载地址,得到目标bin文件后可以通过idapython将bin加载到目标地址处。
import os import idc import idaapi import ida_segment import ida_funcs def load_bin_with_new_segment (bin_path , target_address , seg_name =".text.unidbg" ): if not os .path .exists (bin_path ): print (f"[-] 找不到 bin 文件,请检查路径: {bin_path}" ) return False with open (bin_path , "rb" ) as f : bin_data = f .read () size = len (bin_data ) end_pc = target_address + size print (f"[+] 正在读取文件: {bin_path} (大小: {size} 字节)" ) existing_seg = ida_segment .getseg (target_address ) if not existing_seg : print (f"[+] 地址 0x{target_address:X} 为空闲区,开始为 IDA 9.2 创建全新 64 位代码段..." ) success = ida_segment .add_segm (0 , target_address , end_pc , seg_name , "CODE" , 0 ) if not success : print (f"[-] 在 0x{target_address:X} 创建新段失败!" ) return False new_seg = ida_segment .getseg (target_address ) new_seg .perm = 5 # READ | EXEC new_seg .bitness = 2 # 64-bit new_seg .update () print (f"[+] 成功创建 64 位代码段 [{seg_name}] (0x{target_address:X} - 0x{end_pc:X})" ) else : if existing_seg .end_ea < end_pc : ida_segment .set_segm_end (target_address , end_pc , 0 ) existing_seg .perm = 5 existing_seg .bitness = 2 idc .set_segm_class (target_address , "CODE" ) existing_seg .update () idc .del_items (target_address , 0 , size ) for offset , byte_value in enumerate (bin_data ): idc .patch_byte (target_address + offset , byte_value ) current_pc = target_address success_count = 0 while current_pc < end_pc : if idc .create_insn (current_pc ): success_count += 1 current_pc += 4 p_func = ida_funcs .get_func (target_address ) if p_func : ida_funcs .del_func (target_address ) success_func = ida_funcs .add_func (target_address , end_pc ) if success_func : print (f"[★] success" ) else : print (f"[!] error" ) idc .add_func (target_address , idc .BADADDR ) idaapi .auto_wait () return True BIN_PATH = r"/Users/leido/traced_code.bin" TARGET_ADDR = 0x880000 load_bin_with_new_segment (BIN_PATH , TARGET_ADDR )处理后如下图。另外,这里可能因为函数过大无法反编译,可以修改cfg/hexrays.cfg相关配置即可。
最后精简一下,可以得到JNI onLoad的伪代码(IDA里面的什么_cxa_guard_acquire这里就先省略。),主要行为是覆盖并定义了 com/xingin/tiny/internal/t包中的@Keep public static native Object a(int i19, Object... objArr);为sub_182D8C。
__int64 JNI_onLoad (JavaVM * vm ){ JNIEnv * env ; vm -> GetEnv (vm , & env , JNI_VERSION_1_6 ); JavaVM * realVm ; env -> GetJavaVM (env , & realVm ); uint8_t * buf = (uint8_t * )(g_ctx -> base ); int len = g_ctx -> len ; for (int i = 0 ; i < len ; i ++ ) { uint8_t key = gen_key (i ); switch (i % 5 ) { case 0 : buf [i ] ^ = key ; break ; case 1 : buf [i ] ^ = ~key ; break ; case 2 : buf [i ] -= key ; break ; case 3 : buf [i ] = ROL (buf [i ], (key ^ 7 )); break ; case 4 : buf [i ] = ROR (buf [i ], (key ^ 7 )); break ; } } void * clazz = env -> FindClass ("com/xingin/tiny/internal/t" ); JNINativeMethod methods [] = { { "a" , "(I[Ljava/lang/Object;)Ljava/lang/Object;" , (void * )sub_182D8C } }; env -> RegisterNatives (clazz , methods , 1 ); if ( * (char ** )(MEMORY [0xFFFFFFFFFC94662C ] + 40LL ) != v71 ) stack_check_fail (); return JNI_VERSION_1_6 ; } 0X3 NATIVE函数 接下来我们尝试dump一下sub_182D8C,在尝试dump下来执行的机器码在反编译后,并没有发现什么相关的逻辑,如下图
但是在unidbg执行的汇编代码部分看到了这些东西,有cmp但是后面居然没有直接跟着bne指令?伪代码居然也没有?
[Trace] PC: 0x40182DB8 (相对偏移: 0x182DB8) | 62 A6 00 B9 | str w2, [x19, #0xa4] [*] 触发内存读写指令 [str w2, [x19, #0xa4]],当前寄存器状态如下: SP = 0xbfffbb00 FP = 0x40182d00 LR = 0x40182d00 PC = 0x40182db8X0 = 0xfffe1640 X1 = 0xffffffff98adad7e X2 = 0xfffffffff967df14 X3 = 0x564718df X4 = 0x0 X5 = 0x1 X6 = 0x0 X7 = 0x0 X8 = 0x0 X9 = 0x0 X10 = 0xfca0cb94 X11 = 0x9e85d8 X12 = 0x41ae3dc X13 = 0x407c6000 X14 = 0x401b1ff8 X15 = 0x407c6c20 X16 = 0x4083e9a8 X17 = 0x0 X18 = 0x40b82060 X19 = 0xbfffbb00 .... [Trace] PC: 0x401ACFF8 (相对偏移: 0x1ACFF8) | 68 A6 40 B9 | ldr w8, [x19, #0xa4] [*] 触发内存读写指令 [ldr w8, [x19, #0xa4]],当前寄存器状态如下: SP = 0xbfff9520 FP = 0x401acf00 LR = 0x401acf00 PC = 0x401acff8X0 = 0x43a85cec X1 = 0x43666c7c X2 = 0x440d0ce4 X3 = 0x43a85cec X4 = 0x43a85cec X5 = 0x43a85cec X6 = 0x401ab85c X7 = 0x43a85cec X8 = 0x401acff8 X9 = 0x43cafe5c X10 = 0xfffffffffc065dbc X11 = 0xfffffffffc7482a4 X12 = 0x3ec8a98 X13 = 0x43a85cec X14 = 0x43705a9c X15 = 0x43a85cec X16 = 0x43a85cec X17 = 0x43a85cec X18 = 0x40b82060 X19 = 0xbfffbb00 -------------------------------------------------------------------------------- [Trace] PC: 0x401ACFFC (相对偏移: 0x1ACFFC) | 29 47 88 52 | movz w9, #0x4239 [Trace] PC: 0x401AD000 (相对偏移: 0x1AD000) | E9 25 A2 72 | movk w9, #0x112f, lsl #16 [Trace] PC: 0x401AD004 (相对偏移: 0x1AD004) | 2A FF FF 90 | adrp x10, #0x40191000 [Trace] PC: 0x401AD008 (相对偏移: 0x1AD008) | 1F 01 09 6B | cmp w8, w9 [Trace] PC: 0x401AD00C (相对偏移: 0x1AD00C) | C8 30 00 D0 | adrp x8, #0x407c7000 [Trace] PC: 0x401AD010 (相对偏移: 0x1AD010) | 89 E2 9D 52 | movz w9, #0xef14 [Trace] PC: 0x401AD014 (相对偏移: 0x1AD014) | 08 B1 41 F9 | ldr x8, [x8, #0x360] ,我们知道函数传入参数对应如下,因此判断是我们传入的i19不同值会导致不同的执行结果,从而产生不一样的函数运行结果。
Java参数 JNI参数 寄存器 -- JNIEnv X0 this/class jclass X1 i19 jint X2 objArr jobjectArray X3
通过搜索,发现i19的可能值为0xc9d3562d、0xe83bce36、0xf967df14、0xffdec8d9、0x112f4239,这五种。
那是如何隐藏导致伪代码也看不出判断-跳转逻辑的呢?
关键机制是 cmp → cset → strb → ldrb → csel → br 链条:
0x401AD008: cmp w8, w9 # 比较 i19 vs 0x112f4239, 设置 NZCV flags 0x401AD03C: cset w11, lt # 将 PSTATE.LT 具体化为寄存器值 0x401AD044: strb w11, [x8] # 存储标志到栈上 flag_buffer[0] = w11 ... (跳过了很多间接跳转混淆块) ... 0x401911E4: ldrb w8, [x15] # 重新读取 flag_buffer[0] 0x401911F4: cmp w8, #0 # 检查标志是否为0 ... 0x40191228: csel x10, x10, x9, ne # 如果 w8≠0 选 x10,否则选 x9 (下一跳目标) 0x40191268: br x9 # 跳转到选中的目标 本质上: 混淆器把 if (cond) goto A else goto B 拆成了两个阶段:
先用 cset 把条件结果物化成一个布尔变量存起来
后用 csel 根据这个布尔变量从两个预计算地址中选一个跳转
接下来我们可以执行并且dump一下不同分支执行的伪代码, 非native中定义分支(以下面两个调用处为例) public static boolean d () { PatchProxyResult patchProxyResultProxy0Para = PatchProxy .proxy0Para (null , g3 .class , 262844 ); if (patchProxyResultProxy0Para .isSupported ) { return ((Boolean ) patchProxyResultProxy0Para .result ).booleanValue (); } Boolean bool = (Boolean ) t .a (1313319915 , new Object [0 ]); return bool != null && bool .booleanValue (); } public static void c () { if (PatchProxy .proxyVoid0Para (null , g3 .class , 262836 )) { return ; } t .a (682275832 , new Object [0 ]); } 代码
private void callSub182D8Cinit () { long sub_182D8C_Addr = 0x182D8CL ; Pointer jniEnv = vm .getJNIEnv (); DvmClass dvmClass = vm .resolveClass ("com/xingin/tiny/internal/t" ); // 1. 获取正确的 class 句柄 int classLocalHandle = vm .addLocalObject (dvmClass ); int argInt = 682275832 ;//0x4E464FEB; ArrayObject arrayObject = new ArrayObject (); int arrayLocalHandle = vm .addLocalObject (arrayObject ); List < Object > list = new ArrayList <> (); list .add (jniEnv ); // X0 list .add (classLocalHandle ); // X1 list .add (argInt ); // X2 list .add (arrayLocalHandle ); // X3 System .out .println ("[+] 开始步入 Native 函数..." ); Number numbers = module .callFunction (emulator , sub_182D8C_Addr , list .toArray ()); System .out .println ("[+] sub_182D8C 调用完成。返回值为: " + numbers .intValue ()); } native对应执行的的伪代码(无主要逻辑,因为不是指定的5个int)
__int64 __fastcall sub_880000 (_JNIEnv * a1 , __int64 a2 , int a3 , void * a4 ){ v580 = a4 ; v581 = a1 ; v579 = a3 ; StatusReg = _ReadStatusReg (TPIDR_EL0 ); v1149 = * (_QWORD * )(StatusReg + 40 ); if ( * (_QWORD * )(StatusReg + 40 ) != v1149 ) { v1142 = v580 ; v1143 = v581 ; v581 -> functions -> GetArrayLength (& v581 -> functions , v580 ); v1144 = v620 ; v1143 -> functions -> GetObjectArrayElement (& v581 -> functions , v580 , 0 ); v1146 = v620 ; * v620 = v1145 ; v1147 = v621 ; return (__int64 )v1143 -> functions -> GetObjectArrayElement (& v581 -> functions , v580 , 1 ); } return result ; } 分支:C9D3562D,以下面这个调用处为例进行调用。 public static void a (long j42 ) { if (PatchProxy .proxyVoid1Para (new Long (j42 ), null , g3 .class , 262834 )) { return ; } t .a (- 908896723 , Long .valueOf (j42 )); } 补环境
private void callSub182D8C9D3562D () { long sub_182D8C_Addr = 0x182D8CL ; Pointer jniEnv = vm .getJNIEnv (); DvmClass dvmClass = vm .resolveClass ("com/xingin/tiny/internal/t" ); // 1. 获取正确的 class 句柄 int classLocalHandle = vm .addLocalObject (dvmClass ); int argInt = 0xC9D3562D ;//0x4E464FEB; long uptimeValue = System .currentTimeMillis (); DvmObject <?> [] innerArray = new DvmObject <?> [1 ]; innerArray [0 ] = vm .resolveClass ("java/lang/Long" ).newObject (uptimeValue ); ArrayObject arrayObject = new ArrayObject (innerArray ); int arrayLocalHandle = vm .addLocalObject (arrayObject ); List < Object > list = new ArrayList <> (); list .add (jniEnv ); // X0 list .add (classLocalHandle ); // X1 list .add (argInt ); // X2 list .add (arrayLocalHandle ); // X3 System .out .println ("[+] 开始步入 Native 函数..." ); Number numbers = module .callFunction (emulator , sub_182D8C_Addr , list .toArray ()); System .out .println ("[+] sub_182D8C 调用完成。返回值为: " + numbers .intValue ()); } @Override public long callLongMethodV (BaseVM vm , DvmObject <?> dvmObject , String signature , VaList vaList ) { switch (signature ) { case "java/lang/Long->longValue()J" : return ((Number ) dvmObject .getValue ()).longValue (); case "java/lang/Integer->intValue()I" : return ((Number ) dvmObject .getValue ()).intValue (); } return super .callLongMethodV (vm , dvmObject , signature , vaList ); } native对应执行的的伪代码(这是一个 JNI 方法缓存初始化器,为后续密集的 JNI 调用(如动态解释执行、反射调用等)预先建立全局缓存表,同时从参数中提取操作码供后续分发。)
_BYTE * __fastcall sub_880000 (__int64 a1 , __int64 a2 , int a3 , __int64 a4 ){ v4 = & v597 ; v618 = a4 ; v619 = a1 ; v617 = a3 ; StatusReg = _ReadStatusReg (TPIDR_EL0 ); v1180 = * (StatusReg + 40 ); v6 = a1 ; v7 = v619 -> functions -> FindClass (& v619 -> functions , "java/lang/Object" ); qword_84F490 = v6 -> functions -> NewGlobalRef (& v6 -> functions , v7 ); qword_84F498 = v6 -> functions -> GetMethodID (& v6 -> functions , v7 , "toString" , "()Ljava/lang/String;" ); qword_84F4A0 = v6 -> functions -> GetMethodID (& v6 -> functions , v7 , "hashCode" , "()I" ); qword_84F4A8 = v6 -> functions -> GetMethodID (& v6 -> functions , v7 , "clone" , "()Ljava/lang/Object;" ); qword_84F4B0 = v6 -> functions -> GetMethodID (& v6 -> functions , v7 , "equals" , "(Ljava/lang/Object;)Z" ); v6 -> functions -> DeleteLocalRef (& v6 -> functions , v7 ); v8 = v6 -> functions -> FindClass (& v6 -> functions , "java/lang/Integer" ); qword_84FD98 = v6 -> functions -> NewGlobalRef (& v6 -> functions , v8 ); qword_84FDA0 = v6 -> functions -> GetMethodID (& v6 -> functions , v8 , "intValue" , "()I" ); qword_84FDA8 = v6 -> functions -> GetStaticMethodID (& v6 -> functions , v8 , "valueOf" , "(I)Ljava/lang/Integer;" ); v6 -> functions -> DeleteLocalRef (& v6 -> functions , v8 ); v9 = v6 -> functions -> FindClass (& v6 -> functions , "java/lang/Long" ); qword_84FE70 = v6 -> functions -> NewGlobalRef (& v6 -> functions , v9 ); qword_84FE78 = v6 -> functions -> GetMethodID (& v6 -> functions , v9 , "longValue" , "()J" ); qword_84FE80 = v6 -> functions -> GetStaticMethodID (& v6 -> functions , v9 , "valueOf" , "(J)Ljava/lang/Long;" ); v6 -> functions -> DeleteLocalRef (& v6 -> functions , v9 ); v10 = v6 -> functions -> FindClass (& v6 -> functions , "java/lang/Float" ); qword_850038 = v6 -> functions -> NewGlobalRef (& v6 -> functions , v10 ); qword_850040 = v6 -> functions -> GetMethodID (& v6 -> functions , v10 , "floatValue" , "()F" ); qword_850048 = v6 -> functions -> GetStaticMethodID (& v6 -> functions , v10 , "valueOf" , "(F)Ljava/lang/Float;" ); v6 -> functions -> DeleteLocalRef (& v6 -> functions , v10 ); v11 = v6 -> functions -> FindClass (& v6 -> functions , "java/lang/Boolean" ); qword_8501D8 = v6 -> functions -> NewGlobalRef (& v6 -> functions , v11 ); qword_8501E0 = v6 -> functions -> GetMethodID (& v6 -> functions , v11 , "booleanValue" , "()Z" ); qword_8501E8 = v6 -> functions -> GetStaticMethodID (& v6 -> functions , v11 , "valueOf" , "(Z)Ljava/lang/Boolean;" ); v6 -> functions -> DeleteLocalRef (& v6 -> functions , v11 ); v12 = v6 -> functions -> FindClass (& v6 -> functions , "java/lang/Double" ); v13 = v6 -> functions ; v14 = v12 ; v15 = v6 ; while ( 1 ) { qword_850A70 = v13 -> NewGlobalRef (v15 , v14 ); qword_850A78 = v6 -> functions -> GetMethodID (& v6 -> functions , v14 , "doubleValue" , "()D" ); qword_850A80 = v6 -> functions -> GetStaticMethodID (& v6 -> functions , v14 , "valueOf" , "(D)Ljava/lang/Double;" ); v6 -> functions -> DeleteLocalRef (& v6 -> functions , v14 ); v16 = init_jni_static_method_cache (v6 ); v39 = v17 ; v40 = v5 ; v41 = v6 ; v42 = v4 ; v5 = _ReadStatusReg (TPIDR_EL0 ); v4 = v16 ; v6 = & byte_84F000 ; v38 = * (v5 + 40 ); v18 = atomic_load (& byte_84F378 ); if ( (v18 & 1 ) == 0 ) break ; v15 = _cxa_guard_acquire (& byte_84F378 ); if ( v15 ) { sub_1B286C (); v35 = v19 ; v36 = 0x84F000 ; v37 = v4 ; v20 = _ReadStatusReg (TPIDR_EL0 ); v34 = * (v20 + 40 ); v32 = xmmword_111368 ; v33 = xmmword_111378 ; * (v20 + 896 ) = alloc (27 ); _cxa_guard_release (& byte_84F378 ); JUMPOUT (0x5895FC ); } } (sub_2AF848 )(v18 ); v29 = v21 ; v30 = 0x84F000 ; v31 = v4 ; v22 = _ReadStatusReg (TPIDR_EL0 ); v28 = * (v22 + 40 ); v27 = 170 ; v23 = alloc (2 ); v23 [1 ] = 0 ; v24 = memcpy (v23 , & v27 , sizeof (_BYTE )); v25 = (sub_168214 )(v24 , 1 ); if ( * (v22 + 40 ) != v28 ) { qword_84F370 = v25 ; _cxa_guard_release (& byte_84F368 ); JUMPOUT (0x589644 ); } return v23 ; } 然而,可惜的是,由于IDA错误的把一些代码建立为函数,导致出现类似JUMPOUT(0x589644);这样的情况,结合unidbg的jni verbose输出来看,下面这些JNI执行的相关逻辑依旧没记录(在JUMPOUT(0x589644)/JUMPOUT(0x5895FC)中)
JNIEnv->FindClass(android/content/Context) was called from RX@0x405880bc[libtiny.so]0x5880bc JNIEnv->NewGlobalRef(class android/content/Context) was called from RX@0x405880d4[libtiny.so]0x5880d4 JNIEnv->GetMethodID(android/content/Context.getPackageManager()Landroid/content/pm/PackageManager;) => 0x3acc78f0 was called from RX@0x40588100[libtiny.so]0x588100 JNIEnv->GetMethodID(android/content/Context.getContentResolver()Landroid/content/ContentResolver;) => 0x3fe770ea was called from RX@0x4058812c[libtiny.so]0x58812c JNIEnv->GetMethodID(android/content/Context.getSystemService(Ljava/lang/String;)Ljava/lang/Object;) => 0x545bcf2d was called from RX@0x40588158[libtiny.so]0x588158 JNIEnv->GetMethodID(android/content/Context.getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;) => 0xa0a12d1f was called from RX@0x40588184[libtiny.so]0x588184 JNIEnv->GetMethodID(android/content/Context.checkPermission(Ljava/lang/String;II)I) => 0x9daa2205 was called from RX@0x405881b0[libtiny.so]0x5881b0 JNIEnv->GetMethodID(android/content/Context.getClassLoader()Ljava/lang/ClassLoader;) => 0x188ba130 was called from RX@0x405881dc[libtiny.so]0x5881dc JNIEnv->GetMethodID(android/content/Context.deleteSharedPreferences(Ljava/lang/String;)Z) => 0xf8814ae5 was called from RX@0x40588208[libtiny.so]0x588208 JNIEnv->GetMethodID(android/content/Context.getAssets()Landroid/content/res/AssetManager;) => 0x3b2d309d was called from RX@0x4058825c[libtiny.so]0x58825c JNIEnv->GetMethodID(android/content/Context.bindService(Landroid/content/Intent;Landroid/content/ServiceConnection;I)Z) => 0x1bb87487 was called from RX@0x40588288[libtiny.so]0x588288 JNIEnv->GetMethodID(android/content/Context.unbindService(Landroid/content/ServiceConnection;)V) => 0xa9850764 was called from RX@0x405882b4[libtiny.so]0x5882b4 JNIEnv->GetObjectArrayElement([java.lang.Long@64c87930], 0) => java.lang.Long@64c87930 was called from RX@0x4018f7d0[libtiny.so]0x18f7d0 JNIEnv->CallLongMethodV(java.lang.Long@64c87930, longValue() => 0x19f1cc820c9L) was called from RX@0x40597990[libtiny.so]0x597990 [+] sub_182D8C 调用完成。返回值为: 0 然后踩了一些坑发现native函数是一个多路复用的分发函数,不同的 i19 值走不同的分支,需要先传入一些值做初始化,好像还有什么反射调用获取java.lang.reflect.Method的事,看到了 021K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6*7K9r3E0D9x3o6t1J5z5q4)9J5c8Y4g2F1K9h3c8T1k6#2)9J5c8X3W2K6M7%4g2W2M7#2)9J5c8U0j5&6z5b7`.`. 这个讨论和 https://bbs.kanxue.com/thread-289620.htm 这个补环境的帖子,感觉好牛逼,但是帖子里的版本8590914和我目前的版本差别有点大,i19的值也完全不一样,所以就只分析一下初始化的分支吧,毕竟只是个新手。
0X4 写在最后 本来这篇文章写到 0x3 就已经差不多了,但想了想还是留个尾巴,算是对自己这段时间折腾 libtiny 的一个小结。
回过头看这条路,目前做到的这个程度,离真正"逆向透" libtiny 还有不小的距离。五个分支只分析了一个,自定义 VM 也只是瞄了个大概,更别说那些密密麻麻的 JNI 环境调用怎么在 unidbg 里一个一个补上。这东西的体量和复杂度,确实不是两周的业余时间能啃完的,感觉要真完整无误的把间接跳转给patch掉得先过反调试用frida来hook,嗯,就先这样吧。
[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!
最后于 2026-7-7 21:12
被ldz666编辑
,原因: