360数字加固解析
加固特征:
1 | application 的第一入口是android:name = "com.stub.StubApp"
|
Application 的 onCreate 和 attachBaseContext 是 Application 的两个回调方法,通常我们会在其中做一些初始化操作,attachBaseContext 在 onCreate 之前执行
a.b():
这里其实是有对应的字符串加密的,在对应的com.qihoo.util.a里面有着对应的解密函数,应该是在执行过程中对于字符串进行动态解密的
这里通过jeb的脚本功能,去实现指令级别的脚本编写,当函数去调用这个解密函数时提前把字符串进行解密的同时直接进行赋值
JEB脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | 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):
mmethod = e.getMethod()
mname = mmethod.getName()
msig = mmethod.getSignature()
if mname = = methodName[ 1 ] and methodName[ 0 ] in msig:
v = []
for arg in e.getArguments():
if isinstance (arg, IJavaConstant):
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脚本已经可以把上面的字符串来进行解密了,但是我试过了虽然已经打印了done,但是还是没用,修改了JEB代码也没用,于是我去下载了JEB5.18版本,这个最新版本可以直接把字符串混淆除去了。
之后就是看attachBaseContext类了,这里是能够控制执行的早于JNI_OnLoad的位置
这里去判定之后去实现了加载不同的so文件
我们直接去看对应的libjiagu_a64.so——>对应的应该是arm64的so
正常情况下应该去看导出表和导入表,看看最可能出现的关键函数,但是这里的导出表和导入表是空的,那么就说明这里不是通过静态链接来到,而是通过动态加载时确定的导出表和导入表的函数地址的,所以我们要dump下在执行过程中的so文件!
这里的dump_so和脱壳的原理是一样的,通过去找到对应的文件起始地址和文件的大小来进行对于dump文件的写入
dump_so:
通过遍历module去确定是dlopen加载的,通过获取对应的so文件的相关信息并进行dump
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | function Hook_dlopen(target, soName, callback) {
Interceptor.attach(target, {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null ) {
var path = ptr(pathptr).readCString();
console.log( "load " + path);
if (path.indexOf(soName) >= 0) {
this .is_can_hook = true ;
}
}
},
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函数,查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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的参数是啥了
绕过检测/proc/self/maps
首先是获取正常的,在没有进行frida注入的maps,因为这里是需要对应的程序的maps来进行相对数据的获取的,所以需要一个正常的maps,然后我们进行对应open函数参数的重定位,实现利用open函数来读取正常的maps文件
1 | cp / proc / self / maps / data / data / com.oacia.apk_protect / maps
|
重定向frida脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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" ;
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);
} 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文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | 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));
}
}
}
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" ;
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(filename, flag);
}
if (pathname.indexOf( "dex" ) >= 0) {
Thread.backtrace( this .context, Backtracer.FUZZY).map(addr_in_so(openPtr));
}
var fd = open(pathnameptr, flag);
return fd;
}, 'int' , [ 'pointer' , 'int' ]));
}
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也是一样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | 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.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' );
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);
}
}
var dump_once = false ;
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" ;
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(filename, flag);
}
if (pathname.indexOf( "dex" ) >= 0) {
if (!dump_once) {
dump_once = true ;
dump_so( "libjiagu_64.so" );
}
}
var fd = open(pathnameptr, flag);
return fd;
}, 'int' , [ 'pointer' , 'int' ]));
}
}
|
同时进行SOFixer的修复
这时候的对应偏移的地址就已经被填充上了对应的数据了
然后我们通过比对两个填充数据之后的区别来看看到达是多了个啥子
可以通过直接去对比HEX数据(WinMerge,010editor),也就是去利用bindiff看看函数之间的差距值
其实这里填充的是一个新的ELF文件,因为这里文件头都已经有了,所以说这里是在libjiagu_64.so中去添加了一个so文件,有点逆天了
之后就是把这里的文件提取出来了
1 2 3 4 5 6 7 | with open ( 'libjiagu_64.so_0x7a69829000_0x274000_open_classes.dex.so' , 'rb' ) as f:
s = f.read()
with open ( 'libjiagu_0xe7000.so' , 'wb' ) as f:
f.write(s[ 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)等。
所以需要导入对应的结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | #define __LP64__ 1
#if defined(__LP64__)
#define ElfW(type) Elf64_ ## type
#else
#define ElfW(type) Elf32_ ## type
#endif
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;
typedef __u32 Elf32_Addr;
typedef __u16 Elf32_Half;
typedef __u32 Elf32_Off;
typedef __s32 Elf32_Sword;
typedef __u32 Elf32_Word;
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;
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;
Elf64_Xword r_info;
} 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;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} 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;
unsigned char st_info;
unsigned char st_other;
Elf64_Half st_shndx;
Elf64_Addr st_value;
Elf64_Xword st_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;
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];
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry;
Elf64_Off e_phoff;
Elf64_Off e_shoff;
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;
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;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
Elf64_Xword p_filesz;
Elf64_Xword p_memsz;
Elf64_Xword p_align;
} 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;
Elf64_Word sh_type;
Elf64_Xword sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
Elf64_Xword sh_size;
Elf64_Word sh_link;
Elf64_Word sh_info;
Elf64_Xword sh_addralign;
Elf64_Xword sh_entsize;
} Elf64_Shdr;
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;
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;
#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到底应该是怎么样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | function hook_5E6C(){
var module = Process.findModuleByName( "libjiagu_64.so" );
Interceptor.attach(module.base.add(0x5E6C), {
onEnter: function (args) {
console.log(hexdump(args[0], {
offset: 0,
length: 0x38*0x6+0x20,
header: true ,
ansi: true
}));
console.log(args[1])
console.log(args[2])
console.log(`base = ${module.base}`)
},
onLeave: function (ret) {
}
});
}
|
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课