360数字加固解析
加固特征:
application 的第一入口是android:name="com.stub.StubApp"
Application 的 onCreate 和 attachBaseContext 是 Application 的两个回调方法,通常我们会在其中做一些初始化操作,attachBaseContext 在 onCreate 之前执行
)[img=110,0]https://attach.52pojie.cn//forum/202412/13/162847q3z2i45j9ezm228z.png?l[/img]
a.b():
)[img=110,0]https://attach.52pojie.cn//forum/202412/13/162903lg4g5cxxsirozxh2.png?l[/img]
这里其实是有对应的字符串加密的,在对应的com.qihoo.util.a里面有着对应的解密函数,应该是在执行过程中对于字符串进行动态解密的
这里通过jeb的脚本功能,去实现指令级别的脚本编写,当函数去调用这个解密函数时提前把字符串进行解密的同时直接进行赋值
JEB脚本:
from com.pnfsoftware.jeb.client.api import IScript, IconType, ButtonGroupType
from com.pnfsoftware.jeb.core import RuntimeProjectUtil
from com.pnfsoftware.jeb.core.units.code.java import IJavaSourceUnit
from com.pnfsoftware.jeb.core.units.code import ICodeUnit, ICodeItem
from com.pnfsoftware.jeb.core.output.text import ITextDocument
from com.pnfsoftware.jeb.core.units.code.java import IJavaSourceUnit, IJavaStaticField, IJavaNewArray, IJavaConstant, IJavaCall, IJavaField, IJavaMethod, IJavaClass
from com.pnfsoftware.jeb.core.events import JebEvent, J
from com.pnfsoftware.jeb.core.util import DecompilerHelper
methodName = ['Lcom/qihoo/util/a;', 'a']
class dec_str_360jiagu(IScript):
def run(self, ctx):
print('start deal with strings')
self.ctx = ctx
engctx = ctx.getEnginesContext()
if not engctx:
print('Back end engines not initialized')
return
projects = engctx.getProjects()
if not projects:
print('There is no opened project')
return
units = RuntimeProjectUtil.findUnitsByType(projects[0], IJavaSourceUnit, False)
for unit in units:
javaClass = unit.getClassElement()
print('[+] decrypt:' + javaClass.getName())
self.cstbuilder = unit.getFactories().getConstantFactory()
self.processClass(javaClass)
unit.notifyListeners(JebEvent(J.UnitChange))
print('Done.')
def processClass(self, javaClass):
if javaClass.getName() == methodName[0]:
return
for method in javaClass.getMethods(): #遍历类方法
block = method.getBody()#代码块
i = 0
while i < block.size():
stm = block.get(i)#代码
self.checkElement(block, stm)
i += 1
/*block.get(i): block.get(i) 根据索引 i 返回 block 中的第 i 条指令。这些指令可能是:
IJavaCall:方法调用指令。
IJavaAssignment:赋值指令。
IJavaCondition:条件判断(例如 if 语句)指令。
IJavaLoop:循环(例如 while、for 等)指令。
IJavaReturn:返回指令。 */
def checkElement(self, parent, e):
try:
if isinstance(e, IJavaCall):#假如这里的指令是IJavaCall,那么它就有getMethod()方法了,这个方法:它返回的是与此调用相关的 IJavaMethod 对象(即表示被调用方法的对象)
mmethod = e.getMethod()
mname = mmethod.getName()# 例如 someMethod
msig = mmethod.getSignature() #例如 someMethod(int, String)
if mname == methodName[1] and methodName[0] in msig: #函数名是a,对应类是Lcom/qihoo/util/a;
v = []
for arg in e.getArguments():
if isinstance(arg, IJavaConstant): #IJavaConstant是字面量,返回的是arg是不是常量的Boolean值
v.append(arg.getString())
if len(v) == 1:
decstr = self.decryptstring(v[0]) #解密
parent.replaceSubElement(e, self.cstbuilder.createString(decstr)) #直接把需要调用解密代码的字符串进行解密,然后修改调用函数,直接替换成解密之后的字符串
for subelt in e.getSubElements():
if isinstance(subelt, IJavaClass) or isinstance(subelt, IJavaField) or isinstance(subelt, IJavaMethod):
continue
self.checkElement(e, subelt) #递归调用,上面在判定是否当前这个函数有子函数,有就进行递归处理
except:
print('error')
def decryptstring(self, string):
src = []
for index, char in enumerate(string):
src.append(chr(ord(char) ^ 16))
return ''.join(src).decode('unicode_escape')
通过这个JEB脚本已经可以把上面的字符串来进行解密了,但是我试过了虽然已经打印了
,但是还是没用,修改
了JEB代码也没用,于是我去下载了JEB5.18版本,这个最新版本可以直接把字符串混淆除去了。
之后就是看attachBaseContext类了,这里是能够控制执行的早于JNI_OnLoad的位置
这里去判定之后去实现了加载不同的so文件
我们直接去看对应的libjiagu_a64.so——>对应的应该是arm64的so
正常情况下应该去看导出表和导入表,看看最可能出现的关键函数,但是这里的导出表和导入表是空的,那么就说明这里不是通过静态链接来到,而是通过动态加载时确定的导出表和导入表的函数地址的,所以我们要dump下在执行过程中的so文件![image-20241212115609375])
这里的dump_so和脱壳的原理是一样的,通过去找到对应的文件起始地址和文件的大小来进行对于dump文件的写入
dump_so:
通过遍历module去确定是dlopen加载的,通过获取对应的so文件的相关信息并进行dump
function Hook_dlopen(target, soName, callback) {
Interceptor.attach(target, {
onEnter: function (args) {
var pathptr = args[0]; // 获取库路径参数,android_dlopen_ext("libjiagu_64.so") or dlopen("libjiagu_64.so")
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
console.log("load " + path);//同时把这个加载了so的android_dlopen_ext函数的so的路径打印下来
if (path.indexOf(soName) >= 0) {
this.is_can_hook = true;
// 如果库路径中包含目标库名,执行回调函数
// callback(path);
}
}
},
onLeave: function (retval) {
if(this.is_can_hook) {
callback("libjiagu_64.so");
}
// 这里可以处理函数返回值
}
});
}
var dlopen = Module.findExportByName(null, "dlopen");
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
Hook_dlopen(dlopen,"libjiagu_64.so",dump_so)
Hook_dlopen(android_dlopen_ext,"libjiagu_64.so",dump_so)
function dump_so(so_name) {
// 获取加载的模块信息
var libso = Process.getModuleByName(so_name);
console.log("[name]:", libso.name);
console.log("[base]:", libso.base);
console.log("[size]:", ptr(libso.size));
console.log("[path]:", libso.path);
// 构造文件路径
var file_path = "/data/data/com.dytool/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
var file_handle = new File(file_path, "wb");
// 如果文件句柄有效,进行内存保护并导出文件
if (file_handle && file_handle != null) {
Memory.protect(ptr(libso.base), libso.size, 'rwx'); // 设置内存区域的可读写执行权限
var libso_buffer = ptr(libso.base).readByteArray(libso.size); // 读取内存内容
file_handle.write(libso_buffer); // 写入文件
file_handle.flush(); // 刷新文件缓冲
file_handle.close(); // 关闭文件
console.log("[dump]:", file_path); // 输出文件保存路径
}
}
同时这里需要算法转发,把frida中对应的获取的sodump下的数据转发到本机上面进行
adb shell
./data/local/tmp/fsarm64 -l 0.0.0.0:1234
adb forward tcp:1234 tcp:1234
frida -H 127.0.0.1:1234 -l .\hook.js -f "com.oacia.apk_protect"
之后用SOFixer进行修复,这里这个APP已经挂了,已经连不上去了,所以这里的sodump不了东西了
frida反调试:
这里面有关于frida的反调试的信息,其中的主要的反调试检测其实是对于frida的部分检测了,我们通过在dlopen加载了对应的libjiagu_a64.so的so中去查看对应可能出现的open函数,查看
function my_hook_dlopen(soName = '') {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
if (path.indexOf(soName) >= 0) {
this.is_can_hook = true;
}
}
},
onLeave: function (retval) {
if (this.is_can_hook) {
hook_open();
}
}});
}
function hook_open(){
var pth = Module.findExportByName(null,"open");
Interceptor.attach(ptr(pth),{
onEnter:function(args){
this.filename = args[0];
console.log("",this.filename.readCString())
},
onLeave:function(retval){
}})
}
这里就可以返回对应的open的参数是啥了
open /proc/self/maps
绕过检测/proc/self/maps
首先是获取正常的,在没有进行frida注入的maps,因为这里是需要对应的程序的maps来进行相对数据的获取的,所以需要一个正常的maps,然后我们进行对应open函数参数的重定位,实现利用open函数来读取正常的maps文件
cp /proc/self/maps /data/data/com.oacia.apk_protect/maps
重定向frida脚本
function hoook_proc_self_maps()
{
const openPtr = Module.findExportByName(null,"open");
const open_old = new NativeFunction(openPtr,'int',['pointer', 'int']); //创建一个原函数指针,用于之后返回原函数
var fakePath = "/data/data/com.oacia.apk_protect/maps"; //设置对应的fakepath
Interceptor.replace(openPtr,new NativeCallback(function(pathnameptr,flag){
var pathname = Memory.readUtf8String(pathnameptr);
console.log("open:",pathname);
if(pathname.indexOf("maps") >= 0){
console.log("find",pathname,",redirect to",fakePath);
var filename = Memory.allocUtf8String(fakePath);
return open_old(filename,flag); //设置我们cp下的maps
}else{
return open_old(pathname,flag);//原函数
}
}, 'int', ['pointer', 'int']));
}
这里进行了重定向之后发送了闪退,应该是由于在执行过由于我们给定的是原本在正常没有进行frida注入时候的maps,这里可能是因为程序对于maps不仅仅只是简单的数据对比,字符串对比,也有可能进行了对于maps数据的读取,而可能原本的maps和当前需要使用的在内存映射上不同,或者是之前的maps对于映射的内存在执行完成之后就会关闭等等的可能....
但是这样的同时,由于数据的提取的不正常而导致直接进行了程序的退出,结果连里面的dex文件也没有进行删除,这里应该是开发者的失误,正这样的结果直接可以让我们找到对应是在哪使用open函数读取的dex文件了
当前我们需要确定的就是这些dex文件是在哪个so中被加载使用的。那么我们就要去定位到对应的so文件中去
如何去实现so文件的定位,也就是在android_dlopen_ext("libcjiagu_a64.so")的时候,看看open("classes.dex")这个行为是在哪个so中进行的,所以我们要去确定当前的addr,如何通过枚举每个module,来比对这个addr是否在这个module之间来确定,对应的so文件
// 检查地址是否位于某个模块内
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.sub(process_Obj_Module_Arr[i].base)).toString(16));
}
}
}
// Hook open 函数,拦截 maps 文件的打开请求
function hook_proc_self_maps() {
const openPtr = Module.getExportByName(null, 'open');
const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
var fakePath = "/data/data/com.oacia.apk_protect/maps_nonexistent"; // 模拟的路径
// 替换 open 函数
Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
var pathname = Memory.readUtf8String(pathnameptr);
console.log("open", pathname);
// 如果请求的文件路径中包含 "maps",则重定向
if (pathname.indexOf("maps") >= 0) {
console.log("find", pathname + ", redirect to", fakePath);
var filename = Memory.allocUtf8String(fakePath);
return open(filename, flag);
}
// 如果请求的文件路径中包含 "dex",打印堆栈并检查地址
if (pathname.indexOf("dex") >= 0) {
Thread.backtrace(this.context, Backtracer.FUZZY).map(addr_in_so(openPtr));
}
// 继续调用原始的 open 函数
var fd = open(pathnameptr, flag);
return fd;
}, 'int', ['pointer', 'int']));
}
// Hook android_dlopen_ext 函数,在加载特定 SO 文件时执行额外操作
function my_hook_dlopen(soName = '') {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
if (path.indexOf(soName) >= 0) {
this.is_can_hook = true;
}
}
},
onLeave: function (retval) {
if (this.is_can_hook) {
hook_proc_self_maps();
}
}
});
}
这样就可以去打印到在对应module的时候的so文件了
可以发现的是这些dex文件所定位到到的so全是相同的位置,同时在classes3.dex的时候前半部分也是一样的,同时由于可能数据的错误直接导致中断了,所以我们有利用去怀疑这里的dex文件的加载其实是写的循环,通过循环去加载了所以的dex文件
同时我们去访问了一下对应的libjiagu_64.so中上述偏移offset的位置
毫无例外,全是这种数据,%1沾满了很长一段的地址空间。我们知道的是%1的结果都是0,所以其实这里全是0,0,0................
那这里又开辟了这么一长串的空间,里面的值全是0,那么大概率的可能就是用来之后在动态加载的时候填充数据的了。
于是这里就在动态加载的时候,在加载到open classes.dex之后把当前这里的地址给dump下来好了,原理和之前的dump_so也是一样的
// Dump SO 文件到指定路径
function dump_so(so_name) {
var libso = Process.getModuleByName(so_name); // 获取指定的 SO 模块
console.log("[name]:", libso.name); // 打印 SO 名称
console.log("[base]:", libso.base); // 打印 SO 模块的基地址
console.log("[size]:", ptr(libso.size)); // 打印 SO 模块的大小
console.log("[path]:", libso.path); // 打印 SO 文件路径
// 创建用于保存 dump 的文件路径
var file_path = "/data/data/com.oacia.apk_protect/" + libso.name + "_" +
libso.base + "_" + ptr(libso.size) + ".so";
var file_handle = new File(file_path, "wb"); // 打开文件以写入二进制数据
if (file_handle && file_handle != null) {
// 修改内存保护,确保能够读写内存
Memory.protect(ptr(libso.base), libso.size, 'rwx');
// 读取 SO 模块的字节数组
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); // 输出 dump 的文件路径
}
}
var dump_once = false; // 用于确保只在第一次打开 dex 时进行 dump
// Hook open 函数,拦截对 maps 和 dex 文件的访问
function hook_proc_self_maps() {
const openPtr = Module.getExportByName(null, 'open');
const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
var fakePath = "/data/data/com.oacia.apk_protect/maps_nonexistent"; // 模拟的 maps 文件路径
// 替换 open 函数,拦截文件操作
Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
var pathname = Memory.readUtf8String(pathnameptr); // 读取文件路径
console.log("open", pathname); // 打印打开的文件路径
// 如果是访问 maps 文件,重定向到 fakePath
if (pathname.indexOf("maps") >= 0) {
console.log("find", pathname + ", redirect to", fakePath);
var filename = Memory.allocUtf8String(fakePath);
return open(filename, flag); // 返回模拟的文件路径
}
// 如果是访问 dex 文件,进行 dump 操作(只在第一次打开时进行 dump)
if (pathname.indexOf("dex") >= 0) {
if (!dump_once) {
dump_once = true; // 确保只 dump 一次
dump_so("libjiagu_64.so"); // 执行 dump 操作
}
}
// 调用原始的 open 函数
var fd = open(pathnameptr, flag);
return fd;
}, 'int', ['pointer', 'int']));
}
}
同时进行SOFixer的修复
这时候的对应偏移的地址就已经被填充上了对应的数据了
然后我们通过比对两个填充数据之后的区别来看看到达是多了个啥子
可以通过直接去对比HEX数据(WinMerge,010editor),也就是去利用bindiff看看函数之间的差距值
![
)
其实这里填充的是一个新的ELF文件,因为这里文件头都已经有了,所以说这里是在libjiagu_64.so中去添加了一个so文件,有点逆天了
之后就是把这里的文件提取出来了
# 打开源文件并读取其内容
with open('libjiagu_64.so_0x7a69829000_0x274000_open_classes.dex.so', 'rb') as f:
s = f.read() # 读取文件内容
# 从偏移 0xe7000 开始将数据写入目标文件
with open('libjiagu_0xe7000.so', 'wb') as f:
f.write(s[0xe7000:]) # 写入从偏移 0xe7000 开始的数据
但是这里提取出来的ELF文件之后的Program header tabel是加密过程的
这里的文件里面给出了一个特殊的加固的方式自实现linker 加固 so
linker 加固 so
自实现Linker 加固 SO 是一种通过定制或替换系统动态链接器(如 ld-linux.so.2 或 ld-linux-armhf.so.3 等)的行为,来增加对动态共享对象(SO 文件)保护的技术。在这种加固技术中,加固工具会创建一个“壳”ELF 文件,这个壳 ELF 文件在其自身的代码中实现了一个私有的 Linker。这个私有 Linker 的功能包括解析 ELF 文件的结构、处理加密的 Program Header、以及执行各种加固和保护措施。然后,它会解密主要的 ELF 文件(主 ELF)中的程序头部或其他部分,将解密后的内容加载到内存中,最后可能通过调用`dlopen`等函数手动加载解密后的动态共享对象。这种方法的优点是能够提供额外的保护层次,防止未经授权的分析和修改。由于Program Header 等关键信息被加密,传统的工具和方法很难直接分析或执行加固后的程序,增加了破解的难度。
这里通过了dlopen进行的交叉引用来实现查看相关调用的方法
找到了第二个dlopen
复现上面说看到了类似于AOSP源码中对于Linker的相关代码
这里确实是没有这样细致的分析过AOSP的全部源码,所以确实是看不出来
既然是自定义的Linker那么就是同样需要soinfo的结构体
soinfo 结构体是 Android 和 Linux 系统中在动态链接库加载过程中使用的一个结构体,特别是在解析、加载、链接和卸载共享库时。它通常出现在 linker 的内部,表示一个共享库(如 .so 文件)的信息。soinfo 是 Android 系统中链接器(linker)的一个关键结构,用于存储有关共享库的各种信息,如符号表、重定位信息、动态节(Section)等。
所以需要导入对应的结构体
// 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__"],},
}
*/
// 定义 ELF 文件的基本类型,根据架构决定使用 ELF32 或 ELF64 类型
#if defined(__LP64__)
#define ElfW(type) Elf64_ ## type
#else
#define ElfW(type) Elf32_ ## type
#endif
// 32 位和 64 位重定位和符号表数据结构
// Elf32 和 Elf64 基本类型
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;
// 32 位 ELF 基本类型
typedef __u32 Elf32_Addr;
typedef __u16 Elf32_Half;
typedef __u32 Elf32_Off;
typedef __s32 Elf32_Sword;
typedef __u32 Elf32_Word;
// 64 位 ELF 基本类型
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;
// 动态段数据结构(Elf32 和 Elf64)
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;
// 重定位数据结构(Elf32 和 Elf64)
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;
// 符号表数据结构(Elf32 和 Elf64)
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;
// ELF 文件头数据结构(Elf32 和 Elf64)
#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;
// 程序头数据结构(Elf32 和 Elf64)
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;
// 节头数据结构(Elf32 和 Elf64)
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 特有)
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
// Android 中的 soinfo 结构体,用于表示动态链接库信息
struct soinfo {
#if defined(__work_around_b_24465209__)
char old_name_[SOINFO_NAME_LEN];
#endif
const ElfW(Phdr)* phdr;
size_t phnum;
ElfW(Addr) base;
size_t size;
ElfW(Dyn)* dynamic;
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_;
size_t ref_count_;
link_map link_map_head;
bool constructors_called;
ElfW(Addr) load_bias;
bool has_text_relocations;
bool has_DT_SYMBOLIC;
};
但是当把这个结构体传入之后,其实是有点不合的,那么就说明这个自定义的Linker所使用的soinfo结构体也是被修改了的
那么我们就要去找到对应这里被修改的地方到底是怎么样的,在实际的情况下我们也许会有花费大量的时间去寻找,但是现在比较是踩在巨人的肩膀上的,这里已经被找到了
这里的0x38是program header table中的phentsize,代表的是程序头中一个条目的大小。而程序头又是多个条目组成的,这里举例一下条目,v5是程序头的偏移地址,v6的条目的偏移地址
程序头条目 1(加载段)
Elf64_Phdr phdr1 = {
.p_type = PT_LOAD, // 程序段类型:PT_LOAD 表示加载段
.p_flags = PF_R | PF_W, // 可读、可写
.p_offset = 0x1000, // 在 ELF 文件中的偏移量
.p_vaddr = 0x400000, // 程序段加载到内存中的虚拟地址
.p_paddr = 0x0, // 物理地址(一般为 0)
.p_filesz = 0x2000, // 文件中的大小
.p_memsz = 0x3000, // 内存中的大小(包括额外的空间)
.p_align = 0x1000 // 对齐要求:0x1000 字节对齐
};
程序头条目 2(动态段)
Elf64_Phdr phdr2 = {
.p_type = PT_DYNAMIC, // 程序段类型:PT_DYNAMIC 表示动态段
.p_flags = PF_R, // 可读
.p_offset = 0x3000, // 文件中的偏移量
.p_vaddr = 0x500000, // 程序段加载到内存中的虚拟地址
.p_paddr = 0x0, // 物理地址(一般为 0)
.p_filesz = 0x500, // 文件中的大小
.p_memsz = 0x1000, // 内存中的大小
.p_align = 0x1000 // 对齐要求:0x1000 字节对齐
};
程序头条目 3(堆栈段)
Elf64_Phdr phdr3 = {
.p_type = PT_STACK, // 程序段类型:PT_STACK 表示堆栈段
.p_flags = PF_R | PF_W, // 可读、可写
.p_offset = 0x0, // 文件中的偏移量(通常为 0,因为堆栈段不存储在文件中)
.p_vaddr = 0x7fffffff, // 程序段加载到内存中的虚拟地址
.p_paddr = 0x0, // 物理地址(一般为 0)
.p_filesz = 0x0, // 文件中的大小(通常为 0,因为堆栈段不存储在文件中)
.p_memsz = 0x10000, // 内存中的大小
.p_align = 0x1000 // 对齐要求:0x1000 字节对齐
};
而每个条目的偏移的计算公式又是这样的:
程序头表偏移量 (e_phoff)
程序头条目的大小 (e_phentsize)
程序头条目数量 (e_phnum)
第一个程序头条目的偏移量为 e_phoff + 0 * e_phentsize,即 0x40 + 0 * 0x38 = 0x40,表示第一个程序头条目位于文件偏移 0x40。
第二个程序头条目的偏移量为 e_phoff + 1 * e_phentsize,即 0x40 + 1 * 0x38 = 0x78,表示第二个程序头条目位于文件偏移 0x78。
第三个程序头条目的偏移量为 e_phoff + 2 * e_phentsize,即 0x40 + 2 * 0x38 = 0xB0,表示第三个程序头条目位于文件偏移 0xB0。
也就是程序头的偏移加上对应条目的数目*条目的大小,和上面那个一比对,就会知道
v5=程序头表偏移量 (e_phoff)
0x38=程序头条目的大小 (e_phentsize)
a2=程序头条目数量 (e_phnum)
这里我们去HOOK一下这里的位置看一下,顺便可以看看对应传入的结构体soinfo到底应该是怎么样的
function hook_5E6C(){
var module = Process.findModuleByName("libjiagu_64.so");
Interceptor.attach(module.base.add(0x5E6C), {
// fd, buff, len
onEnter: function (args) {
console.log(hexdump(args[0], {
offset: 0,// 相对偏移
length: 0x38*0x6+0x20,//dump 的大小 6个0x38大小的条目+偏移
header: true,
ansi: true
}));
console.log(args[1])
console.log(args[2])
console.log(`base = ${module.base}`)
},
onLeave: function (ret) {
}
});
}