安卓逆向 记录一次加固逆向分析以及加固步骤详解

admin 2025年4月23日11:19:10评论1 views字数 16650阅读55分30秒阅读模式

前言

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

准备阶段

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

 复制代码 隐藏代码
java -jar dpt.jar -f /path/to/apk

等待程序吟唱:

安卓逆向  记录一次加固逆向分析以及加固步骤详解

吟唱结束我们就得到了:

安卓逆向  记录一次加固逆向分析以及加固步骤详解

(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.

逆向分析

壳处理逻辑初步分析

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

壳so解密分析:

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解
静态解密bitcode段:

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解
安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解
安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

 复制代码 隐藏代码
import idc
import ida_segment
import idautils

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

# KSA初始化
for i inrange(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)
returnbytes(out)

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

# 获取段对象
    seg = ida_segment.get_segm_by_name(target_segment)
ifnot 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)
ifnot encrypted_data:
print("[!] 错误:无法读取段数据")
return

# 定义RC4密钥
    rc4_key = bytes([
0xE50x5E0x5A0x200x2C0x250xD90x1C0x720x74,
0x2E0x360x990x020x800x060x000x000x000x00,
0x000x000x00
    ])

# 执行解密
    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 inenumerate(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段代码被成功识别了:

安卓逆向  记录一次加固逆向分析以及加固步骤详解
内存中dump解密了的bitcode:

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

 复制代码 隐藏代码
functionmy_hook_dlopen(soName,index) {
//mapsRedirect();
//hook_memcmp_addr();
Interceptor.attach(Module.findExportByName(null"android_dlopen_ext"),
        {
onEnterfunction (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;
                    }
                }
            },
onLeavefunction (retval) {
if (this.is_can_hook) {
if (index == 1) {
NativeFunc();
dump_so(soName);
                    } else {
dump_so2(soName);
                    }

                }
            }
        }
    );
}

functiondump_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 = newFile(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);
    }

}

functiondump_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 = newFile(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。

启动!

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

 复制代码 隐藏代码
D:ToolSoFixerSoFixer-Windows-64.exe -s E:文章dpt-shell分析动态libdpt.so_0x768ae0e000_0xcc000.so  -o libdpt.so -m 0x768ae0e000 -d
安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

FRIDA检测绕过:

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

 复制代码 隐藏代码
functionantiDetectFrida(Base) {
var crashAddr = Base.add("0x4E864");

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

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

完整:

 复制代码 隐藏代码
functionantiDetectFrida(Base) {
var crashAddr = Base.add("0x4E864");

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

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

functionNativeFunc() {
console.info("[Hook Beging]");
varBase = Module.getBaseAddress("libdpt.so");
console.warn("[Base]->"Base);
antiDetectFrida(Base);
}

functionhook_android_dlopen_ext() {
var isHook = false;
Interceptor.attach(Module.findExportByName(null"android_dlopen_ext"), {
onEnterfunction (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, {
onEnterfunction (args) {
if (!isHook) {
NativeFunc();
                            isHook = true;
                        }
                    },
onLeavefunction () { }
                });

            }
        }, onLeavefunction () { }
    });
}

setImmediate(hook_android_dlopen_ext);
安卓逆向  记录一次加固逆向分析以及加固步骤详解

这样我们就成功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就好了。

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

是否想起
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(
constchar *lib_name,
constchar *sym_name,
void *new_addr,
void **orig_addr)
;

typedefvoid(*shadowhook_hooked_t)(
int error_number,
constchar *lib_name,
constchar *sym_name,
void *sym_addr,
void *new_addr,
void *orig_addr,
void *arg)
;

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

intshadowhook_unhook(void *stub);

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

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

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

那么这里我们只需要在sub_4D608执行完之后解析传入时的a4即可:
安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

 复制代码 隐藏代码
DexFile::DexFile(
constuint8_t* base,             //dex文件基址
size_t size,                             //  dex文件长度
constuint8_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时机,我这里就只是粘贴部分代码了)

 复制代码 隐藏代码
functionanalysisDex(Base) {

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

}
安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

 复制代码 隐藏代码
functionanalysisDex(Base) {
var originalDefineClass = Base.add("0x4DB44");
console.log("originalDefineClassAddr->", originalDefineClass)
Interceptor.attach(originalDefineClass, {
onEnterfunction (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);
        },
onLeavefunction (args) {
        }
    })

}
安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

 复制代码 隐藏代码
console.log("[DexFile]-> magic = ", magic);
安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

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

具体实现如下:

 复制代码 隐藏代码
const dexMap = newMap();

functionanalysisDex(Base) {
var originalDefineClass = Base.add("0x4DB44");
console.log("originalDefineClassAddr->", originalDefineClass)
Interceptor.attach(originalDefineClass, {
onEnterfunction (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}`);
            }
        },
onLeavefunction (args) {
        }
    })

}

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

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

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

 复制代码 隐藏代码
functionget_self_process_name() {
var openPtr = Module.getExportByName('libc.so''open');
var open = newNativeFunction(openPtr, 'int', ['pointer''int']);
var readPtr = Module.getExportByName("libc.so""read");
var read = newNativeFunction(readPtr, "int", ["int""pointer""int"]);
var closePtr = Module.getExportByName('libc.so''close');
var close = newNativeFunction(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"
}

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

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

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

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

functiondumpDex() {

    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 = newFile(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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

原理分析

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

源码可以看到此处是DobbyHook

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

这里就是在根据不同的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);

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

安卓逆向  记录一次加固逆向分析以及加固步骤详解

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

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

总结

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

· 今 日 推 荐 ·

安卓逆向  记录一次加固逆向分析以及加固步骤详解

本文内容来自网络,如有侵权请联系删除

原文始发于微信公众号(逆向有你):安卓逆向 -- 记录一次加固逆向分析以及加固步骤详解

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月23日11:19:10
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   安卓逆向 记录一次加固逆向分析以及加固步骤详解https://cn-sec.com/archives/3967328.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息