首页
社区
课程
招聘
[分享]unidbg还原Libtiny.so间接跳转
发表于: 2天前 965

[分享]unidbg还原Libtiny.so间接跳转

2天前
965

0X00 内容概述

这篇文章也同步发布在我的公众号上:

75bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7q4)9J5k6i4N6W2K9i4S2A6L8W2)9J5k6i4q4I4i4K6u0W2j5$3!0E0i4K6u0r3M7#2)9J5c8W2m8Z5f1o6S2x3L8g2)9#2k6W2q4W2c8f1!0g2j5%4y4J5N6i4N6Y4z5g2q4a6N6#2)9K6c8Y4m8G2j5#2)9#2k6Y4c8G2K9$3g2F1i4K6y4p5d9p5R3J5y4q4u0i4M7h3A6E0f1f1W2s2L8p5&6x3d9#2g2y4d9W2g2x3j5i4y4v1y4#2W2d9K9s2k6b7k6h3g2Y4M7K6c8r3x3q4S2I4f1R3`.`.

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_EL0
STR             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, X9
ADD             X9, X11, X24
ADD             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, X24
BR              X9

但是查看 off_7C6E10 的值是 DCQ 0x3B1453C 发现对应的值和0xFFFFFFFFFC69BE78,相加溢出到0x1B03B4,接下来就该想想怎么恢复了,因此我们可以先看一下0x1B03B4的汇编代码,实现如下,我们暂且将其命名为Block1。

sub_1B03B4
MOV             X9, #0xFFFFFFFFFCE1C8F8
ADD             X8, X8, X9
BR              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, X8
MOV         W11, #0xE62C
SUB         W10, W10, W8
MOVK        W11, #0x30E,LSL#16
ADD         W10, W10, W11
ADD         X9, X9, W10,SXTW
STR         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, X11
ADD             X8, X8, X10
BR              X9

下一跳又是X8相关,在上一条赋值,这一个块和Block1很像,不是吗?

sub_1AF9C4
MOV             X9, #0xFFFFFFFFFC334974
ADD             X8, X8, X9
BR              X8

譬如下一块,就很像Block2。因此,在libtiny.so文件中,因为官方将一个函数拆分为非常多个小的Block,而每个Block通过在Dispatch table读取并且通过加减运算进行混淆进行间接跳转,进而迷惑反编译器隐藏自身逻辑,我们可以总结出这样一个跳转结构:

  1. 真实业务块(Business Block)

    执行实际的算法或 JNI 调用(例如 sub_1B056C 中的 BLR X8)。执行完毕后,混淆器在块尾通过特定的数学公式更新状态寄存器,并产生下一个目的地的计算线索。

  2. 纯计算跳板块(Block 1 - Calculation Trampoline)

    特征:体积极小,不包含业务逻辑。

    职责:专门用于承接上一块传过来的基准寄存器(如 X8),对其加上一个 64 位的大常量硬编码偏移,随即立刻通过 BR 间接跳转。

  3. 状态计算主控块(Block 2 - Dispatcher Logic)

    特征:包含密集型常量拼接(MOV+MOVK)与复杂的“加$\rightarrow$减$\rightarrow$加”数学混淆链。

    职责:负责从全局分发虚表(Dispatch Table)中读取基础指针,并配合前序寄存器或状态变量(如 [X19, #0x37] 的条件判断),计算出长距离跨越的下一跳绝对目标。

  4. 状态中转过渡块(Block 3 - State Transition)

    特征:为后续的 Block 1 提前铺路。

    职责:同时拉起多个大常量的拼接,分别通过不同的寄存器(如 X11 计算 X9 的基址,X10 计算 X8 的基址),确保跳转到下一个 Block 1 后,Block 1 拥有正确的输入寄存器值。

mermaid-1782548465490

当然,上述规则并不固定,在实际分析中这三种Block大体遵循这个规律,而且每个Block的汇编代码不尽相同。


0X2 间接跳转

在用unicorn跑了之后发现普遍存在下面两种状况:

1、在某些Block里面跳转地址依赖于调用者(caller Bolck)的某个寄存器,因此可能同一个Block在运行时会跳转到不同的地址。

2、同一个Block可能是多个Block跳转的地址。

也就是说,Block和Block之前并不是一一对应的,那怎么办呢?

我的想法是,首先先创建一个segment,然后把每个业务函数A识别出来,然后通过unidbg依次执行函数,把所有机器码都写进新segment对应的函数B去创建一个完整的函数。

如何优化?

经过观察,类似于如下的Block2,一般稳定在3-4条指令的大小的基本块,且绝对不包含业务代码,实际观察中,通常后面的基本块会对这里面相关寄存器重新赋值,可以直接去掉。

image-20260629105304633

因此,上面的部分的汇编代码我们可以直接忽略不记录,我们不妨把剩余的执行的机器码DUMP到一个bin文件里。但是直接打开bin文件肯定是不可行的,因为ARM64很多指令都是偏移寻址(如ADRP、B、BL、B.cond),IDA识别会出错,如下图所示。

image-20260629141721732

我们可以增加一个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相关配置即可。

image-20260629174459738

最后精简一下,可以得到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下来执行的机器码在反编译后,并没有发现什么相关的逻辑,如下图

image-20260630151331488

但是在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=0x40182db8
X0 =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=0x401acff8
X0 =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参数寄存器
--JNIEnvX0
this/classjclassX1
i19jintX2
objArrjobjectArrayX3

通过搜索,发现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 拆成了两个阶段:

  1. 先用 cset 把条件结果物化成一个布尔变量存起来

  2. 后用 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的事,看到了c73K9s2c8@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,嗯,就先这样吧。




[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 2
打赏
分享
最新回复 (2)
雪    币: 2353
活跃值: (2645)
能力值: (RANK:140 )
在线值:
发帖
回帖
粉丝
2
有图片链接有问题,可以处理一下哈。谢谢
1天前
0
雪    币: 640
活跃值: (715)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
作者可以把文章调整到android那边看的人更多
17小时前
0
游客
登录 | 注册 方可回帖
返回