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>
mt查看如下:


内容如上,输入66才有反应
1、初探
放入jadx,把learn.so放入ida

java层是可以发现一些蛛丝马迹的,比如这里的libjiagu字符,这是360加固的标志性字符
native层倒是不能直接确定加壳方式
java层初步分析
我们在资源目录下找到AndroidManifest.xml,从里面可以得知360加固的入口是com.stub.StubApp
因为com.stub.StubApp是第一个实例化的application,且StubApp的attachBaseContext方法是加固逻辑的第一个执行入口
我们在这个类中,不仅能看见attachBaseContext还能找到onCreate
Application 的
onCreate和attachBaseContext是 Application 的两个回调方法,通常我们会在其中做一些初始化操作,attachBaseContext在onCreate之前执行

分析attachBaseContext,中间的内容明显是被加密了的,我们查看a方法
可以发现其实就是异或16的操作,写脚本解密过后注释上去
- 在 Android 9.0 (API 28) 中,Google 限制了非 SDK 接口的访问。这段代码通过反射绕过该限制,确保加固框架能够正常调用系统隐藏 API
mHiddenApiWarningShown字段控制隐藏 API 警告的显示,设置为true可避免触发警告
下面这些内容就是它会判断手机的架构,针对不同的架构加载不同的Native文件
再往下可以找到DtcLoader初始化的操作,jadx貌似反编译不出来,我们使用jeb
可以发现它调用了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);
这也是我们要分析的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


壳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);
这里可以注意到反复调用了/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);
通过返回结果,即可以确定是调用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找精确调用栈
这里不管是精确还是模糊,都能看出三个dex文件的调用栈基本一致
根据偏移,我们去ida里寻找
填充的一堆数据,目前也不知道有啥用,往下继续翻到
发现被调用了,查看是sub_8510函数
这里的内容像是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- 明显重复多次调用libjiagu_64.so,标准调用一般是不会重复调用的
- 每次加载时解密不同部分的代码,防止一次性获取完整逻辑
- 加固后的库被存储在应用私有目录的隐藏文件夹(
.jiagu)中
这些信息可以确定它是自定义linker
我们在010中对libjiagu_64.so(壳elf)分析可以找到一个新的elf文件,这里大概率就是上面在ida中找到的自定义linker实现加固so后的so文件

我们需要想办法把它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分析

其实是可以看出elf的program header table是被加密了的,ida也无法分析
那我们就需要想办法解密主elf了
壳 elf 在代码中自己实现了解析 ELF 文件的函数,并将解析结果赋值到 soinfo 结构体中,随后调用 dlopen 进行手动加载
soinfo是 Android 系统中用于表示和管理动态链接库 (Shared Object, SO) 的核心结构体,全称为 “Shared Object Information”。它位于 Bionic C 库 (bionic/libc/include/bits/soinfo.h) 中,是动态链接器 (Linker) 的核心数据结构之一
在ida中对dlopen进行交叉引用,查看调用


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

这里基本就是自定义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__-->signedtypedef 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.htypedef 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

