吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1374|回复: 12
上一主题 下一主题
收起左侧

[Android 脱壳] 记录一次加固逆向分析以及加固步骤详解

[复制链接]
跳转到指定楼层
楼主
Shangwendada 发表于 2025-2-20 18:05 回帖奖励

前言

最近想要重点学习一下类抽取这种类型的加固是如何实现的,故在网上搜寻。最终看到了luoyesiqiu大佬的dpt-shell这个项目。对这个项目研究后发现这一款开源加固已经可以说很成熟了。故先对其逆向分析后再从代码层面研究如何实现的。
项目地址:https://github.com/luoyesiqiu/dpt-shell
分析版本:V1.12.2

准备阶段

欸嘿,是时候请出来之前的老朋友了(在之前分析某加固时用到的demo),然后采用dptshell进行加固
非常的方便,只需要现在编译好了的dpt.jar 然后在命令框念出如下咒语:
(吾爱破解传不了大附件,又不太像搞网盘,附件就放一个脚本吧)

java -jar dpt.jar -f /path/to/apk

等待程序吟唱:

file

吟唱结束我们就得到了:

file

(wow,太贴心了,还帮我们进行了签名)
另外如果不想签名可以看如下帮助:

usage: java -jar dpt.jar [option] -f <apk>
 -c,--disable-acf      Disable app component factory(just use for debug).
 -d,--dump-code        Dump the code item of DEX and save it to .json
                       files.
 -D,--debug            Make apk debuggable.
 -f,--apk-file <arg>   Need to protect apk file.
 -l,--noisy-log        Open noisy log.
 -x,--no-sign          Do not sign apk.

逆向分析

壳处理逻辑初步分析

file

这里可以发现类都已经被抽取了。

首先我们可以看到工厂类已经被替换成了壳的代理工厂类

file

那么这里其实就涉及到了一个知识点:
这个androidx.core.app.AppComponentFactory是用来动态控制组件示例话的,允许在 Activity、Service、BroadcastReceiver、ContentProvider 等组件被系统创建时拦截并替换其实例。dptshell在此处进行壳so的加载,以及对一些系统函数的Hook。
详细的我们可以继续往下面看
ActivityThread.handleBindApplication()
是按照什么顺序加载 APK 并加载应用程序组件的呢,我们可以看下图:

file

这里我们着重要看的就是instantiateClassLoader这个方法了。

file

这里主要载入了壳so,然后到达代理Application,完成了源dex的Application的替换了生命周期函数的调用,开始运行源dex的程序代码,并且为了其能够被正常加载做处理,后续会在源码分析中,详细来分析。

壳so解密分析:

那么对于此类抽取壳的分析,当然是要从Native层入手了。

file

这里我们选择分析arm64架构下的dpt.so
这里我们直接IDA打开会发现ELF文件中bitcode段都被加密了

file

静态解密bitcode段:

一般出现这种情况我们就需要在从initArray段入手分析了,应为initArray段执行的是构造函数,在loadlibray之后就会立马被linker所执行。
正好我们在initArray 段的sub_C67C函数中发现了如下函数:

file

函数直接就以bitCode作为参数了,实属可疑

file

sectionName被传入了sub_FD4C,这里就是在寻找bitcode段的地址,好对其进行后续的处理

file

在sub_EB7C对内存页的权限进行修改,我们可以在segment窗口中看到bitcode段的权限是不可写的,所以要通过mprotect来修改内存页的权限,方便对代码进行动态解密。

file
file

那么上面分析完了,内存页权限修改完了,接下来要做的就是对内存中被加密的字节进行解密了,

file

流程上来看肯定就是这两个了。

file

file

相信大家都能一眼看出来这个是一个RC4吧。
根据下面的参数可以找到key

file

这里需要注意的是,key的长度被限定到了16,我们在写代码的时候不能用lenkey,因为key后面很多0。
使用idapython解密bitcode段:

import idc
import ida_segment
import idautils

