找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4191

积分

0

好友

545

主题
发表于 2 小时前 | 查看: 9| 回复: 0

前言

最近分析了一个 APK 加固样本,大致看了下,确认是某知名搜索引擎出品的加固方案。部分函数带有 DexVMP 保护,整体架构和开源项目 nmmp 有不少相似之处。可以参考以下文章辅助理解:

初步分析

定位检测 so

还是老套路:先 Hook linker 的 call_array,判断加载到哪个 so 时闪退。

终端输出显示进程终止信息及libbaiduprotect.so地址

看一下 Logcat,发现退出时会打印 XOX: state=545

红色系统日志显示状态码545

JNI_OnLoad 被加密了,第三个 init_array 函数中出现了对未解密函数的调用,由此推断解密的逻辑在前两个 init_array 函数里。

被加密的JNI_OnLoad函数反编译代码

调用未解密函数的汇编指令

.init_array1

第一个函数明显在做偏移计算,其中 sub_9F090 调用了 syscall mprotect,这是要修改内存中代码/数据的前提条件。

条件判断与反混淆后的代码片段

mprotect系统调用的反编译代码

sub_9F130 负责解密 data 段。它用 v2 作为起始地址、qword_D0010 作为长度,按顺序与 0x67, 0x69, 0xBD, 0xE7, 0x11, 0xD1, 0x54, 0x3C 做 XOR 运算。

数据段定义及长度变量

XOR解密循环逻辑

sub_9B7D4 对指定内存区域做 cache flush,让解密后的代码/数据生效(刷新 D-cache)。Hook 这个函数打印参数一和参数二,会触发两次。第一次的返回值:

cache flush函数反编译代码

两次调用返回的地址范围

0xad080 是 data 段的起始地址,0xb5898 就是结束地址。

rodata段数据定义

eh_frame_hdr段数据定义

init_array2

sub_9F370 插满了不透明谓词,把代码段的解密、修补、权限切换、cache flush 全部打散进状态机里,分析起来比较头疼。

不透明谓词混淆的流程图

case 9分支中的复杂循环结构

so 脱壳

还原成完整算法确实麻烦。不过既然手里已经有数据段和代码段的起始地址与长度,那就在 sub_9F370 执行完后直接把这两段 dump 下来。

Interceptor.attach(libbaiduprotect.add(0x9F370), {
    onLeave(ret) {
        var libxx = Process.findModuleByName("libbaiduprotect.so");
        var file_path = "/data/data/xxx/data.bin";
        console.log(file_path);
        var file_handle = new File(file_path, "wb");
        if (file_handle && file_handle != null) {
            Memory.protect(ptr(libxx.base.add(0xad080)), 0x8818, 'rwx');
            var libso_buffer = ptr(libxx.base.add(0xad080)).readByteArray(0x8818);
            file_handle.write(libso_buffer);
            file_handle.flush();
            file_handle.close();
            console.log("[dump]:", file_path);
        }
    }
})

把 dump 下来的数据在 010 Editor 里找到对应偏移并覆盖,以下是修改前后对比。

修复前高亮的异常十六进制数据

修复后正常的十六进制数据

解密字符串

修复完的 so 不用 sofix 也能直接打开(如果 dump 整个 so 再 sofix,容易出现写内存地址、导入函数识别错误的情况)。JNI_OnLoad 此时已经能正常查看了。

JNI_OnLoad函数栈帧初始化代码

字符串表里突然多了一大堆十六进制字符串。

十六进制格式的加密字符串列表

随便挑一个字符串的引用跟进去,发现都被统一当做参数一传递给了某个解密函数。

多个字符串常量赋值给函数参数的代码

解密算法很简单:malloc 申请对应大小的内存,拿 byte_AE4B9 当 key,循环按位异或解密出原始字符串。问题是,字符串和密钥的组合非常多,手动处理不现实,得用 IDAPython 来匹配并批量模拟解密。

字符串解密函数的循环体实现

粗略扫一遍,发现所有解密算法都一样,唯独 key 不同,对应的汇编指令也完全一致。那就可以取下面这组特征指令,在 .text 段从头到尾做模式匹配。