但是我们观察一下可以发现,其实360加固里的soinfo是被魔改了的,比如上图中框起来的部分,没有完全修复
向上找调用关系,去看怎么魔改的
在源码中也可以找到类似的地方
所以可以直接说sub_4C7C是register_soinfo_tls函数,往里面找到
这里的0x38,我们在010里看对应关系
恰好程序头大小也是0x38,那么这个方法肯定就是在加载程序头了
其他的比较乱,我们去找程序执行流,再通过上面发现的内容去推
- 推荐oacia大佬的工具https://github.com/oacia/stalker_trace_so
[Pixel 3::com.example.learn ]-> start Stalker!Stalker end!call1:JNI_OnLoadcall2:j_interpreter_wrap_int64_tcall3:interpreter_wrap_int64_tcall4:_Znwmcall5:sub_13F10call6:_Znamcall7:sub_11838call8:memsetcall9:sub_A534call10:sub_E9F8call11:calloccall12:malloccall13:freecall14:sub_EC60call15:_ZdaPvcall16:sub_CF64call17:sub_D41Ccall18:sub_A0E4call19:sub_A0C0call20:sub_D58Ccall21:sub_D150call22:sub_A220call23:sub_16200call24:sub_16978call25:sub_16A44call26:sub_16578call27:sub_17238call28:sub_165F8call29:sub_162D4call30:sub_16240call31:sub_A05Ccall32:sub_D474call33:sub_D670call34:sub_D3BCcall35:sub_99F8call36:dladdrcall37:strstrcall38:setenvcall39:_Z9__arm_a_1P7_JavaVMP7_JNIEnvPvRicall40:sub_A5B4call41:sub_A0F8call42:sub_10F7Ccall43:j__ZdlPv_1call44:_ZdlPvcall45:sub_9E3Ccall46:sub_8510call47:__strncpy_chk2call48:sub_62DCcall49:sub_6740call50:sub_4EB0call51:sub_6324call52:_ZN9__arm_c_19__arm_c_0Evcall53:sub_AB0Ccall54:sub_A128call55:sub_A0A0call56:sub_D808call57:sub_6680call58:sub_678Ccall59:memcpycall60:sub_6894call61:sub_6184call62:j__ZdlPv_3call63:j__ZdlPv_2call64:j__ZdlPv_0call65:sub_AAC0call66:sub_A1ECcall67:sub_61DCcall68:sub_6234 <- rc4_enccall69:sub_A73Ccall70:sub_312Ccall71:sub_399Ccall72:sub_380C <- uncompresscall73:inflateInit_call74:inflatecall75:inflateEndcall76:sub_D4D8call77:sub_4D48call78:sub_5544call79:sub_55BCcall80:sub_5C4Ccall81:sub_5794call82:sub_5950call83:mprotectcall84:__strlen_chkcall85:strncpycall86:sub_3FAC <- preLinker_imagecall87:dlopencall88:sub_4C7Ccall89:sub_4364call90:sub_4518call91:sub_3188call92:dlsymcall93:strcmpcall94:sub_5FB0call95:sub_5588call96:sub_6538call97:sub_8648call98:sub_4FCCcall99:sub_8774call100:sub_9068call101:sub_93F8call102:sub_8948call103:interpreter_wrap_int64_t_bridgecall104:sub_A4BCcall105:sub_164F0call106:putscall107:_Z9__arm_a_2PcmS_Rii[Pixel 3::com.example.learn ]-> Process crashed: Bad access due to invalid addressprelink_image <- sub_4D48 <- sub_4EB0
这是我们在ida里交叉引用的结果,再接下来sub_4EB0 可能被 sub_8510 或 sub_918C 调用,我们看上面的程序流程,可以确定是sub_8510
这个函数并不陌生,我们在分析壳elf时就曾找到过这个函数(世界线收束)
跟着函数调用链一处一处的在 IDA 中跳转到相应的地址进行查看,只用关注这两个函数之间的内容
call46:sub_8510call47:__strncpy_chk2call48:sub_62DCcall49:sub_6740call50:sub_4EB0call51:sub_6324call52:_ZN9__arm_c_19__arm_c_0Evcall53:sub_AB0Ccall54:sub_A128call55:sub_A0A0call56:sub_D808call57:sub_6680call58:sub_678Ccall59:memcpycall60:sub_6894call61:sub_6184call62:j__ZdlPv_3call63:j__ZdlPv_2call64:j__ZdlPv_0call65:sub_AAC0call66:sub_A1ECcall67:sub_61DCcall68:sub_6234call69:sub_A73Ccall70:sub_312Ccall71:sub_399Ccall72:sub_380Ccall73:inflateInit_call74:inflatecall75:inflateEndcall76:sub_D4D8call77:sub_4D48call78:sub_5544call79:sub_55BCcall80:sub_5C4Ccall81:sub_5794call82:sub_5950call83:mprotectcall84:__strlen_chkcall85:strncpycall86:sub_3FAC然后我们找到了rc4的函数sub_6234 # rc4_enc
找到了rc4的加密部分,还要找初始化部分,对其交叉引用