def rc4_decrypt(key, data):
    """RC4解密实现"""
    S = list(range(256))
    j = 0
    out = []

    # KSA初始化
    for i in range(256):
        j = (j + S[i] + key[i % 16]) % 256
        S[i], S[j] = S[j], S[i]

    # PRGA生成密钥流并解密
    i = j = 0
    for byte in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        k = S[(S[i] + S[j]) % 256]
        out.append(byte ^ k)
    return bytes(out)

def decrypt_bitcode():
    # 配置目标段名(根据步骤1结果修改)
    target_segment = ".bitcode"  # 修改为你的段名

    # 获取段对象
    seg = ida_segment.get_segm_by_name(target_segment)
    if not seg:
        print(f"[!] 错误:未找到段 '{target_segment}'")
        return

    start_ea = seg.start_ea
    end_ea = seg.end_ea
    print(f" 找到段 {target_segment}: 0x{start_ea:X}-0x{end_ea:X}")

    # 读取加密数据
    encrypted_data = idc.get_bytes(start_ea, end_ea - start_ea)
    if not encrypted_data:
        print("[!] 错误:无法读取段数据")
        return

    # 定义RC4密钥
    rc4_key = bytes([
        0xE5, 0x5E, 0x5A, 0x20, 0x2C, 0x25, 0xD9, 0x1C, 0x72, 0x74,
        0x2E, 0x36, 0x99, 0x02, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00
    ])

    # 执行解密
    decrypted_data = rc4_decrypt(rc4_key, encrypted_data)
    print("[+] 解密完成,正在写回IDA数据库...")

    # 临时修改段权限为可写
    original_perms = idc.get_segm_attr(start_ea, idc.SEGATTR_PERM)
    idc.set_segm_attr(start_ea, idc.SEGATTR_PERM, 0x7)  # RWX

    # 逐字节修补数据
    for offset, byte in enumerate(decrypted_data):
        idc.patch_byte(start_ea + offset, byte)

    # 恢复段权限
    idc.set_segm_attr(start_ea, idc.SEGATTR_PERM, original_perms)

    print("[+] 解密数据已成功写入,建议重新分析代码区域!")
    print("    操作完成!")

# 执行解密函数
decrypt_bitcode()

执行完后保存再重载文件会看到bitcode段代码被成功识别了:

file

内存中dump解密了的bitcode:

Dump SO的话,不管你用GDA也好,还是什么小工具都可以,我这里展示frida的。
frida的话首先还是得hook dlopen找到dlopen打开我们需要dump的so的时机,然后就可以开始获取so的Base和Size了具体实现如下:

function my_hook_dlopen(soName,index) {
    //mapsRedirect();
    //hook_memcmp_addr();
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    if (path.indexOf(soName) >= 0) {
                        this.is_can_hook = true;
                    }
                }
            },
            onLeave: function (retval) {
                if (this.is_can_hook) {
                    if (index == 1) {
                        NativeFunc();
                        dump_so(soName);
                    } else {
                        dump_so2(soName);
                    }

                }
            }
        }
    );
}

function dump_so2(so_name) {
    var libso = Process.getModuleByName(so_name);
    console.log("[name]:", libso.name);
    console.log("[base]:", libso.base);
    console.log("[size]:", ptr(libso.size));
    console.log("[path]:", libso.path);
    var file_path = "/sdcard/Download/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
    var file_handle = new File(file_path, "wb");
    if (file_handle && file_handle != null) {
        Memory.protect(ptr(libso.base), libso.size, 'rwx');
        var libso_buffer = ptr(libso.base).readByteArray(libso.size);
        file_handle.write(libso_buffer);
        file_handle.flush();
        file_handle.close();
        console.log("[dump]:", file_path);
    }

}