AND             W16, W9, #0xFF
LDURB           W17, [X8,#-1]
CMP             W16, #0x3A ; ':'
CSEL            W16, WZR, W10, CC
CMP             W17, #0x3A ; ':'
CSEL            W18, W12, W11, CC
CMP             W14, #8
CSEL            W14, WZR, W14, EQ

整体逻辑是:  

  1. 匹配包含算法特征的函数  
  2. 在函数内匹配 key 地址  
  3. 查找当前函数的交叉引用  
  4. 匹配待解密字符串的地址  

这样就能批量解密出大部分字符串。

import idaapi
from idaapi import *
import idautils
import idc
from idc import *
from capstone import *
from keystone import *
import json

TARGET_SEQUENCE = [
    "AND",
    "LDURB",
    "CMP",
    "CSEL",
    "CMP",
    "CSEL",
    "CMP",
    "CSEL",
]

def decrypt(hex_str: str, key):
    out = bytearray()

    for i in range(len(hex_str) // 2):
        b = int(hex_str[i * 2:i * 2 + 2], 16)
        key_byte = ida_bytes.get_byte(key + (i % 8))
        out.append(b ^ key_byte)

    return out.split(b'\x00')[0].decode(errors="ignore")

def find_x13_define(ea):
    cur = ea
    while cur != idaapi.BADADDR:
        mnem = idc.print_insn_mnem(cur)
        if mnem in ["ADR", "ADRL", "ADRP"]:
            if idc.print_operand(cur, 0) == "X13":
                return idc.get_operand_value(cur, 1)
        cur = idc.prev_head(cur)
    return None

def get_x0_string(addr):
    """
    从某条指令向上回溯 ADRL X0
    """
    cur = addr
    for _ in range(20):  # 防止死循环
        cur = idc.prev_head(cur)
        if cur == idc.BADADDR:
            break
        mnem = idc.print_insn_mnem(cur)
        # ARM64 常见:ADRP + ADD / ADRL
        if mnem in ["ADRP", "ADRL", "ADR"]:
            op0 = idc.print_operand(cur, 0)
            # 确保是 X0
            if "X0" in op0:
                op1 = idc.print_operand(cur, 1)
                # 直接取字符串引用
                str_ea = idc.get_operand_value(cur, 1)
                if str_ea != idc.BADADDR:
                    return idc.get_strlit_contents(str_ea, -1, idc.STRTYPE_C)
    return None

def find_fun_cross_list(key_addr, ea):
    for xref in idautils.XrefsTo(ea):
        call_ea = xref.frm
        print("\nCALL at:", hex(call_ea))
        s = get_x0_string(call_ea)
        if s:
            try:
                print("X0 string: %s key地址:0x%x" % (s.decode(), key_addr))
                dec = decrypt(
                    s.decode(),
                    key_addr
                )
                print(dec)
            except:
                print("X0 raw:", s)
        else:
            print("no X0 string found")

def find_target_sequence(start_addr, end_addr):
    current_addr = start_addr
    sequence_index = 0
    locations = []

    while current_addr < end_addr:

        if not idc.is_code(idc.get_full_flags(current_addr)):
            current_addr += 4
            continue

        mnem = idc.print_insn_mnem(current_addr)

        if mnem == TARGET_SEQUENCE[sequence_index]:
            if sequence_index == 0:
                start_hit = current_addr

            sequence_index += 1

            if sequence_index == len(TARGET_SEQUENCE):
                locations.append(start_hit)
                print("[MATCH]", hex(idaapi.get_func(start_hit).start_ea))
                key_addr = find_x13_define(current_addr)
                find_fun_cross_list(key_addr, idaapi.get_func(start_hit).start_ea)
                sequence_index = 0

        else:
            sequence_index = 0

        current_addr += 4

    return locations

seg = idaapi.get_segm_by_name(".text")
start_address = seg.start_ea
end_address = seg.end_ea

results = find_target_sequence(start_address, end_address)

print("total:", len(results))

匹配到的字符串解密结果输出

Frida 检测

开头提到的 Logcat 打印 XOX: state=545,在字符串表里搜 state=,随便跟到一个引用位置,逻辑清晰得很,甚至直接打印了对应数字。那直接搜常数 545 就行。

state等于545的检测代码分支

有三处打印了 545,都在同一个函数里。

三处MOV W0, #0x221的检测点

sub_30310 里面全是检测 Frida 的特征。用 Interceptor.replace 直接替换这个函数后,应用不闪退了。

检测gum-js-loop线程的代码

检测linjector文件描述符的代码

去不透明谓词混淆

分析 JNI_OnLoad 时,发现大量不透明谓词混淆。根据混淆设定,这些判断条件永远不执行——因为 dword_D7ED0 所在段可读可写,IDA 无法确定它的值,所以伪代码把这些垃圾分支保留了下来,让分析者自己判断。解决思路很直接:要么把 dword_D7ED0 所在段改成只读不可写,要么手动 patch 成 0。

不透明谓词控制流的循环结构

import idaapi
import ida_name
import ida_bytes
import ida_segment
import ida_auto
import idautils
import idc

opaque_vars = [
    "dword_D7ECC",
    "dword_D7EC8",
    "dword_D7EDC",
    "dword_D7ED8",
]

touched_segments = set()

for name in opaque_vars:
    ea = ida_name.get_name_ea(idaapi.BADADDR, name)
    if ea == idc.BADADDR:
        print("[!] not found:", name)
        continue

    old = ida_bytes.get_dword(ea)
    ida_bytes.patch_dword(ea, 0)
    print("[+] %s %s: 0x%x -> 0" % (name, hex(ea), old))

    seg = ida_segment.getseg(ea)
    if seg:
        touched_segments.add(seg.start_ea)

for seg_ea in touched_segments:
    seg = ida_segment.getseg(seg_ea)
    if seg:
        print("[+] set read-only:", ida_segment.get_segm_name(seg), hex(seg.start_ea), hex(seg.end_ea))
        seg.perm = ida_segment.SEGPERM_READ
        ida_segment.update_segm(seg)

for f in idautils.Functions():
    end = idc.get_func_attr(f, idc.FUNCATTR_END)
    ida_auto.plan_and_wait(f, end)

print("[+] done, press F5 again")

Patch 完后让 IDA 按 F5 重新分析,JNI_Onload 立刻变得清清爽爽。

Patch后清晰简洁的JNI_OnLoad逻辑

Dump Dex

dump 大法

用yang 佬的 dump dex 脚本可以直接把所有 dex 拉出来(会夹带一些奇怪的东西)。

AI 还原 Dex 解密算法

详细分析过程就不展开了,太长了,感觉单独写一篇都足够。这里直接列出文件结构:

文件 用途
baiduprotect.md 存放 Dex 数量、解密 Key 等
baiduprotect1.d.jar 带 d.jar 的是 Dex VMP
baiduprotect1.jar 普通 DEX

这些文件开头都有共同的特征字节 14 94 B5 35

文件开头的十六进制数据

它并没有调用 Assets 打开文件,而是直接打开 /data/app/xxx/base.apk,然后按 ZIP 格式提取需要的条目。

打开APK并按ZIP格式解析的代码

提取出来的文件需要解密:把头部 0x100 字节(d.jar 是 0x200)还原成正确的 zlib 文件头。具体流程如下:

  • 用 HKDF-SHA256 派生 key/counter
  • 用 AES-128 加密 counter 生成 keystream
  • data XOR keystream
  • zlib 解压

解密/解压payload的代码片段

已知 key 是 0x10 个字节,固定存放在 baiduprotect.md。直接 dump g_payload_index_header,然后让 AI 生成对应的解密算法。

获取payload索引并关闭ZIP归档的代码

python .\decrypt_djar.py .\baiduprotect1.jar test.dex --ikm-hex 289313ddec23416b48695a7ee2c8b02d --crypt-len 0x100

GPT 一拳下去,裤子都给打掉,太强了。(解密代码见文末)

解密成功写入DEX文件的终端输出

DexVMP onCreate 函数

onCreate 原本的函数体变成了 AB.v 调用。不同函数,参数一的数值不同,估计就是用这个来区分不同的 VMP method。

被替换为AB.v调用的onCreate方法

大量以AB.-开头的JNI调用日志

AB.v 函数动态注册,指向 0x4e020

[RegisterNatives] java_class: com.sagittarius.v6.AB name: v sig: (ILjava/lang/Object;[Ljava/lang/Object;)V fnPtr: 0x78a6d27020 module_name: libbaiduprotect.so module_base: 0x78a6cd9000 offset: 0x4e020

Trace 一份函数调用日志

为了方便后续分析,先 trace 一份日志。推荐几个大佬写的 trace 项目:
https://github.com/zgy0x01/QTrace
https://github.com/lidongyooo/GumTrace (本文用的这个)

trace 过程可能会报错中断,需要修改 transform_callback,跳过报错指令,不然 trace 不下去。

trace 完才 13M,估计是原本函数代码也没多少。

SplashActivity_onCreate日志文件信息

0x4e020 的逻辑也很简单,就是个直接调用。这里因为堆栈原因,IDA 的参数识别有误——后面不应该是 0, 0, 0, 0,而应该是 i2 的低 16 位、objobjArr 等。

根据高16位选择VMP上下文的 dispatch_stub

g_vmp_context_by_id[256] 是已经初始化好的上下,数据来源是 baiduprotect1.d.jar。处理方式和普通 dex 一样:提取、解密头部 0x200 字节、zlib 解压,最后由 vmp_load_context_from_dex_and_register_bridge 进行解析。

加载并注册VMP上下文的逻辑

进来就看到解析 Dex 头部,解析出一堆 Dex 字段偏移地址。

创建DexView结构体解析DEX头

大概还原下返回的结构体,主要是 method_idsclass_defsfield_idsstring_ids,因为 ins 转换成 smali 需要这些信息。

typedef struct DexView
{
    OdexHeader *odex_header;     // +0x00, only set for dey/odex input
    DexHeader  *dex_header;      // +0x08, actual dex header
    void  *type_ids;             // +0x10 = dex + type_ids_off
    void  *proto_ids;            // +0x18 = dex + proto_ids_off
    void  *method_ids;           // +0x20 = dex + method_ids_off
    void  *class_defs;           // +0x28 = dex + class_defs_off
    void  *field_ids;            // +0x30 = dex + field_ids_off
    void  *data_section;         // +0x38 = dex + data_off
    void  *string_ids;           // +0x40 = dex + string_ids_off
    void  *odex_pklc_chunk;      // +0x48, payload of tag "PKLC"
    void  *odex_pamr_chunk;      // +0x50, payload of tag "PAMR"
    DexHeader *raw_dex_base;     // +0x58, same as dex_header after odex unwrap
    void  *reserved_60;          // +0x60, allocated size is 0x68
} DexView;

while 循环将初始化好的 context 填进 vmp_context 数组里。

VMP上下文分配与初始化循环

vmp_execute_method

回到 vmp_execute_method。参数一命名为 vmp_ctx,参数二是 method_id,其他都是 Java 层传进来的参数。一进来先把 JNI 参数压到栈里,最终的 handler 会用到它们。

vmp_execute_method函数栈帧与参数准备

Tips:DexVMP 最后都逃不掉 JNI 反射调用(除了 if 等逻辑运算,所以那些 dexvmp dump 系统用的 jni trace 也只能还原一部分 call 调用)。

vmp_interpreter_enter

vmp_interpreter_enter 是最终分发 opcode handler 的函数。这是强制让 IDA 识别为 Switch 的 Graph 图。

vmp_interpreter_enter的控制流图

大概还原下 vmp_interpreter_enter 参数 method_desc 的结构体:

__int64 __fastcall vmp_interpreter_enter(
    void  *vmp_ctx,
    JNIEnv *env,
    void  *method_desc,
    unsigned  __int64 *regs,
    unsigned  __int8 *reg_type_map)

typedef struct  VmpMethodDesc  {
    uint32_t  method_key;        // +0x00, 例如 0xAB000074
    uint16_t  unknown_04;        // +0x04, 不是 code size
    uint16_t  pad_06;            // +0x06
    uint32_t  access_flags;      // +0x08

    uint16_t  unknown_0c;        // +0x0C
    uint16_t  first_arg_reg;     // +0x0E

    uint16_t  ins_size;          // +0x10, 入参数量
    uint16_t  registers_size;    // +0x12, .registers
    uint16_t  signature_idx;     // +0x14

    uint16_t  insns_size;        // +0x16, 指令数量,单位 code unit
    uint16_t  *insns;            // +0x18, VMP 指令起始地址

    uint16_t  handler_info_20;   // +0x20
    uint16_t  handler_size;      // +0x22
    uint32_t  pad_24;            // +0x24
    void  *handler_data;         // +0x28
} VmpMethodDesc;                 // size 0x30

g_vmp_base_handler_table 取 handler,间接跳转过去。

解释器入口取出第一条指令并跳转

g_vmp_base_handler_table

g_vmp_base_handler_table 默认并不是标准顺序,第一次调用时会根据 vmp_ctx 进行重排。

运行时handler表重排的条件判断

vmp_build_runtime_handler_table的实现

用 Frida hook vmp_build_runtime_handler_table,循环打印 g_vmp_base_handler_table 256 次,拿指针减去 libso 基地址,然后 patch 回 IDA。

Interceptor.attach(libbaiduprotect.add(vmp_build_runtime_handler_table), {
    onLeave(retval) {
        for (let op = 0; op < 256; op++) {
            const handler = libbaiduprotect.add(g_vmp_base_handler_table).add(op * Process.pointerSize).readPointer();
            console.log(
                'op=0x' + ('00' + op.toString(16)).slice(-2) +
                ' -> ' + ptrOff(libbaiduprotect, handler) +
                ' abs=' + handler
            );
        }
    },
});
import ida_auto
import ida_bytes
import ida_idaapi
import ida_kernwin
import ida_name
import ida_nalt

TABLE_NAME = "g_vmp_base_handler_table"
TABLE_RVA = 0xD1418

# Set True first if you only want to preview the target table and values.
DRY_RUN = False

# Opcode order:
#   VMP_REORDERED_HANDLER_OFFSETS[opcode] == handler offset to write into table.
VMP_REORDERED_HANDLER_OFFSETS = [
    # 填入 256 个 handler 偏移
]

def msg(s):
    ida_kernwin.msg("[patch_vmp_handler_table] %s\n" % s)

def resolve_table_ea():
    ea = ida_name.get_name_ea(ida_idaapi.BADADDR, TABLE_NAME)
    if ea != ida_idaapi.BADADDR:
        return ea

    candidates = [TABLE_RVA, ida_nalt.get_imagebase() + TABLE_RVA]
    for cand in candidates:
        if ida_bytes.is_loaded(cand):
            return cand

    return ida_idaapi.BADADDR

def patch_table(table_ea, offsets):
    if len(offsets) != 256:
        raise ValueError("expected 256 handler offsets, got %d" % len(offsets))

    msg("table_ea=0x%X dry_run=%s" % (table_ea, DRY_RUN))
    msg("first: op=0x00 -> 0x%X, last: op=0xFF -> 0x%X" % (offsets[0], offsets[-1]))

    for opcode, handler_off in enumerate(offsets):
        item_ea = table_ea + opcode * 8
        if not DRY_RUN:
            ida_bytes.patch_qword(item_ea, handler_off)
            ida_bytes.set_cmt(item_ea, "op=0x%02X -> handler=0x%X" % (opcode, handler_off), 0)

    if not DRY_RUN:
        ida_name.set_name(table_ea, TABLE_NAME, ida_name.SN_CHECK | ida_name.SN_NOWARN)
        ida_auto.auto_wait()
        ida_kernwin.refresh_idaview_anyway()

    msg("done: patched %d qwords" % len(offsets))

def main():
    table_ea = resolve_table_ea()
    if table_ea == ida_idaapi.BADADDR:
        raise RuntimeError("cannot resolve %s / 0x%X" % (TABLE_NAME, TABLE_RVA))
    patch_table(table_ea, VMP_REORDERED_HANDLER_OFFSETS)

if __name__ == "__main__":
    main()

重排前后的对比效果显著。

打乱顺序的handler表

重排后的正常handler表

看第一条 opcode

当 handler 表重排后,看第一次分发的指令对应的 handler。第一次在 0x59FF0

寄存器保存与跳转代码

0x59FF0 在日志里只出现一次,它不是循环解析再调用,而更像是控制 PC 或者 goto 的形式。第一次实际调用的其实是 0x60bcc0x60bcc 在 handler 表里排第 151 位(0x97)。

间接跳转至0x60bcc的调试信息

0x59f9c:从 x26 寄存器取两个字节到 w22,即 0x2097

0x59fa0add x8, x16, w22, uxtb #3w22 取低 8 位,即 0x97,正好对应了上面的 0x60bcc

取指令并计算handler地址的汇编序列

0x789bb1ef02 的取值来自 [x19, #0x18],这正好对上了 uint16_t *insns; // +0x18, VMP 指令起始地址

从vmp_ctx读取insns指针的指令

用 Frida hook vmp_interpreter_enter 打印出当前函数的所有 ins。

Frida脚本打印insns十六进制dump

97 20 对上了,符合 trace 日志。注意,目前看到的还是 DEX CodeItem 里的 Dalvik 字节码(可能经过混淆或加密)。

VMP解释器入口参数与指令hexdump输出

vmp_resolve_method_ref_for_invoke

一进来就看到各种 JNI GetMethodID,这时候基本可以大胆推测这是在模拟 invoke-***

JNI GetMethodID及异常检查代码

dex_get_method_ref_info(a1, method_idx, out_class_idx, out_class_desc, out_name, out_sig, out_return_type) 结合 AI 给的注释看,Frida hook 打印一下,基本确定就是 invoke-***,而且只改了 opcode。method_idx 1486 也正好对得上 dex 里的 method[1486]

dex_get_method_ref_info解析出的onCreate签名

DEX method_id_item中对应的FragmentActivity.onCreate

Dalvik 字节码

先看一下正常 Dalvik 字节码格式跟 smali 的关系:

正常 Dalvik 字节码
6F 20 04 00 32 00 14 00 1C 00 0B 7F 6E 20 09 00
02 00 1A 00 15 00 1A 01 24 00 71 20 00 00 10 00
1A 00 29 00 12 01 71 30 01 00 02 01 0C 00 6E 10
02 00 00 00 0E 00
Smali 代码
    .registers 4
    .param p1, "savedInstanceState"    # Landroid/os/Bundle;
    invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V
    const v0, 0x7f0b001c
    invoke-virtual {p0, v0}, Lcom/demo/baidujiagu/MainActivity;->setContentView(I)V
    const-string v0, "MainActivity"
    const-string v1, "onCreate: "
    invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    const-string v0, "test"
    const/4 v1, 0x0
    invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
    move-result-object v0
    invoke-virtual {v0}, Landroid/widget/Toast;->show()V
    return-void
各字节解析
6f 20 04 00 32 00
unit0 = 0x206f
  opcode = 0x6f = invoke-super
  A = 2 参数个数
  G = 0
unit1 = 0x0004 = method_idx 4
  method@4 = Landroidx/appcompat/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V
unit2 = 0x0032
  C = 2, D = 3
  参数 = {v2, v3} = {p0, p1}   

Dalvik 字节码格式参考:https://source.android.com/docs/core/runtime/dalvik-bytecode?hl=zh-cn#instructions

经过上面的标准对比,invoke 范围在 0x6e - 0x72。目测 VMP 第一条指令还原成:

invoke-super {p0, p1}, Landroidx/fragment/app/FragmentActivity;->onCreate(Landroid/os/Bundle;)V

invoke-kind指令格式说明

0x97 handler 最后 goto loc_628loc_628 集中处理所有 invoke 类调用。

case 0x08及invoke分发逻辑

各类JNI CallMethodA调用分支

还原 opcode

vmp_interpreter_enter 整体可读性很强,没有特别复杂的 OLLVM,只是打乱了 opcode。

但是!戏剧性的一幕来了——代码里默认的 table 其实就是标准顺序。invoke-super 标准对应的是 0x6f,打乱后 0x97 对应的也是 loc_60BCC。只需要给 AI 提供两份前后 table,做一次对比,就能还原出可以正常解析的顺序。

vmp_table中0x6f对应处理代码

opcode与handler映射表

最终效果

invoke-super {v1, v2}, Landroidx/fragment/app/Fragment;->onCreate(Landroid/os/Bundle;)V                                            
new-instance v2, Lcom/baidu/rp/lib/base/c;
invoke-virtual {v1}, Landroidx/fragment/app/Fragment;->getActivity()Landroidx/fragment/app/FragmentActivity;
    move-result-obj ect v1
const/4 v0, 0x0    # object-null
invoke-direct {v2, v0}, Lcom/baidu/rp/lib/base/c;-><init>(Landroid/content/Context;)V
iput-obj ect v2, v1, Lcom/baidu/rp/lib/base/BaseFragment;->baseContextWrapper:Lcom/baidu/rp/lib/base/c;
return-void

// Java-like hints
void method(/* recovered */)  {
    // cu_0000: 2097 05b1 0021
    super.onCreate(v2);
    // cu_0005: 109b 059b 0001
    v1.getActivity();
    // cu_0008: 008d
    v0 = null;
    // cu_0009: 2052 3a92 0002
    v2 = new com.baidu.rp.lib.base.c(v0);
    // cu_000c: 12e0 1ad8
    v1.baseContextWrapper = v2;
    // cu_000e: 0011
    return;
}

总结

分析和写文章花了两天多时间。这个样本之所以定为“入门级”,是因为它没有太强的混淆和反调式,甚至 opcode 指令相关都没做加密,和开源项目 nmmp 很像。完全脱壳还原 dex 也并没有太大难度。完整样本就不提供了,不过应该也很好找。




上一篇:硬件面试题剖析:电源设计、高速PCB布局与低功耗调试实战要点
下一篇:CVE-2026-64531:潜伏13年的OVS内核漏洞可瞬间提权,普通用户秒变Root
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-7 04:24 , Processed in 0.785543 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表