这里没有被识别,按P创建函数
果然是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 0123456789ABCDEF74aa7f0630 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 })); } });}


我们发现传入的第一个参数是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 0123456789ABCDEF74aa7f06d0 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 0123456789ABCDEF7480496f70 43 83 7f bf a5 a0 33 14 7a 96 6e ef 17 70 a9 59 C.....3.z.n..p.Y7480496f80 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 0123456789ABCDEF754a7f0c70 76 ac 57 5d 84 1a 43 9d fb 5f f8 59 35 9c 05 36 v.W]..C.._.Y5..6754a7f0c80 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.sB754a7f0cc0 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.M754a7f0ce0 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.-a754a7f0d70 03 05 ..[+]hooked rc4_enc return value 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF7480496f70 f9 52 1a 00 78 9c 7c dd 01 d8 a3 ff 3d e7 7b 43 .R..x.|.....=.{C7480496f80 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],直接解密的结果很混乱,继续看调用链
我们能找到sub_380C,且它调用了下面inflateInit_、inflate、inflateEnd三个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 = 0x2E260cipherSize = 0xB8090with 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)
但是解密内容肯定是不对的,不过往下翻还是能找到一个elf
还需要找其他关键东西
继续看调用链
能看见在sub_55BC里也出现了像样的内容,也有0x38,且这个函数是被sub_4D48所调用的(这个函数同时也调用了preLinker_image,之前分析过)。所以大概率得从这个函数入手分析,从调用链和函数逻辑都合理
call79:sub_55BCcall80:sub_5C4Ccall81:sub_5794call82:sub_5950call83:mprotectcall84:__strlen_chkcall85:strncpycall86:sub_3FAC <- preLinker_image最后的逻辑范围被缩小到了sub_5C4C、sub_5794、sub_5950之中,我们挨个找,能发现在sub_5C4C中有很奇怪的逻辑
这里是ARM64 NEON 的两个指令,用于向量操作,参考:
https://developer.arm.com/architectures/instruction-sets/intrinsics/#q=vdupq_n_s8
vdupq_n_s8用于将单个 8 位有符号整数复制到 64 位向量的所有元素veorq_s8用于对两个 64 位向量的 8 位有符号整数元素进行按位异或运算

相当于上图,0xBD是要异或的值,后面是长度0x150,依次分组。知道了逻辑就可以继续往下推,在上面解密脚本中添加如下:
class part: def __init__(self): self.name = "" self.value = b'' self.offset = 0 self.size = 0
index = 1extra_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 += sizefor 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
得到四段,前面我们找过program_header_table有6段,每一段0x38,刚好是0x150,这个a正好满足程序头的大小
然后wrap_elf是由两大部分组成的,wrap_elf中分离出来的4大段数据只是第一部分,长度0x25709,但是我们在wrap_elf找到这个位置
这后面还有一个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_tabled -> dynamic一块一块来搞,我们修复的是wrap_elf_part2(主elf)
修复 program header table
复制 libjiagu.so_0x150_a 的所有字节,然后来到 wrap_elf_part2 中选中 struct program_header_table 粘贴
Mac:
command+shift+c/v全复制/粘贴

修复 .dynamic
program header table 的 (RW_) Dynamic Segment 的 p_offset 指向 .dynamic 段的位置
然后跳转到该位置去修复,同修复program_header_table
这里来解析一下.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_JMPREL | 0x17 | .rela.plt 在文件中的偏移 |
| DT_PLTRELSZ | 0x2 | .rela.plt 的大小 |
| DT_RELA | 0x7 | .rela.dyn 在文件中的偏移 |
| DT_RELASZ | 0x8 | .rela.dyn 的大小 |
我们可以在 .dynamic 中发现这些 tag 以及对应的值
每一个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
同之前的修复方式,根据找到的偏移和大小去复制粘贴