function dump_so(so_name) {
    var libso = Process.getModuleByName(so_name);
    console.log("[name]:", libso.name);
    console.log("[base]:", libso.base);
    console.log("[size]:", ptr(libso.size));
    console.log("[path]:", libso.path);
    var file_path = "/data/data/cn.pbcdci.fincryptography.appetizer/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
    var file_handle = new File(file_path, "wb");
    if (file_handle && file_handle != null) {
        Memory.protect(ptr(libso.base), libso.size, 'rwx');
        var libso_buffer = ptr(libso.base).readByteArray(libso.size);
        file_handle.write(libso_buffer);
        file_handle.flush();
        file_handle.close();
        console.log("[dump]:", file_path);
    }
}

setImmediate(my_hook_dlopen("libdpt.so",2));

这里dump_so 1和2的区别在于一个是dump到私有目录,另一个是sdcard,到sdcard是为了方便我们pull,所以我默认使用2。

启动!

file

欸嘿,虽然sodump下来了但是居然崩溃了,显然是frida被检测了,但是问题不大我们稍后分析。
先看看dump下来的so,dump的内存中的SO通常IDA是没有办法识别出导入导出表和一些字符的,需要用SOFixer来修正:
注意-m的参数是我们so在内存中的基地址。

D:\Tool\SoFixer\SoFixer-Windows-64.exe -s E:\文章\dpt-shell分析\动态\libdpt.so_0x768ae0e000_0xcc000.so  -o libdpt.so -m 0x768ae0e000 -d

file

这样就是修复好了,打开IDA检查一下:

file

发现已经没有bitcode段了,可能是由于section节不完整,但是对应的代码肯定是被解密的:

file

但是依旧出现了一些IDA识别失误的问题,但这都不是影响,能够正常的查看代码。

FRIDA检测绕过:

之前在DumpSO的时候就发现了存在Frida,检测。

file

在initArray的调用中可以看到此处创建了一个线程,我们看看线程函数是什么:

file

检测frida的关键字,这就好说了。

file

逻辑中检测可以发现就是遍历字符串扫描
sub_100E0是自实现的一个strstr

file

可以看到很经典的逐字节匹配算法。在长串中寻找字串
所以这个检测函数的功能就是通过遍历maps,寻找是否出现了frida-agent的特招,如果存在特征就直接进行崩溃,这里做的好的地方就是通过自己实现的strstr来遍历maps,可以防止直接通过hook strstr来防止检测,但是frida-agent这个特征串居然是明文存储在内存中,实属不该。

file

他们检测到之后都调用了同一个函数,这个函数就是处理检测到frida之后的崩溃逻辑的。

file

点开发现没有东西,需要查看汇编代码:

file

X30寄存器在ARM64中相当于rsp,在ret之前储存的是返回地址,这里函数将X30赋值为0之后就会产生一个 Process crashed: Bad access due to invalid address 的报错,导致程序崩溃。
那么既然这样,我们直接用一个空函数将其替换掉就好了。

function antiDetectFrida(Base) {
    var crashAddr = Base.add("0x4E864");

    var originalFunc = new NativeFunction(crashAddr, 'void', []);
    Interceptor.replace(originalFunc, new NativeCallback(function () {
    //    console.log("[Replaced] - Empty function executed");
        console.log('sub_4E894 called from:\n' + Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n') + '\n');

    }, 'void', []));
}

完整:

function antiDetectFrida(Base) {
    var crashAddr = Base.add("0x4E864");

    var originalFunc = new NativeFunction(crashAddr, 'void', []);
    Interceptor.replace(originalFunc, new NativeCallback(function () {
    //    console.log("[Replaced] - Empty function executed");
        console.log('sub_4E894 called from:\n' + Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n') + '\n');

    }, 'void', []));
}

function NativeFunc() {
    console.info("[Hook Beging]");
    var Base = Module.getBaseAddress("libdpt.so");
    console.warn("[Base]->", Base);
    antiDetectFrida(Base);
}

function hook_android_dlopen_ext() {
    var isHook = false;
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function (args) {
            this.name = args[0].readCString();
            if (this.name.indexOf("libdpt.so") > 0) {
                console.log(this.name);
                var symbols = Process.getModuleByName("linker64").enumerateSymbols();
                var callConstructorAdd = null;
                for (var index = 0; index < symbols.length; index++) {
                    const symbol = symbols[index];
                    if (symbol.name.indexOf("__dl__ZN6soinfo17call_constructorsEv") != -1) {
                        callConstructorAdd = symbol.address;
                    }
                }
                console.log("callConstructorAdd -> " + callConstructorAdd);
                Interceptor.attach(callConstructorAdd, {
                    onEnter: function (args) {
                        if (!isHook) {
                            NativeFunc();
                            isHook = true;
                        }
                    },
                    onLeave: function () { }
                });

            }
        }, onLeave: function () { }
    });
}

