6822 字
34 分钟
360加固复现
2025-11-06

0、前言#

创建一个简单的app,里面就是一个数据验证和一句话,然后360免费加固

学习参考来自SWDD大佬和oacia大佬

PKID看确实是360加固

app里的代码:

package com.example.learn;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EditText inputField = findViewById(R.id.inputField);
Button checkButton = findViewById(R.id.checkButton);
checkButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String input = inputField.getText().toString().trim();
if (input.equals("66")) {
showSuccessDialog();
}
// 其他输入不做任何反应
}
});
}
private void showSuccessDialog() {
new AlertDialog.Builder(this)
.setTitle("结果")
.setMessage("success")
.setPositiveButton("确定", null)
.show();
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="study by sh4d0w"
android:textSize="18sp"
android:layout_marginBottom="20dp"/>
<EditText
android:id="@+id/inputField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入数字"
android:inputType="number"/>
<Button
android:id="@+id/checkButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="验证"
android:layout_marginTop="20dp"/>
</LinearLayout>

image-20250612221553518

mt查看如下:

image-20250612221620218

image-20250612221820271

内容如上,输入66才有反应

1、初探#

放入jadx,把learn.so放入ida

image-20250612222618285

java层是可以发现一些蛛丝马迹的,比如这里的libjiagu字符,这是360加固的标志性字符

native层倒是不能直接确定加壳方式

java层初步分析#

我们在资源目录下找到AndroidManifest.xml,从里面可以得知360加固的入口是com.stub.StubAppimage-20250612223206686

因为com.stub.StubApp是第一个实例化的application,且StubAppattachBaseContext方法是加固逻辑的第一个执行入口

我们在这个类中,不仅能看见attachBaseContext还能找到onCreate

Application 的 onCreateattachBaseContext 是 Application 的两个回调方法,通常我们会在其中做一些初始化操作, attachBaseContextonCreate 之前执行

image-20250612233355185

分析attachBaseContext,中间的内容明显是被加密了的,我们查看a方法image-20250612233520668

可以发现其实就是异或16的操作,写脚本解密过后注释上去image-20250612234407697

  • 在 Android 9.0 (API 28) 中,Google 限制了非 SDK 接口的访问。这段代码通过反射绕过该限制,确保加固框架能够正常调用系统隐藏 API
  • mHiddenApiWarningShown字段控制隐藏 API 警告的显示,设置为true可避免触发警告image-20250612234913289

下面这些内容就是它会判断手机的架构,针对不同的架构加载不同的Native文件image-20250612235351933

再往下可以找到DtcLoader初始化的操作,jadx貌似反编译不出来,我们使用jebimage-20250613001435773

可以发现它调用了native层的jgdtc.so,当DtcLoader被加载到jvm时,会调用这个so文件,但是我们跟着路径去寻找是找不到这个文件的(看其他文章都略过了,应该不重要。。。)

我们要分析的是libjiagu.so,这个 so 在 assets 目录下

壳elf导入导出表修复#

选取libjiagu_a64.so分析,可以发现它导入表导出表都没有内容的,我们需要去修复

先去看看dlopen调用了哪些so文件

function hookTest1() {
Interceptor.attach(Module.findExportByName("libdl.so", "android_dlopen_ext"), {
onEnter: function (args) {
console.log("Load -> ", args[0].readCString());
}, onLeave: function () {
}
})
}
function main() {
Java.perform(function () {
hookTest1();
});
}
setImmediate(main);

image-20250613010136702

这也是我们要分析的so,需要把libjiagu_64.sodump下来

function hookTest1() {
var libSo = Process.getModuleByName("libjiagu_64.so");
console.log("[+]base: ", libSo.base);
console.log("[+]size: ", ptr(libSo.size));
var save_path = "/data/data/com.example.learn/" + libSo.name + "_Dump";
var handle = new File(save_path, "wb");
Memory.protect(ptr(libSo.base), libSo.size, 'rwx');
var Buffer = libSo.base.readByteArray(libSo.size);
handle.write(Buffer);
handle.flush();
handle.close();
console.log("[+]path: ", save_path);
}
function main() {
Java.perform(function () {
hookTest1();
});
}
setImmediate(main);
// [+]base: 0x7250470000
// [+]size: 0x27a000
// [+]path: /data/data/com.example.learn/libjiagu_64.so_Dump

然后使用Sofixer工具去修复elf

命令如下./SoFixer-macOS-64 -s /Users/lanzhiqiang/Desktop/360test/protect/assets/libjiagu_64.so_Dump -o /Users/lanzhiqiang/Desktop/360test/protect/assets/libjiagu_fix.so -m 0x7250470000 -d

image-20250613012453996image-20250613012604265

壳elf分析#

虽然有了导入表和导出表,但是我们还是需要想办法跟踪逻辑找关键函数,那我们就可以hook open函数来看看打开了哪些

function hookOpen() {
var openPtr = Module.getExportByName(null, 'open');
console.log("[*]hook open");
Interceptor.attach(openPtr,{
onEnter: function (args) {
this.filename = args[0];
console.log("[+]open: ", this.filename.readCString());
}, onLeave: function (retval) {
}
})
}
function hook_dlopne() {
Interceptor.attach(Module.findExportByName("libdl.so", "android_dlopen_ext"), {
onEnter: function (args) {
var loadFileName = args[0].readCString();
if (loadFileName.indexOf('libjiagu') != -1) {
this.is_can_hook = true;
}
}, onLeave: function () {
if (this.is_can_hook) {
hookOpen();
}
}
})
}
function main() {
Java.perform(function () {
hook_dlopne();
});
}
setImmediate(main);

image-20250613015630857

这里可以注意到反复调用了/proc/self/maps,这就是典型的内存映射检测反frida,因为frida使用时会在内存中注入frida-agent.so文件

绕过方式呢也不难,即然它检测的这个maps,那我们就手动调用open函数,在其调用maps时重定向至其他自定义maps即可

比如我在data/data/com.example.learn/下新建了一个maps空文件

然后用如下脚本去看看是否成功 and 调用了哪些dex

function hookOpen() {
var FakeMaps = "/data/data/com.example.learn/maps";
var openPtr = Module.getExportByName(null, 'open');
console.log("[*] Replacing open function");
// 获取原始open函数
var originalOpen = new NativeFunction(openPtr,'int',['pointer', 'int']);
// 替换open函数
Interceptor.replace(openPtr, new NativeCallback(function(fileNamePtr, flags) {
var fileName = fileNamePtr.readCString();
if (fileName.indexOf("maps") >= 0) {
console.warn("[-] Intercepted read of maps file");
var fakeFilename = Memory.allocUtf8String(FakeMaps);
return originalOpen(fakeFilename, flags);
}
if (fileName.indexOf('dex') != -1) {
console.log("[+] Opening dex:", fileName);
}
// 其他情况调用原始函数
return originalOpen(fileNamePtr, flags);
}, 'int', ['pointer', 'int']));
}
function hook_dlopne() {
Interceptor.attach(Module.findExportByName("libdl.so", "android_dlopen_ext"), {
onEnter: function(args) {
var loadFileName = args[0].readCString();
if (loadFileName.indexOf('libjiagu') != -1) {
this.is_can_hook = true;
}
},
onLeave: function() {
if (this.is_can_hook) {
hookOpen();
}
}
});
}
function main() {
Java.perform(function() {
hook_dlopne();
});
}
setImmediate(main);

image-20250613184504386

通过返回结果,即可以确定是调用maps去隐藏内存映射,也发现打开了三个dex文件

那么我们就要去追踪这些dex文件的调用情况,即追踪调用栈

我们在上一份代码中加一些内容即可

function hookOpen() {
var FakeMaps = "/data/data/com.example.learn/maps";
var openPtr = Module.getExportByName(null, 'open');
console.log("[*] Replacing open function");
// 获取原始open函数
var originalOpen = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
// 替换open函数
Interceptor.replace(openPtr, new NativeCallback(function (fileNamePtr, flags) {
var fileName = fileNamePtr.readCString();
if (fileName.indexOf("maps") >= 0) {
console.warn("[-] Intercepted read of maps file");
var fakeFilename = Memory.allocUtf8String(FakeMaps);
return originalOpen(fakeFilename, flags);
}
if (fileName.indexOf('dex') != -1) {
console.log("[+] Opening dex:", fileName);
Thread.backtrace(this.context, Backtracer.FUZZY).map(addr_in_so);
}
// 其他情况调用原始函数
return originalOpen(fileNamePtr, flags);
}, 'int', ['pointer', 'int']));
}
function hook_dlopne() {
Interceptor.attach(Module.findExportByName("libdl.so", "android_dlopen_ext"), {
onEnter: function (args) {
var loadFileName = args[0].readCString();
if (loadFileName.indexOf('libjiagu') != -1) {
this.is_can_hook = true;
}
},
onLeave: function () {
if (this.is_can_hook) {
hookOpen();
}
}
});
}
function addr_in_so(addr){
var process_Obj_Module_Arr = Process.enumerateModules();
for(var i = 0; i < process_Obj_Module_Arr.length; i++) {
if(addr>process_Obj_Module_Arr[i].base && addr<process_Obj_Module_Arr[i].base.add(process_Obj_Module_Arr[i].size)){
console.log(addr.toString(16),"is in",process_Obj_Module_Arr[i].name,"offset: 0x"+(addr-process_Obj_Module_Arr[i].base).toString(16));
}
}
}
function main() {
Java.perform(function () {
hook_dlopne();
});
}
setImmediate(main);
// Backtracer.FUZZY找模糊调用栈,Backtracer.ACCURATE找精确调用栈

image-20250613190044043

这里不管是精确还是模糊,都能看出三个dex文件的调用栈基本一致

根据偏移,我们去ida里寻找image-20250613193059079

填充的一堆数据,目前也不知道有啥用,往下继续翻到image-20250613193624170

发现被调用了,查看是sub_8510函数image-20250613193712128

这里的内容像是so的加载器,这时候是得猜测是自定义linker实现加固so

此时可以hookdlopen验证猜想

function hookTest1() {
Interceptor.attach(Module.findExportByName("libdl.so", "android_dlopen_ext"), {
onEnter: function (args) {
console.log("[-]android_dlopen_ext -> ", args[0].readCString());
}, onLeave: function () {
}
})
}
function hookTest2(){
Interceptor.attach(Module.findExportByName("libdl.so", "dlopen"), {
onEnter: function (args) {
console.log("[+]dlopen -> ", args[0].readCString());
}, onLeave: function () {
}
})
}
function main() {
Java.perform(function () {
hookTest1();
hookTest2();
});
}
setImmediate(main);
[Pixel 3::com.example.learn ]-> [-]android_dlopen_ext -> /data/data/com.example.learn/.jiagu/libjiagu_64.so
[+]dlopen -> liblog.so
[+]dlopen -> libz.so
[+]dlopen -> libc.so
[+]dlopen -> libm.so
[+]dlopen -> libstdc++.so
[+]dlopen -> libdl.so
[+]dlopen -> libjiagu_64.so
[+]dlopen -> libjiagu_64.so
[+]dlopen -> libart.so
[+]dlopen -> libjiagu_64.so
[+]dlopen -> libjiagu_64.so
[+]dlopen -> libjiagu_64.so
[+]dlopen -> libjiagu_64.so
[+]dlopen -> libjiagu_64.so
[+]dlopen -> libjiagu_64.so
[-]android_dlopen_ext -> libjgdtc.so
[-]android_dlopen_ext -> /data/app/~~ngtLehdiUPtT0kijbL8dtg==/com.example.learn-82fYzt_aqOJ6-F_XHYtjlg==/lib/arm64/libjgdtc.so
[+]dlopen -> libandroid.so
[-]android_dlopen_ext -> /vendor/lib64/hw/gralloc.sdm845.so
[+]dlopen -> libEGL_adreno.so
[-]android_dlopen_ext -> /vendor/lib64/hw/android.hardware.graphics.mapper@2.0-impl-qti-display.so
[+]dlopen -> libandroid.so
  1. 明显重复多次调用libjiagu_64.so,标准调用一般是不会重复调用的
    • 每次加载时解密不同部分的代码,防止一次性获取完整逻辑
  2. 加固后的库被存储在应用私有目录的隐藏文件夹(.jiagu)中

这些信息可以确定它是自定义linker

我们在010中对libjiagu_64.so(壳elf)分析可以找到一个新的elf文件,这里大概率就是上面在ida中找到的自定义linker实现加固so后的so文件

image-20250613201544055

我们需要想办法把它dump下来

with open('/Users/lanzhiqiang/Desktop/360test/protect/assets/libjiagu_fix.so','rb') as f:
s=f.read()
with open('/Users/lanzhiqiang/Desktop/360test/protect/assets/main.so','wb') as f:
f.write(s[0xe7000::])

然后我们这里dump出来的main.so就是主elf了

主elf分析#

image-20250613203613538

其实是可以看出elf的program header table是被加密了的,ida也无法分析image-20250613203656055

那我们就需要想办法解密主elf了

壳 elf 在代码中自己实现了解析 ELF 文件的函数,并将解析结果赋值到 soinfo 结构体中,随后调用 dlopen 进行手动加载

  • soinfo 是 Android 系统中用于表示和管理动态链接库 (Shared Object, SO) 的核心结构体,全称为 “Shared Object Information”。它位于 Bionic C 库 (bionic/libc/include/bits/soinfo.h) 中,是动态链接器 (Linker) 的核心数据结构之一

在ida中对dlopen进行交叉引用,查看调用

image-20250613214835458

image-20250613222355297

这个函数的内容和AOSP里的linker.cpp源码很像,如下d12d28ae89f336a9c0701a5ea76cc050_720

7a24659789ac95d907a3e77fcc63e7e8_720

这里基本就是自定义linker实现的预链接函数了,我们需要导入soinfo结构体

在 ida 中依次点击 View->Open subviews->Local Types , 然后按下键盘上的 Insert 将下面的结构体添加到对话框中

//IMPORTANT
//ELF64 启用该宏
#define __LP64__ 1
//ELF32 启用该宏
//#define __work_around_b_24465209__ 1
/*
//https://android.googlesource.com/platform/bionic/+/master/linker/Android.bp
架构为 32 位 定义__work_around_b_24465209__宏
arch: {
arm: {cflags: ["-D__work_around_b_24465209__"],},
x86: {cflags: ["-D__work_around_b_24465209__"],},
}
*/
//android-platform\bionic\libc\include\link.h
#if defined(__LP64__)
#define ElfW(type) Elf64_ ## type
#else
#define ElfW(type) Elf32_ ## type
#endif
//android-platform\bionic\linker\linker_common_types.h
// Android uses RELA for LP64.
#if defined(__LP64__)
#define USE_RELA 1
#endif
//android-platform\bionic\libc\kernel\uapi\asm-generic\int-ll64.h
//__signed__-->signed
typedef signed char __s8;
typedef unsigned char __u8;
typedef signed short __s16;
typedef unsigned short __u16;
typedef signed int __s32;
typedef unsigned int __u32;
typedef signed long long __s64;
typedef unsigned long long __u64;
//A12-src\msm-google\include\uapi\linux\elf.h
/* 32-bit ELF base types. */
typedef __u32 Elf32_Addr;
typedef __u16 Elf32_Half;
typedef __u32 Elf32_Off;
typedef __s32 Elf32_Sword;
typedef __u32 Elf32_Word;
/* 64-bit ELF base types. */
typedef __u64 Elf64_Addr;
typedef __u16 Elf64_Half;
typedef __s16 Elf64_SHalf;
typedef __u64 Elf64_Off;
typedef __s32 Elf64_Sword;
typedef __u32 Elf64_Word;
typedef __u64 Elf64_Xword;
typedef __s64 Elf64_Sxword;
typedef struct dynamic{
Elf32_Sword d_tag;
union{
Elf32_Sword d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
typedef struct {
Elf64_Sxword d_tag; /* entry tag value */
union {
Elf64_Xword d_val;
Elf64_Addr d_ptr;
} d_un;
} Elf64_Dyn;
typedef struct elf32_rel {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
typedef struct elf64_rel {
Elf64_Addr r_offset; /* Location at which to apply the action */
Elf64_Xword r_info; /* index and type of relocation */
} Elf64_Rel;
typedef struct elf32_rela{
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela;
typedef struct elf64_rela {
Elf64_Addr r_offset; /* Location at which to apply the action */
Elf64_Xword r_info; /* index and type of relocation */
Elf64_Sxword r_addend; /* Constant addend used to compute value */
} Elf64_Rela;
typedef struct elf32_sym{
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
typedef struct elf64_sym {
Elf64_Word st_name; /* Symbol name, index in string tbl */
unsigned char st_info; /* Type and binding attributes */
unsigned char st_other; /* No defined meaning, 0 */
Elf64_Half st_shndx; /* Associated section index */
Elf64_Addr st_value; /* Value of the symbol */
Elf64_Xword st_size; /* Associated symbol size */
} Elf64_Sym;
#define EI_NIDENT 16
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
/* These constants define the permissions on sections in the program
header, p_flags. */
#define PF_R 0x4
#define PF_W 0x2
#define PF_X 0x1
typedef struct elf32_phdr{
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
typedef struct elf64_phdr {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment, file & memory */
} Elf64_Phdr;
typedef struct elf32_shdr {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;
typedef struct elf64_shdr {
Elf64_Word sh_name; /* Section name, index in string tbl */
Elf64_Word sh_type; /* Type of section */
Elf64_Xword sh_flags; /* Miscellaneous section attributes */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Size of section in bytes */
Elf64_Word sh_link; /* Index of another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
//android-platform\bionic\linker\linker_soinfo.h
typedef void (*linker_dtor_function_t)();
typedef void (*linker_ctor_function_t)(int, char**, char**);
#if defined(__work_around_b_24465209__)
#define SOINFO_NAME_LEN 128
#endif
struct soinfo {
#if defined(__work_around_b_24465209__)
char old_name_[SOINFO_NAME_LEN];
#endif
const ElfW(Phdr)* phdr;
size_t phnum;
#if defined(__work_around_b_24465209__)
ElfW(Addr) unused0; // DO NOT USE, maintained for compatibility.
#endif
ElfW(Addr) base;
size_t size;
#if defined(__work_around_b_24465209__)
uint32_t unused1; // DO NOT USE, maintained for compatibility.
#endif
ElfW(Dyn)* dynamic;
#if defined(__work_around_b_24465209__)
uint32_t unused2; // DO NOT USE, maintained for compatibility
uint32_t unused3; // DO NOT USE, maintained for compatibility
#endif
soinfo* next;
uint32_t flags_;
const char* strtab_;
ElfW(Sym)* symtab_;
size_t nbucket_;
size_t nchain_;
uint32_t* bucket_;
uint32_t* chain_;
#if !defined(__LP64__)
ElfW(Addr)** unused4; // DO NOT USE, maintained for compatibility
#endif
#if defined(USE_RELA)
ElfW(Rela)* plt_rela_;
size_t plt_rela_count_;
ElfW(Rela)* rela_;
size_t rela_count_;
#else
ElfW(Rel)* plt_rel_;
size_t plt_rel_count_;
ElfW(Rel)* rel_;
size_t rel_count_;
#endif
linker_ctor_function_t* preinit_array_;
size_t preinit_array_count_;
linker_ctor_function_t* init_array_;
size_t init_array_count_;
linker_dtor_function_t* fini_array_;
size_t fini_array_count_;
linker_ctor_function_t init_func_;
linker_dtor_function_t fini_func_;
/*
#if defined (__arm__)
// ARM EABI section used for stack unwinding.
uint32_t* ARM_exidx;
size_t ARM_exidx_count;
#endif
size_t ref_count_;
// 怎么找不 link_map 这个类型的声明...
link_map link_map_head;
bool constructors_called;
// When you read a virtual address from the ELF file, add this
//value to get the corresponding address in the process' address space.
ElfW (Addr) load_bias;
#if !defined (__LP64__)
bool has_text_relocations;
#endif
bool has_DT_SYMBOLIC;
*/
};

导入后,在ida中sub_8510函数的a1进行类型声明soinfo* a1

image-20250613223146115

但是我们观察一下可以发现,其实360加固里的soinfo是被魔改了的,比如上图中框起来的部分,没有完全修复

向上找调用关系,去看怎么魔改的image-20250613223403814

在源码中也可以找到类似的地方74b53eda8b7930aa01e4fc9ed3f04c44

所以可以直接说sub_4C7C是register_soinfo_tls函数,往里面找到image-20250613224723415

这里的0x38,我们在010里看对应关系108f62d9877e6831bfe114e888618c91_720

恰好程序头大小也是0x38,那么这个方法肯定就是在加载程序头了

其他的比较乱,我们去找程序执行流,再通过上面发现的内容去推

[Pixel 3::com.example.learn ]-> start Stalker!
Stalker end!
call1:JNI_OnLoad
call2:j_interpreter_wrap_int64_t
call3:interpreter_wrap_int64_t
call4:_Znwm
call5:sub_13F10
call6:_Znam
call7:sub_11838
call8:memset
call9:sub_A534
call10:sub_E9F8
call11:calloc
call12:malloc
call13:free
call14:sub_EC60
call15:_ZdaPv
call16:sub_CF64
call17:sub_D41C
call18:sub_A0E4
call19:sub_A0C0
call20:sub_D58C
call21:sub_D150
call22:sub_A220
call23:sub_16200
call24:sub_16978
call25:sub_16A44
call26:sub_16578
call27:sub_17238
call28:sub_165F8
call29:sub_162D4
call30:sub_16240
call31:sub_A05C
call32:sub_D474
call33:sub_D670
call34:sub_D3BC
call35:sub_99F8
call36:dladdr
call37:strstr
call38:setenv
call39:_Z9__arm_a_1P7_JavaVMP7_JNIEnvPvRi
call40:sub_A5B4
call41:sub_A0F8
call42:sub_10F7C
call43:j__ZdlPv_1
call44:_ZdlPv
call45:sub_9E3C
call46:sub_8510
call47:__strncpy_chk2
call48:sub_62DC
call49:sub_6740
call50:sub_4EB0
call51:sub_6324
call52:_ZN9__arm_c_19__arm_c_0Ev
call53:sub_AB0C
call54:sub_A128
call55:sub_A0A0
call56:sub_D808
call57:sub_6680
call58:sub_678C
call59:memcpy
call60:sub_6894
call61:sub_6184
call62:j__ZdlPv_3
call63:j__ZdlPv_2
call64:j__ZdlPv_0
call65:sub_AAC0
call66:sub_A1EC
call67:sub_61DC
call68:sub_6234 <- rc4_enc
call69:sub_A73C
call70:sub_312C
call71:sub_399C
call72:sub_380C <- uncompress
call73:inflateInit_
call74:inflate
call75:inflateEnd
call76:sub_D4D8
call77:sub_4D48
call78:sub_5544
call79:sub_55BC
call80:sub_5C4C
call81:sub_5794
call82:sub_5950
call83:mprotect
call84:__strlen_chk
call85:strncpy
call86:sub_3FAC <- preLinker_image
call87:dlopen
call88:sub_4C7C
call89:sub_4364
call90:sub_4518
call91:sub_3188
call92:dlsym
call93:strcmp
call94:sub_5FB0
call95:sub_5588
call96:sub_6538
call97:sub_8648
call98:sub_4FCC
call99:sub_8774
call100:sub_9068
call101:sub_93F8
call102:sub_8948
call103:interpreter_wrap_int64_t_bridge
call104:sub_A4BC
call105:sub_164F0
call106:puts
call107:_Z9__arm_a_2PcmS_Rii
[Pixel 3::com.example.learn ]-> Process crashed: Bad access due to invalid address

prelink_image <- sub_4D48 <- sub_4EB0

这是我们在ida里交叉引用的结果,再接下来sub_4EB0 可能被 sub_8510sub_918C 调用,我们看上面的程序流程,可以确定是sub_8510

这个函数并不陌生,我们在分析壳elf时就曾找到过这个函数(世界线收束)

跟着函数调用链一处一处的在 IDA 中跳转到相应的地址进行查看,只用关注这两个函数之间的内容

call46:sub_8510
call47:__strncpy_chk2
call48:sub_62DC
call49:sub_6740
call50:sub_4EB0
call51:sub_6324
call52:_ZN9__arm_c_19__arm_c_0Ev
call53:sub_AB0C
call54:sub_A128
call55:sub_A0A0
call56:sub_D808
call57:sub_6680
call58:sub_678C
call59:memcpy
call60:sub_6894
call61:sub_6184
call62:j__ZdlPv_3
call63:j__ZdlPv_2
call64:j__ZdlPv_0
call65:sub_AAC0
call66:sub_A1EC
call67:sub_61DC
call68:sub_6234
call69:sub_A73C
call70:sub_312C
call71:sub_399C
call72:sub_380C
call73:inflateInit_
call74:inflate
call75:inflateEnd
call76:sub_D4D8
call77:sub_4D48
call78:sub_5544
call79:sub_55BC
call80:sub_5C4C
call81:sub_5794
call82:sub_5950
call83:mprotect
call84:__strlen_chk
call85:strncpy
call86:sub_3FAC

然后我们找到了rc4的函数sub_6234 # rc4_encimage-20250614003736784

找到了rc4的加密部分,还要找初始化部分,对其交叉引用image-20250614001559029

image-20250614001616815

这里没有被识别,按P创建函数image-20250614001752368

果然是rc4的init函数,基于对rc4加密的理解,可以确定result就是key,也就是args[0],把它hook出来

function hookRc4() {
var module = Process.findModuleByName("libjiagu_64.so");
Interceptor.attach(module.base.add(0x6064), {
onEnter(args) {
console.log("[+]hooked\n",hexdump(args[0], {length: 0x10, header: true, ansi: true}));
},
onLeave(reval) {
}
});
}
function hook_dlopen() {
Interceptor.attach(Module.findExportByName("libdl.so", "android_dlopen_ext"), {
onEnter: function (args) {
var loadFileName = args[0].readCString();
if (loadFileName.indexOf('libjiagu') != -1) {
this.is_can_hook = true;
}
}, onLeave: function () {
if (this.is_can_hook) {
hookRc4();
}
}
})
}
function main() {
Java.perform(function () {
hook_dlopen();
});
}
setImmediate(main);
[Pixel 3::com.example.learn ]-> [+]hooked
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
74aa7f0630 76 56 57 34 23 91 23 53 56 74 00 00 00 00 00 00 vVW4#.#SVt......
那么rc4的key就是 key = "vVW4#.#SVt" 了

刚刚对rc4_enc还原中也可以发现rc4是魔改了的,也hook rc4_enc的传入传出参数看看

function hookRc4Enc() {
var module = Process.findModuleByName("libjiagu_64.so");
Interceptor.attach(module.base.add(0x6234), {
onEnter: function (args) {
console.log("[+]hook args[0]");
console.log(hexdump(args[0], { offset: 0, length: 0x30, header: true, ansi: true }));
console.log("[+]hook args[1]");
console.log(args[1]);
// 将输入缓冲区保存到this对象,使其在onLeave中可用
this.inputBuffer = args[0];
},
onLeave: function (ret) {
// 从this对象获取保存的输入缓冲区
var inputBuffer = this.inputBuffer;
console.log("[+]hooked return value");
console.log(hexdump(inputBuffer, { offset: 0, length: 0x30, header: true, ansi: true }));
}
});
}

image-20250614005226132image-20250614010838570

image-20250614005256885

我们发现传入的第一个参数是sub_8510的v3[0],也就是qword_3E260数组,第二个参数是sub_8510的v3[1]

而这这个数组的数据就是壳elf填充的,我们解密这一段即可

刚刚分析过程中,知道rc4_enc是魔改过的,且sbox本来是256长度,但他用了257和258,所以我们需要吧sbox hook出来看看(顺便规整这个rc4调用的脚本)

function hookRc4Init() {
var module = Process.findModuleByName("libjiagu_64.so");
Interceptor.attach(module.base.add(0x6064), {
onEnter(args) {
console.log("[+]hook rc4_init args[0]");
console.log(hexdump(args[0], { length: 0x10, header: true, ansi: true }));
},
onLeave(reval) {
}
});
}
function hookRc4Enc() {
var module = Process.findModuleByName("libjiagu_64.so");
Interceptor.attach(module.base.add(0x6234), {
onEnter: function (args) {
console.log("[+]hook rc4_enc args[0]");
console.log(hexdump(args[0], { offset: 0, length: 0x30, header: true, ansi: true }));
console.log("[+]hook rc4_enc args[1]");
console.log(args[1]);
console.log("[+]hook rc4_enc args[2]/sbox");
console.log(hexdump(args[2], { offset: 0, length: 258, header: true, ansi: true }));
// 将输入缓冲区保存到this对象,使其在onLeave中可用
this.inputBuffer = args[0];
},
onLeave: function (ret) {
// 从this对象获取保存的输入缓冲区
var inputBuffer = this.inputBuffer;
console.log("[+]hooked rc4_enc return value");
console.log(hexdump(inputBuffer, { offset: 0, length: 0x30, header: true, ansi: true }));
}
});
}
function hook_dlopen() {
Interceptor.attach(Module.findExportByName("libdl.so", "android_dlopen_ext"), {
onEnter: function (args) {
var loadFileName = args[0].readCString();
if (loadFileName.indexOf('libjiagu') != -1) {
this.is_can_hook = true;
}
}, onLeave: function () {
if (this.is_can_hook) {
hookRc4Init();
hookRc4Enc();
}
}
})
}
function main() {
Java.perform(function () {
hook_dlopen();
});
}
setImmediate(main);
[Pixel 3::com.example.learn ]-> [+]hook rc4_init args[0]
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
74aa7f06d0 76 56 57 34 23 91 23 53 56 74 00 00 00 00 00 00 vVW4#.#SVt......
[+]hook rc4_enc args[0]
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7480496f70 43 83 7f bf a5 a0 33 14 7a 96 6e ef 17 70 a9 59 C.....3.z.n..p.Y
7480496f80 d0 0c 80 56 c5 23 ba 36 87 21 96 26 25 e1 0e c1 ...V.#.6.!.&%...
7480496f90 d1 9b 9e 27 63 bf dd 70 6f 2c e4 f0 55 c3 57 2d ...'c..po,..U.W-
[+]hook rc4_enc args[1]
0xb8090
[+]hook rc4_enc args[2]/sbox
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
754a7f0c70 76 ac 57 5d 84 1a 43 9d fb 5f f8 59 35 9c 05 36 v.W]..C.._.Y5..6
754a7f0c80 cd d1 01 cc 39 49 b6 10 0e 5e 2e 2a 29 7f 72 88 ....9I...^.*).r.
754a7f0c90 9f 13 2c 6f 44 9b 67 4a e0 ee 77 34 97 0b 68 0c ..,oD.gJ..w4..h.
754a7f0ca0 4f cf 8f 95 83 52 ef 78 6a de 09 1d b5 48 a8 a1 O....R.xj....H..
754a7f0cb0 46 85 02 e7 cb 41 b3 3e 71 b9 3b e4 53 c9 73 42 F....A.>q.;.S.sB
754a7f0cc0 e5 30 25 75 f9 df 14 38 ae d2 0d 82 6c 93 6e be .0%u...8....l.n.
754a7f0cd0 5b 20 f3 47 d8 f1 8b 64 b1 ab ad f6 b8 7a 80 4d [ .G...d.....z.M
754a7f0ce0 b7 56 ec b0 66 18 c4 92 33 c8 60 4e 31 d9 5a 03 .V..f...3.`N1.Z.
754a7f0cf0 e6 15 d3 a3 21 a7 1c c1 26 3c 1e 70 bf a2 c5 c3 ....!...&<.p....
754a7f0d00 a0 c2 c0 98 28 89 50 4b 90 6b e1 55 79 7c fd ff ....(.PK.k.Uy|..
754a7f0d10 e3 aa 2b a4 bd 62 2f 16 b4 7e c6 fe 63 da 51 d6 ..+..b/..~..c.Q.
754a7f0d20 32 3a 11 c7 3f 8e d5 ea a5 ba ca ed 08 22 74 5c 2:..?........"t\
754a7f0d30 24 4c 7b bb a9 8d 96 91 1b f2 17 94 45 19 ce 06 $L{.........E...
754a7f0d40 8a 65 37 86 f5 12 9a 69 8c 87 d4 e8 6d eb 58 23 .e7....i....m.X#
754a7f0d50 00 40 1f af 99 dd 04 9e 7d 0a a6 81 f0 f7 3d e9 .@......}.....=.
754a7f0d60 db 0f bc 27 fa e2 fc f4 b2 d0 dc d7 54 07 2d 61 ...'........T.-a
754a7f0d70 03 05 ..
[+]hooked rc4_enc return value
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7480496f70 f9 52 1a 00 78 9c 7c dd 01 d8 a3 ff 3d e7 7b 43 .R..x.|.....=.{C
7480496f80 83 41 30 08 9d 25 18 a4 3c 88 ee ec 8a 76 54 d8 .A0..%..<....vT.
7480496f90 d9 3d b1 66 57 96 41 f0 9c 3d 39 6b 1c 39 bb b3 .=.fW.A..=9k.9..

我们就得到了完整的sbox[258],直接解密的结果很混乱,继续看调用链image-20250614223337921

我们能找到sub_380C,且它调用了下面inflateInit_inflateinflateEnd三个zlib标准库里面的函数,说明这个函数是解压缩函数,即uncompress

然后我们来尝试解密

import zlib
sbox = [
0x76, 0xAC, 0x57, 0x5D, 0x84, 0x1A, 0x43, 0x9D, 0xFB, 0x5F, 0xF8, 0x59, 0x35, 0x9C, 0x05, 0x36,
0xCD, 0xD1, 0x01, 0xCC, 0x39, 0x49, 0xB6, 0x10, 0x0E, 0x5E, 0x2E, 0x2A, 0x29, 0x7F, 0x72, 0x88,
0x9F, 0x13, 0x2C, 0x6F, 0x44, 0x9B, 0x67, 0x4A, 0xE0, 0xEE, 0x77, 0x34, 0x97, 0x0B, 0x68, 0x0C,
0x4F, 0xCF, 0x8F, 0x95, 0x83, 0x52, 0xEF, 0x78, 0x6A, 0xDE, 0x09, 0x1D, 0xB5, 0x48, 0xA8, 0xA1,
0x46, 0x85, 0x02, 0xE7, 0xCB, 0x41, 0xB3, 0x3E, 0x71, 0xB9, 0x3B, 0xE4, 0x53, 0xC9, 0x73, 0x42,
0xE5, 0x30, 0x25, 0x75, 0xF9, 0xDF, 0x14, 0x38, 0xAE, 0xD2, 0x0D, 0x82, 0x6C, 0x93, 0x6E, 0xBE,
0x5B, 0x20, 0xF3, 0x47, 0xD8, 0xF1, 0x8B, 0x64, 0xB1, 0xAB, 0xAD, 0xF6, 0xB8, 0x7A, 0x80, 0x4D,
0xB7, 0x56, 0xEC, 0xB0, 0x66, 0x18, 0xC4, 0x92, 0x33, 0xC8, 0x60, 0x4E, 0x31, 0xD9, 0x5A, 0x03,
0xE6, 0x15, 0xD3, 0xA3, 0x21, 0xA7, 0x1C, 0xC1, 0x26, 0x3C, 0x1E, 0x70, 0xBF, 0xA2, 0xC5, 0xC3,
0xA0, 0xC2, 0xC0, 0x98, 0x28, 0x89, 0x50, 0x4B, 0x90, 0x6B, 0xE1, 0x55, 0x79, 0x7C, 0xFD, 0xFF,
0xE3, 0xAA, 0x2B, 0xA4, 0xBD, 0x62, 0x2F, 0x16, 0xB4, 0x7E, 0xC6, 0xFE, 0x63, 0xDA, 0x51, 0xD6,
0x32, 0x3A, 0x11, 0xC7, 0x3F, 0x8E, 0xD5, 0xEA, 0xA5, 0xBA, 0xCA, 0xED, 0x08, 0x22, 0x74, 0x5C,
0x24, 0x4C, 0x7B, 0xBB, 0xA9, 0x8D, 0x96, 0x91, 0x1B, 0xF2, 0x17, 0x94, 0x45, 0x19, 0xCE, 0x06,
0x8A, 0x65, 0x37, 0x86, 0xF5, 0x12, 0x9A, 0x69, 0x8C, 0x87, 0xD4, 0xE8, 0x6D, 0xEB, 0x58, 0x23,
0x00, 0x40, 0x1F, 0xAF, 0x99, 0xDD, 0x04, 0x9E, 0x7D, 0x0A, 0xA6, 0x81, 0xF0, 0xF7, 0x3D, 0xE9,
0xDB, 0x0F, 0xBC, 0x27, 0xFA, 0xE2, 0xFC, 0xF4, 0xB2, 0xD0, 0xDC, 0xD7, 0x54, 0x07, 0x2D, 0x61,
0x03, 0x05
]
def rc4_decrypt(data):
i = sbox[256] # 0x3
j = sbox[257] # 0x5
out = []
for ch in data:
i = (i + 2) % 256
j = (j + sbox[i] + 1) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
out.append(ch ^ sbox[(sbox[i] + sbox[j]) % 256])
return out
cipherStart = 0x2E260
cipherSize = 0xB8090
with open('/Users/lanzhiqiang/Desktop/360test/protect/assets/libjiagu_fix.so','rb') as f:
wrap_elf = f.read()
# 对密文进行解密
dec_compress_elf = rc4_decrypt(wrap_elf[cipherStart:cipherStart+cipherSize])
dec_elf = zlib.decompress(bytes(dec_compress_elf[4::]))
with open('/Users/lanzhiqiang/Desktop/360test/wrap_elf','wb') as f:
f.write(dec_elf)

image-20250614022048581

但是解密内容肯定是不对的,不过往下翻还是能找到一个elfimage-20250614205101995还需要找其他关键东西

继续看调用链

能看见在sub_55BC里也出现了像样的内容,也有0x38,且这个函数是被sub_4D48所调用的(这个函数同时也调用了preLinker_image,之前分析过)。所以大概率得从这个函数入手分析,从调用链和函数逻辑都合理

call79:sub_55BC
call80:sub_5C4C
call81:sub_5794
call82:sub_5950
call83:mprotect
call84:__strlen_chk
call85:strncpy
call86:sub_3FAC <- preLinker_image

最后的逻辑范围被缩小到了sub_5C4C、sub_5794、sub_5950之中,我们挨个找,能发现在sub_5C4C中有很奇怪的逻辑image-20250614224617847

这里是ARM64 NEON 的两个指令,用于向量操作,参考:

https://developer.arm.com/architectures/instruction-sets/intrinsics/#q=vdupq_n_s8

  • vdupq_n_s8 用于将单个 8 位有符号整数复制到 64 位向量的所有元素
  • veorq_s8 用于对两个 64 位向量的 8 位有符号整数元素进行按位异或运算

image-20250614225507888

相当于上图,0xBD是要异或的值,后面是长度0x150,依次分组。知道了逻辑就可以继续往下推,在上面解密脚本中添加如下:

class part:
def __init__(self):
self.name = ""
self.value = b''
self.offset = 0
self.size = 0
index = 1
extra_part = [part() for _ in range(7)]
seg = ["a", "b", "c", "d"]
v_xor = dec_elf[0]
for i in range(4):
size = int.from_bytes(dec_elf[index:index + 4], 'little')
index += 4
extra_part[i + 1].name = seg[i]
extra_part[i + 1].value = bytes(map(lambda x: x ^ v_xor, dec_elf[index:index + size]))
extra_part[i + 1].size = size
index += size
for p in extra_part:
if p.value != b'':
filename = f"libjiagu.so_{hex(p.size)}_{p.name}"
print(f"[{p.name}] get {filename}, size: {hex(p.size)}")
with open(filename, 'wb') as f:
f.write(p.value)
# [a] get libjiagu.so_0x150_a, size: 0x150
# [b] get libjiagu.so_0x1818_b, size: 0x1818
# [c] get libjiagu.so_0x23be0_c, size: 0x23be0
# [d] get libjiagu.so_0x1b0_d, size: 0x1b0

image-20250614230719552

得到四段,前面我们找过program_header_table有6段,每一段0x38,刚好是0x150,这个a正好满足程序头的大小

然后wrap_elf是由两大部分组成的,wrap_elf中分离出来的4大段数据只是第一部分,长度0x25709,但是我们在wrap_elf找到这个位置image-20250615001348168

这后面还有一个elf,也就是wrap_elf的第二部分内容,我们把两段分开

with open('/Users/lanzhiqiang/Desktop/360test/protect/dumped_main/wrap_elf', 'rb') as f:
wrap_elf = f.read()
ELF_magic = bytes([0x7F, 0x45, 0x4C, 0x46])
for i in range(len(wrap_elf) - len(ELF_magic) + 1):
if wrap_elf[i:i + len(ELF_magic)] == ELF_magic:
print(hex(i))
with open('/Users/lanzhiqiang/Desktop/360test/protect/dumped_main/wrap_elf_part1', 'wb') as f:
f.write(wrap_elf[0:i])
with open('/Users/lanzhiqiang/Desktop/360test/protect/dumped_main/wrap_elf_part2', 'wb') as f:
f.write(wrap_elf[i::])
break

然后我们继续观察,.rela.plt.rela.dyn储存的内容是要远远大于dynamic的,所以我们可以锁定dynamic是d

再加上上面我们分析的

a -> program_header_table
d -> dynamic

一块一块来搞,我们修复的是wrap_elf_part2(主elf)

修复 program header table#

复制 libjiagu.so_0x150_a 的所有字节,然后来到 wrap_elf_part2 中选中 struct program_header_table 粘贴

Mac: command+shift+c/v全复制/粘贴

image-20250615005045617

修复 .dynamic#

program header table(RW_) Dynamic Segmentp_offset 指向 .dynamic 段的位置image-20250615005318863

然后跳转到该位置去修复,同修复program_header_tableimage-20250615005652029

这里来解析一下.dynamic结构

.dynamic 节由多个 Elf64_Dyn 组成,每个结构的大小为 16 字节(64 位)
typedef struct {
Elf64_Sxword d_tag; // 动态表项类型(标记)
union {
Elf64_Xword d_val; // 数值(如标志位、版本号等)
Elf64_Addr d_ptr; // 内存地址(指向其他节或表)
} d_un;
} Elf64_Dyn;

修复重定位表#

我们需要通过 .dynamic 段的 d_tag 字段来直到重定位表的位置

对于我们修复主 ELF 比较重要的 tag

d_tag含义
DT_JMPREL0x17.rela.plt 在文件中的偏移
DT_PLTRELSZ0x2.rela.plt 的大小
DT_RELA0x7.rela.dyn 在文件中的偏移
DT_RELASZ0x8.rela.dyn 的大小

我们可以在 .dynamic 中发现这些 tag 以及对应的值image-20250615010941271

每一个Elf64_Dyn结构大小刚好是16字节,010里面看是一整行,前四个字节代表表项(tag),依次找0x2、0x17、0x7、0x8

就可以知道.rela.plt 在文件中的偏移为0x2C070,大小为0x1818;.rela.dyn 在文件中的偏移0x8490,大小为0x23BE0。这里的值和我们之前分离出的b和c的大小一样,也证明b是.rela.plt,c是.rela.dyn

修复.rela.plt.rela.dyn#

同之前的修复方式,根据找到的偏移和大小去复制粘贴

image-20250615012456543

再把文件基地址设置为0xe7000image-20250615012724218

至此,主elf就修复完了

dex释放分析#

思路跳回到我们hook open函数的结果,同时ida分析刚刚修复好的主elf

[Pixel 3::com.example.learn ]-> [*] Replacing open function
[-] Intercepted read of maps file
[-] Intercepted read of maps file
[-] Intercepted read of maps file
[-] Intercepted read of maps file
[-] Intercepted read of maps file
[-] Intercepted read of maps file
[-] Intercepted read of maps file
[+] Opening dex: /data/data/com.example.learn/.jiagu/classes.dex
6d76864558 is in libjiagu_64.so offset: 0x19f558
6d767fac94 is in libjiagu_64.so offset: 0x135c94
6dfdf9df48 is in libart.so offset: 0x59df48
6dfdca7f2c is in libart.so offset: 0x2a7f2c
6dfdd9497c is in libart.so offset: 0x39497c
70913c4488 is in libc.so offset: 0x42488
6d767f99b4 is in libjiagu_64.so offset: 0x1349b4
6d767f99b4 is in libjiagu_64.so offset: 0x1349b4
6d766dc29c is in libjiagu_64.so offset: 0x1729c
6d766dc29c is in libjiagu_64.so offset: 0x1729c
6d767f99b4 is in libjiagu_64.so offset: 0x1349b4
6d766dbafc is in libjiagu_64.so offset: 0x16afc
6d766d29f8 is in libjiagu_64.so offset: 0xd9f8
6d766da4d8 is in libjiagu_64.so offset: 0x154d8
6d766dae40 is in libjiagu_64.so offset: 0x15e40
[+] Opening dex: /data/data/com.example.learn/.jiagu/classes2.dex
6d76864558 is in libjiagu_64.so offset: 0x19f558
6d767fad7c is in libjiagu_64.so offset: 0x135d7c
6dfdf9df48 is in libart.so offset: 0x59df48
6dfdca7f2c is in libart.so offset: 0x2a7f2c
6dfdd9497c is in libart.so offset: 0x39497c
70913c4488 is in libc.so offset: 0x42488
6d767f99b4 is in libjiagu_64.so offset: 0x1349b4
6d767f99b4 is in libjiagu_64.so offset: 0x1349b4
6d766dc29c is in libjiagu_64.so offset: 0x1729c
6d766dc29c is in libjiagu_64.so offset: 0x1729c
6d767f99b4 is in libjiagu_64.so offset: 0x1349b4
6d766dbafc is in libjiagu_64.so offset: 0x16afc
6d766d29f8 is in libjiagu_64.so offset: 0xd9f8
6d766da4d8 is in libjiagu_64.so offset: 0x154d8
6d766dae40 is in libjiagu_64.so offset: 0x15e40
[+] Opening dex: /data/data/com.example.learn/.jiagu/classes3.dex
6d76864558 is in libjiagu_64.so offset: 0x19f558
6d767fad7c is in libjiagu_64.so offset: 0x135d7c
6dfdf9df48 is in libart.so offset: 0x59df48
6dfdca7f2c is in libart.so offset: 0x2a7f2c
6dfdd9497c is in libart.so offset: 0x39497c
70913c4488 is in libc.so offset: 0x42488
6d767f99b4 is in libjiagu_64.so offset: 0x1349b4
6d767f99b4 is in libjiagu_64.so offset: 0x1349b4
6d766dc29c is in libjiagu_64.so offset: 0x1729c
6d766dc29c is in libjiagu_64.so offset: 0x1729c
6d767f99b4 is in libjiagu_64.so offset: 0x1349b4
6d766dbafc is in libjiagu_64.so offset: 0x16afc
6d766d29f8 is in libjiagu_64.so offset: 0xd9f8
6d766da4d8 is in libjiagu_64.so offset: 0x154d8
6d766dae40 is in libjiagu_64.so offset: 0x15e40
Process crashed: Bad access due to invalid address

image-20250615014242886image-20250615014308698

跳转到0x19F558,很明显的open逻辑,那么这里就是open dex的,继续看下一个0x135d7cimage-20250615014513248

再往下就是调用libart.so里的内容,所以解密一定在0x135d7c附近

原因:当应用需要访问某个类(如通过反射或直接调用)时,ART 会通过FindClass在已加载的类定义中查找。若类定义未被正确解密,ART 将无法解析其结构,导致加载失败。

在这个地址所在函数上下去尝试hook,之前我们hook的是Android_dlopen_ext,但是由于这个是主elf不是使用上面的加载的了,所以我们得改用对dlopen做hook,最后找到sub_193D78image-20250615020715024

image-20250615020948115

这个函数的第二个参数居然就是我们的dex文件,那么我们想办法dump下来就行

function hookDex() {
var base = Process.findModuleByName("libjiagu_64.so").base.add(0x193D78);
var fileIndex = 0
Interceptor.attach(base, {
onEnter: function (args) {
// console.log(hexdump(args[1], {offset: 0, length: 0x30, header: true, ansi: true}));
// console.log(args[2]);
try {
var length = args[2].toInt32();
var data = Memory.readByteArray(args[1], length);
var filePath = "/data/data/com.example.learn/files/" + fileIndex + ".dex";
var file_handle = new File(filePath, "wb");
if (file_handle && file_handle != null) {
file_handle.write(data);
file_handle.flush();
file_handle.close();
console.log("Data written to " + filePath);
fileIndex++;
} else {
console.log("Failed to create file: " + filePath);
}
} catch (e) {
console.log("Error: " + e.message);
}
}, onLeave: function (args) { }
})
}
function hook_dlopne() {
var once = true;
Interceptor.attach(Module.findExportByName(null, "dlopen"), {
onEnter: function (args) {
var loadFileName = args[0].readCString();
if (loadFileName.indexOf('libjiagu') != -1) {
this.is_can_hook = true;
}
}, onLeave: function () {
if (this.is_can_hook && once) {
hookDex();
once = false;
}
}
})
}
function main() {
Java.perform(function () {
hook_dlopne();
});
}
setImmediate(main);

然后我们把这三个dex pull到电脑上

# 1. 将文件复制到公共可访问的/sdcard目录
adb shell su -c "cp /data/data/com.example.learn/files/2.dex /sdcard/2.dex"
# 2. 从/sdcard拉取文件
adb pull /sdcard/2.dex /Users/lanzhiqiang/Desktop/360test/protect/dumped_dex/
# 3. 清理临时文件(可选)
adb shell su -c "rm /sdcard/2.dex"

image-20250615022151554

0.dex即可看到我们最开始自己写的逻辑

至此分析完毕

360加固复现
https://mizuki.mysqil.com/posts/360加固复现/
作者
sh4d0w
发布于
2025-11-06
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时