再把文件基地址设置为0xe7000
至此,主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.dex6d76864558 is in libjiagu_64.so offset: 0x19f5586d767fac94 is in libjiagu_64.so offset: 0x135c946dfdf9df48 is in libart.so offset: 0x59df486dfdca7f2c is in libart.so offset: 0x2a7f2c6dfdd9497c is in libart.so offset: 0x39497c70913c4488 is in libc.so offset: 0x424886d767f99b4 is in libjiagu_64.so offset: 0x1349b46d767f99b4 is in libjiagu_64.so offset: 0x1349b46d766dc29c is in libjiagu_64.so offset: 0x1729c6d766dc29c is in libjiagu_64.so offset: 0x1729c6d767f99b4 is in libjiagu_64.so offset: 0x1349b46d766dbafc is in libjiagu_64.so offset: 0x16afc6d766d29f8 is in libjiagu_64.so offset: 0xd9f86d766da4d8 is in libjiagu_64.so offset: 0x154d86d766dae40 is in libjiagu_64.so offset: 0x15e40[+] Opening dex: /data/data/com.example.learn/.jiagu/classes2.dex6d76864558 is in libjiagu_64.so offset: 0x19f5586d767fad7c is in libjiagu_64.so offset: 0x135d7c6dfdf9df48 is in libart.so offset: 0x59df486dfdca7f2c is in libart.so offset: 0x2a7f2c6dfdd9497c is in libart.so offset: 0x39497c70913c4488 is in libc.so offset: 0x424886d767f99b4 is in libjiagu_64.so offset: 0x1349b46d767f99b4 is in libjiagu_64.so offset: 0x1349b46d766dc29c is in libjiagu_64.so offset: 0x1729c6d766dc29c is in libjiagu_64.so offset: 0x1729c6d767f99b4 is in libjiagu_64.so offset: 0x1349b46d766dbafc is in libjiagu_64.so offset: 0x16afc6d766d29f8 is in libjiagu_64.so offset: 0xd9f86d766da4d8 is in libjiagu_64.so offset: 0x154d86d766dae40 is in libjiagu_64.so offset: 0x15e40[+] Opening dex: /data/data/com.example.learn/.jiagu/classes3.dex6d76864558 is in libjiagu_64.so offset: 0x19f5586d767fad7c is in libjiagu_64.so offset: 0x135d7c6dfdf9df48 is in libart.so offset: 0x59df486dfdca7f2c is in libart.so offset: 0x2a7f2c6dfdd9497c is in libart.so offset: 0x39497c70913c4488 is in libc.so offset: 0x424886d767f99b4 is in libjiagu_64.so offset: 0x1349b46d767f99b4 is in libjiagu_64.so offset: 0x1349b46d766dc29c is in libjiagu_64.so offset: 0x1729c6d766dc29c is in libjiagu_64.so offset: 0x1729c6d767f99b4 is in libjiagu_64.so offset: 0x1349b46d766dbafc is in libjiagu_64.so offset: 0x16afc6d766d29f8 is in libjiagu_64.so offset: 0xd9f86d766da4d8 is in libjiagu_64.so offset: 0x154d86d766dae40 is in libjiagu_64.so offset: 0x15e40Process crashed: Bad access due to invalid address

跳转到0x19F558,很明显的open逻辑,那么这里就是open dex的,继续看下一个0x135d7c
再往下就是调用libart.so里的内容,所以解密一定在0x135d7c附近
原因:当应用需要访问某个类(如通过反射或直接调用)时,ART 会通过
FindClass在已加载的类定义中查找。若类定义未被正确解密,ART 将无法解析其结构,导致加载失败。
在这个地址所在函数上下去尝试hook,之前我们hook的是Android_dlopen_ext,但是由于这个是主elf不是使用上面的加载的了,所以我们得改用对dlopen做hook,最后找到sub_193D78

这个函数的第二个参数居然就是我们的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"
0.dex即可看到我们最开始自己写的逻辑
至此分析完毕
部分信息可能已经过时