setImmediate(hook_android_dlopen_ext);

file

这样我们就成功hook上程序了。

DEX填充分析:

首先我们需要知道抽取壳,肯定是要对dex处理并且回填CodeItem的,那么程序肯定是要对DefineClass或者loadMEthod来在执行方法之前回填正确的字节码,那么让我们看一下在执行一个Java方法时的调用链(复制自luoyesiqiu博客):

ClassLoader.java::loadClass -> DexPathList.java::findClass -> DexFile.java::defineClass -> class_linker.cc::LoadClass -> class_linker.cc::LoadClassMembers -> class_linker.cc::LoadMethod

那么既然这样,我们思路就很明确了,看看程序在哪里注册的hook就好了。

file

这个函数就非常的像了,这里大家凭借经验应该是可以猜测出在进行hook了,但是这里似乎是使用了两个hook框架,可以尝试恢复一下符号

首先我们还是先注意一下如下格式

file

是否想起
https://github.com/bytedance/android-inline-hook
shadowhook的注册hook格式呢

#include "shadowhook.h"

void *shadowhook_hook_func_addr(
    void *func_addr,
    void *new_addr,
    void **orig_addr);

void *shadowhook_hook_sym_addr(
    void *sym_addr,
    void *new_addr,
    void **orig_addr);

void *shadowhook_hook_sym_name(
    const char *lib_name,
    const char *sym_name,
    void *new_addr,
    void **orig_addr);

typedef void (*shadowhook_hooked_t)(
    int error_number,
    const char *lib_name,
    const char *sym_name,
    void *sym_addr,
    void *new_addr,
    void *orig_addr,
    void *arg);

void *shadowhook_hook_sym_name_callback(
    const char *lib_name,
    const char *sym_name,
    void *new_addr,
    void **orig_addr,
    shadowhook_hooked_t hooked,
    void *hooked_arg);

int shadowhook_unhook(void *stub);

那就可以大胆猜测shadowhook,或者利用shadowhook的模板了。
我们直接搞一个libshadowhook.so,自己编译或者去现成的app里面搞都是可以的,我们只需要利用bindiff来载入符号就好了,类似的操作可以在
https://bbs.kanxue.com/thread-285152.htm#msg_header_h2_4
中详细查阅,我这里就简单的概述一下:

file

这里直接就能看出来壳用的基本还是shadowhook的框架,但是是存在改动的,其实到这里恢复符号的意义以及不大了。

sub_1F640的那个参数肯定就是注册的hook,但是这个时候我们又该发现问题了:

file

怎么hookDefineClass不是这个板子了,看起来也不像是ShadowHook了

file

但是我们看这个写法,显然还是在做hook。那么我们就应该知道
sub_4DAC0
这个就是在DefineClass之前通过hook执行的函数了,这里大概率也就是对Dex进行填充了。

file

这个操作也是非常模板的操作了,在我们执行完hook之后还是需要还原现场的,所以return回了原本记录的originMethod。那么sub_4D608(a3, a6, a7);肯定有一个参数是DexFile了,我们只需要在这个函数之后利用frida介入就好了。

另外指的注意的是,我们要分析这个sdk的版本,他这里sdk版本大于22走的是下面的hook小于22走的是上面的hook,不要hook错了。

后面就是sub_4D608的逻辑了
逻辑中可以翻找到

file

读取了静态资源,那么既然是抽取壳肯定是要从Assets中去读的。所以基本可以猜测这里是有对DexFile处理的逻辑了。

file

这里在对不同版本的SDK版本做不同的处理。

另外有一个非常指的注意的地方,就是处理文件传入的时候,首先我们肯定是要进行空指针判断的,这里对应的地方则是:

file

那么这里我们就可以发现,a2其实就是Dexfile的指针了。

那么这里我们只需要在sub_4D608执行完之后解析传入时的a4即可:
file

既然要解析这个DexFile,那么我们不妨看看这个DexFile对象的结构

DexFile::DexFile(
        const uint8_t* base,             //dex文件基址
        size_t size,                             //  dex文件长度
        const uint8_t* data_begin,
        size_t data_size,
        const std::string& location,
        uint32_t location_checksum,
        const OatDexFile* oat_dex_file,
        std::unique_ptr<DexFileContainer> container,
        bool is_compact_dex
)

第一个是基地址,第二个是长度,那么只需要这两个我们就可以dump下来完整的dexfile了,那么这个时候我们,使用如下(frida代码spwn启动,注意dlopen时机,我这里就只是粘贴部分代码了)

function analysisDex(Base) {

    var originalDefineClass = Base.add("0x4DB44");
    console.log("originalDefineClassAddr->", originalDefineClass)
    Interceptor.attach(originalDefineClass, {
        onEnter: function (args) {
            this.dex_file = this.context.x5;
            console.log(hexdump(this.context.x5))
        },
        onLeave: function (args) {
            var dex_file = this.dex_file;
        }
    })

}

file

阅读这里我们发现,前面8个bytes好像并不是dex的基地址,应为第2组8bytes 显然是一个地址,而不是size,而第三组8bytes才是size的样子,这是为何呢。其实是应为C++的调用约定里面第一个参数实际上是this指针,我们如果要解析的话是需要跳过这个指针的,接下来我们再看这一段内存就可以和之前Dexfile对应的参数呼应了。
获取Dexfile基地址代码如下:

function analysisDex(Base) {
    var originalDefineClass = Base.add("0x4DB44");
    console.log("originalDefineClassAddr->", originalDefineClass)
    Interceptor.attach(originalDefineClass, {
        onEnter: function (args) {
            this.dex_file = this.context.x5;
            var base = ptr(this.dex_file).add(Process.pointerSize).readPointer();
            var size = ptr(this.dex_file).add(Process.pointerSize + Process.pointerSize).readUInt();
            console.log("[DexFile]-> Base = ", base);
            console.log("[DexFile]-> size = ", size);
        },
        onLeave: function (args) {
        }
    })

}

file

为了确保我们读取的是否正确,我们可以读取base的前8个字节来看一下magic:

console.log("[DexFile]-> magic = ", magic);

file

满足我们DexFile的格式,那么聪明的你肯定发现了,这同一个Base怎么调用这么多次啊,应为抽取壳并不是一次性填充好的,他是调用的时候动态回填insns的,所以会多次的操作一个dex文件。
并且在hook的逻辑中我们可以发现,dpt-shell并没有把已经装载好的method卸载,其实也很少会有厂商这样做,会导致过多的性能损失。

那么我们只要用一个maps创建映射,存入所有不同的Dex的base和size,在我们程序加载完了之后,我们遍历这个maps进行dump不就好了嘛?

具体实现如下:

const dexMap = new Map();

function analysisDex(Base) {
    var originalDefineClass = Base.add("0x4DB44");
    console.log("originalDefineClassAddr->", originalDefineClass)
    Interceptor.attach(originalDefineClass, {
        onEnter: function (args) {
            this.dex_file = this.context.x5;
            var base = ptr(this.dex_file).add(Process.pointerSize).readPointer();
            var size = ptr(this.dex_file).add(Process.pointerSize + Process.pointerSize).readUInt();
            console.log("[DexFile]-> Base = ", base);
            console.log("[DexFile]-> size = ", size);
            var magic = ptr(base).readCString();
            console.log("[DexFile]-> magic = ", magic);
            // 检查 base 和 size 是否已存在
            let isDuplicate = false;
            for (let [existingBase, existingSize] of dexMap.entries()) {
                if (existingBase.equals(base) && existingSize === size) {
                    isDuplicate = true;
                    break;
                }
            }

            if (isDuplicate) {
                console.log(`[WARN] DexFile with base ${base} and size ${size} already exists, skipping...`);
            } else {
                dexMap.set(base, size);
                console.log(`[INFO] New DexFile found: base=${base}, size=${size}`);
            }
        },
        onLeave: function (args) {
        }
    })

}

function printDexMap() {
    console.log("Current DexFile Map:");
    for (let [base, size] of dexMap.entries()) {
        console.log(`Base: ${base}, Size: ${size}`);
    }
}

当我们frida输出变得缓慢的时候,或者不再输出的时候我们调用一下printDexMap():

file

这样我们就获得了所有加载的dex的base与size,然后写一个遍历脚本进行dump就行了:

这里我已经写好了一个直接dump到私有目录的:

function get_self_process_name() {
    var openPtr = Module.getExportByName('libc.so', 'open');
    var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
    var readPtr = Module.getExportByName("libc.so", "read");
    var read = new NativeFunction(readPtr, "int", ["int", "pointer", "int"]);
    var closePtr = Module.getExportByName('libc.so', 'close');
    var close = new NativeFunction(closePtr, 'int', ['int']);
    var path = Memory.allocUtf8String("/proc/self/cmdline");
    var fd = open(path, 0);

    if (fd != -1) {
        var buffer = Memory.alloc(0x1000);
        var result = read(fd, buffer, 0x1000);
        close(fd);
        result = ptr(buffer).readCString();
        return result
    }

    return "-1"
}

function Mkdir(path) {
    if (path.indexOf("com") == -1) {
        console.log("[Mkdir]-> Pass:", path);
        return 0;
    }
    var mkdirPtr = Module.getExportByName('libc.so', 'mkdir');
    var mkdir = new NativeFunction(mkdirPtr, 'int', ['pointer', 'int']);
    var opendirPtr = Module.getExportByName('libc.so', 'opendir');
    var opendir = new NativeFunction(opendirPtr, 'pointer', ['pointer']);
    var closedirPtr = Module.getExportByName('libc.so', 'closedir');
    var closedir = new NativeFunction(closedirPtr, 'int', ['pointer']);
    var cPath = Memory.allocUtf8String(path);
    var dir = opendir(cPath);

    if (dir != 0) {
        closedir(dir);
        return 0
    }

    mkdir(cPath, 0o755);
    chmod(path)
    console.log("[Mkdir]->", path);
}

function chmod(path) {
    var chmodPtr = Module.getExportByName('libc.so', 'chmod');
    var chmod = new NativeFunction(chmodPtr, 'int', ['pointer', 'int']);
    var cPath = Memory.allocUtf8String(path);
    chmod(cPath, 755)
}

function dumpDex() {

    dexMap.forEach((size, base) => {
        console.log(`Base: ${base}, Size: ${size}`);
        var magic = ptr(base).readCString();
        console.log("DesFileMagic->", magic);
        if (magic.indexOf("dex") == 0) {
            var process_name = get_self_process_name();
            if (process_name != "-1") {
                var dex_dir_path = "/data/data/" + process_name + "/files"
                Mkdir(dex_dir_path)
                dex_dir_path += "/dump_dex_" + process_name
                Mkdir(dex_dir_path)
                var dex_path = dex_dir_path + "/class" + (dex_count == 1 ? "" : dex_count) + ".dex"; console.log("[find dex]:", dex_path); var fd = new File(dex_path, "wb");
                if (fd && fd != null) {
                    dex_count++; var dex_buffer = ptr(base).readByteArray(size);
                    fd.write(dex_buffer); fd.flush();
                    fd.close(); console.log("[dump dex]:", dex_path)
                }
            }
        }
    });
}

等待程序加载好后我们直接调用DumpDex即可:

file

私有目录中即可找到这个file

file

反编译即可发现被抽取的类都填充好了:

file

原理分析

这里主要分析一下程序如何处理被抽取的类填充回Class,对照源码进行分析。另外的步骤在上文的逆向过程说以及解释的差不多了

源码可以看到此处是DobbyHook

file

校验了SDK版本,不同SDK版本不同处理方式

file

这里直接就走patchClass了,那么我们重点要分析的就是patchClass的逻辑了

file

这里就是在根据不同的SDK版本来解析 DexFile

uint64_t static_fields_size = 0;
read += DexFileUtils::readUleb128(class_data, &static_fields_size);

uint64_t instance_fields_size = 0;
read += DexFileUtils::readUleb128(class_data + read, &instance_fields_size);

uint64_t direct_methods_size = 0;
read += DexFileUtils::readUleb128(class_data + read, &direct_methods_size);

uint64_t virtual_methods_size = 0;
read += DexFileUtils::readUleb128(class_data + read, &virtual_methods_size);

获取类中字段和方法的数量,为后续解析做准备

dex::ClassDataField staticFields[static_fields_size];
read += DexFileUtils::readFields(class_data + read, staticFields, static_fields_size);

dex::ClassDataField instanceFields[instance_fields_size];
read += DexFileUtils::readFields(class_data + read, instanceFields, instance_fields_size);

dex::ClassDataMethod directMethods[direct_methods_size];
read += DexFileUtils::readMethods(class_data + read, directMethods, direct_methods_size);

dex::ClassDataMethod virtualMethods[virtual_methods_size];
read += DexFileUtils::readMethods(class_data + read, virtualMethods, virtual_methods_size);

获取类中所有字段和方法的详细信息,为后续修补做准备

file

这里就将之前读取到的所有的方法都传入patchMethod中来修改。

file

然后就是patchMethod了,这里主要是利用了之前维护好的dexMap,修改对应内存段权限后在Map查找CodeItem,然后使用memcopy填入。

这样的流程就完成了类的动态回填。

总结

dpt-shell上有很多值得学习的技术和加固原理,一次非常充实的学习过程

hook.zip

2.08 KB, 下载次数: 12, 下载积分: 吾爱币 -1 CB

js脚本

免费评分

参与人数 8吾爱币 +9 热心值 +8 收起 理由
GDEason + 1 + 1 我很赞同!
5omggx + 1 + 1 用心讨论,共获提升!
lst13145920 + 1 + 1 用心讨论,共获提升!
helian147 + 1 + 1 热心回复!
allspark + 1 + 1 用心讨论,共获提升!
ttonott + 1 + 1 谢谢@Thanks!
ngiokweng + 2 + 1 用心讨论,共获提升!
Aircraftcomman5 + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
Aircraftcomman5 发表于 2025-2-22 09:24
感谢分享,一篇实用的app逆向
3#
amwquhwqas128 发表于 2025-2-22 22:24
4#
柯南丶 发表于 2025-2-23 00:01
5#
Lhnn73 发表于 2025-2-23 17:46
多谢分享
6#
xiaoniudia 发表于 2025-2-24 11:14
留着慢慢研究,讲得透彻
7#
hbw0510 发表于 2025-3-1 17:45
多谢分享,又是一次学习
8#
badcby 发表于 2025-3-13 14:02
精华帖都看不懂啊
9#
klhabc 发表于 2025-3-14 14:17
谢谢分享。
10#
NXXX13 发表于 2025-3-14 20:54
谢谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-3-18 16:39

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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