-
-
《基于linker实现so加壳技术基础》下篇
-
发表于: 2021-9-24 18:43 8044
-
《基于linker实现so加壳技术基础》下篇
获得linker维护的本so的soinfo
但是问题又来了如何获得当前so的soinfo指针的基址呢?翻阅网上的资料说可以dlopen打开self,我看了一下那是安卓7之前的方法安卓8.1不支持了(555这不是坑人嘛咋搞),于是我阅读安卓源码发现了获得soinfo的方法,这是一套组合拳,可以先dlopen自己然后再用soinfo_from_handle函数来把handle转换成soinfo,正当我性高彩烈的打开ida查看它的symble的时候,发现没有这个函数,他不是导出函数(sblinker 5555),坑人呢呀,那么就只能照着ida一点一点的翻译它的代码了,找一个调用它的稍微短一点的函数,我找到的是do_dlclose函数,那么中间那一大坨就是soinfo_from_handle的实现了,返回值就是soinfo_unload,的参数,接着我傻眼了,f5之后这玩意没参数(逆天f5),只能看汇编了,还好不长,就是这个x12+0x18中的地址值,切过去一看就是v7[3]那么就对了,我就可以写一个属于自己的handle转soinfo
1 2 | void * dlopen(const char * filename, int flag); static soinfo * soinfo_from_handle(void * handle) |
就是如下的这个函数,有些东西不好处理,比如它搞了好多全局变量,所以我们要从maps里面扫描linker的基址,剩下的直接抄就好了
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 | _QWORD * getsoinfo(unsigned __int64 a1,void * base){ unsigned int v2; / / w19 unsigned __int64 v3; / / x11 __int64 v4; / / x9 __int64 v5; / / x10 _QWORD * v6; / / x12 uint64 * bas1e = reinterpret_cast<uint64 * >((char * ) base + 0xFD468 ); uint64 * bas2 = reinterpret_cast<uint64 * >((char * ) base + 0xFD460 ); _QWORD qword_FD468 = * bas1e; _QWORD _dl_g_soinfo_handles_map = * bas2; unsigned __int64 v7; / / x13 __int64 v8; / / x20 __int64 v9; / / x0 __int64 v11; / / [xsp + 0h ] [xbp - 20h ] BYREF char v12[ 8 ]; / / [xsp + 8h ] [xbp - 18h ] BYREF if ( (a1 & 1 ) ! = 0 ) { if ( qword_FD468 ) { v3 = a1 - a1 / qword_FD468 * qword_FD468; v4 = qword_FD468 - 1 ; v5 = (qword_FD468 - 1 ) & qword_FD468; if ( qword_FD468 > a1 ) v3 = a1; if ( !v5 ) v3 = v4 & a1; v6 = * (_QWORD * * )(_dl_g_soinfo_handles_map + 8 * v3); if ( v6 ) { while ( 1 ) { v6 = (_QWORD * ) * v6; if ( !v6 ) break ; v7 = v6[ 1 ]; if ( v7 = = a1 ) { if ( v6[ 2 ] = = a1 ) { if ( v6[ 3 ] ) break ; } } else { if ( v5 ) { if ( v7 > = qword_FD468 ) v7 - = v7 / qword_FD468 * qword_FD468; } else { v7 & = v4; } if ( v7 ! = v3 ) break ; } } } } } _QWORD * st = reinterpret_cast<uint64 * >((char * ) (v6[ 3 ]) ); return st; } |
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 | void * ax = dlopen( "libnative-lib.so" ,RTLD_NOW); __android_log_print( 6 , "r0ysue" , "%s" ,strerror(errno)); char line[ 1024 ]; int * startr; int * end; int n = 1 ; FILE * fp = fopen( "/proc/self/maps" , "r" ); while (fgets(line, sizeof(line), fp)) { if (strstr(line, "linker64" ) ) { __android_log_print( 6 , "r0ysue" , "%s" , line); if (n = = 1 ){ startr = reinterpret_cast< int * >(strtoul(strtok(line, "-" ), NULL, 16 )); end = reinterpret_cast< int * >(strtoul(strtok(NULL, " " ), NULL, 16 )); } else { strtok(line, "-" ); end = reinterpret_cast< int * >(strtoul(strtok(NULL, " " ), NULL, 16 )); } n + + ; } } void * * old_soinfo = reinterpret_cast<void * * >(getsoinfo((unsigned __int64) ax, startr)); |
链接&soinfo的修正
这里修正soinfo直接用了结构体的->,由于我没有实现soinfo类所以这篇文章就到这里了。。。。。。。那是不可能的肉丝老师教我们永远不放弃,没有条件要创造条件也要解决这个问题,既然没实现soinfo我就用笨方法来实现就是c的偏移,而一个一个数soinfo当中的变量大小太过于麻烦,因为它的变量实在是太多了(555),于是我想到可以使用ida来辅助查看它的偏移,先直接查看LoadTask对象的Load函数
那么其实就是这里,只需要一一对应即可,也就是说
1 2 3 4 5 | si_ - >base = * (si + 16 ) si_ - >size = * (si + 24 ) si_ - >load_bias = * (si + 256 ) si_ - >phnum = * (si + 8 ) si_ - >phdr = * (si) |
那么修正代码就是
1 2 3 4 5 6 7 8 | memcpy(&secstr,(char * )(start) + bb.sh_offset,bb.sh_size); mprotect((void * )PAGE_START((ElfW(Addr))((char * )start)),a.load_size_,PROT_WRITE|PROT_READ|PROT_EXEC); / / 申请读写执行权限因为我们要执行插件so的代码所以要执行权限 __android_log_print( 6 , "r0ysue" , "size %s" ,strerror(errno)); * reinterpret_cast<uint64 * >((char * ) old_soinfo + 16 ) = reinterpret_cast<uint64>(a.load_start_); * ( int * )((char * )(old_soinfo) + 24 ) = a.load_size_; * reinterpret_cast<uint64 * >((char * ) old_soinfo + 256 ) = reinterpret_cast<uint64>(start); * ( int * )((char * )(old_soinfo) + 8 ) = a.phdr_num_; * reinterpret_cast<uint64 * >((char * ) old_soinfo ) = (uint64) a.loaded_phdr_; |
接下来就是链接过程,要将函数的绝对地址填上去,并且将引用的其他so的函数地址也填上去,这里安卓源码实现的函数是prelink_image,非常的长仔细读一下就知道,它其实是可以抄的,这里我们主要修正的是导入表、导出表、重定向表、符号表、字符串表、重定位表、异常处理,但是其实可以照着安卓源码和ida全部把它抄上,这里我从elf头开始获得了程序头然后再程序头中寻找Dynamic段,因为这些表都在动态段中,至于起始地址直接用mmap将上面load得到的loadbias映射过来即可
1 2 3 4 5 6 7 8 9 10 11 12 13 | Elf64_Ehdr aa; void * start = mmap(reinterpret_cast<void * >(a.load_bias_), sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0 ); memcpy(&aa,start,sizeof(Elf64_Ehdr)); / / elf头解析,其实直接用a里面的也行我这里忘了 int secoff = aa.e_shoff; int secsnum = aa.e_shnum; Elf64_Shdr bb; Elf64_Phdr cc; memcpy (&cc,((char * )(start) + aa.e_phoff),sizeof(Elf64_Phdr)); / / 将程序头表存入cc里面 for ( int y = 0 ;y<aa.e_phnum;y + + ){ / / 做遍历 memcpy(&cc, (char * ) (start) + aa.e_phoff + sizeof(Elf64_Phdr) * y, sizeof(Elf64_Phdr)); if (cc.p_type = = 2 ){ / / 当p_type为 0x2 是就代表是Dynamic段 } |
接下来就开始漫长的修正过程了,可以对照着ida都抄源码,主要对照着上面的段都要修复成功。主要就是要将相对地址转化为绝对地址,内容部分使用Elf64_Dyn这个结构体对他进行解析就好,也就是d_tag等于0x6ffffef5时的导出表(so一定要导出给art使用),等于5时的字符串表,等于6时的符号表等等这些都要修正,最终我只取了几个我的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 63 64 65 66 67 68 | if (dd.d_tag = = 0x6ffffef5 ){ / / 对导出表进行修正这个很重要导出失败则无法运行 size_t gnu_nbucket_ = reinterpret_cast<uint32_t * >((char * )start + dd.d_un.d_ptr)[ 0 ]; / / skip symndx uint32_t gnu_maskwords_ = reinterpret_cast<uint32_t * >((char * )start + dd.d_un.d_ptr)[ 2 ]; uint32_t gnu_shift2_ = reinterpret_cast<uint32_t * >((char * )start + dd.d_un.d_ptr)[ 3 ]; ElfW(Addr) * gnu_bloom_filter_ = reinterpret_cast<ElfW(Addr) * >((char * )start + dd.d_un.d_ptr + 16 ); uint32_t * gnu_bucket_ = reinterpret_cast<uint32_t * >(gnu_bloom_filter_ + gnu_maskwords_); / / amend chain for symndx = header[ 1 ] uint32_t * gnu_chain_ = reinterpret_cast<uint32_t * >( gnu_bucket_ + gnu_nbucket_ - reinterpret_cast<uint32_t * >( (char * ) start + dd.d_un.d_ptr)[ 1 ]); - - gnu_maskwords_; uint32_t flags_ = FLAG_GNU_HASH|flags_; * reinterpret_cast<size_t * >((char * ) old_soinfo + 344 ) = gnu_nbucket_; * reinterpret_cast<uint32_t * >((char * ) old_soinfo + 368 ) = gnu_maskwords_; * reinterpret_cast<uint32_t * >((char * ) old_soinfo + 372 ) = gnu_shift2_; * reinterpret_cast< ElfW(Addr) * * >((char * ) old_soinfo + 376 ) = gnu_bloom_filter_; * reinterpret_cast<uint32_t * * >((char * ) old_soinfo + 352 ) = gnu_bucket_; * reinterpret_cast<uint32_t * * >((char * ) old_soinfo + 360 ) = gnu_chain_; * reinterpret_cast<uint32_t * >((char * ) old_soinfo + 48 ) = * reinterpret_cast<uint32_t * >((char * ) old_soinfo + 48 ) |FLAG_GNU_HASH; } if (dd.d_tag = = 2 ){ * reinterpret_cast<uint64 * >((char * ) old_soinfo + 48 ) = dd.d_un.d_val / sizeof(ElfW(Rela)); } if (dd.d_tag = = 0x17 ){ / / 导入表修正 * reinterpret_cast<uint64 * >((char * ) old_soinfo + 104 ) = reinterpret_cast<uint64>( (char * ) start + dd.d_un.d_ptr); } if (dd.d_tag = = 7 ){ / / 重定位修正 * reinterpret_cast<uint64 * >((char * ) old_soinfo + 120 ) = reinterpret_cast<uint64>( (char * ) start + dd.d_un.d_ptr); } if (dd.d_tag = = 5 ){ / / 对字符串表进行修正 * reinterpret_cast<char * * >((char * ) old_soinfo + 56 ) = reinterpret_cast< char * >((char * ) start + dd.d_un.d_ptr); } if (dd.d_tag = = 6 ){ / / 对符号表进行修正 * reinterpret_cast<uint64 * >((char * ) old_soinfo + 64 ) = reinterpret_cast<uint64>( (char * ) start + dd.d_un.d_ptr); } if (dd.d_tag = = 10 ){ * reinterpret_cast<uint64 * >((char * ) old_soinfo + 336 ) = reinterpret_cast<uint64>( (char * ) start + dd.d_un.d_ptr); } if (dd.d_tag = = 8 ){ * reinterpret_cast<uint64 * >((char * ) old_soinfo + 336 ) = dd.d_un.d_val / sizeof(ElfW(Rela)); } if (dd.d_tag = = 0x6ffffff0 ){ * reinterpret_cast<uint64 * >((char * ) old_soinfo + 440 ) = reinterpret_cast<uint64 >((char * )start + dd.d_un.d_ptr); } if (dd.d_tag = = 0x6fffffff ){ * reinterpret_cast<uint64 * >((char * ) old_soinfo + 472 ) = dd.d_un.d_val; } if (dd.d_tag = = 0x6ffffffe ){ * reinterpret_cast<uint64 * >((char * ) old_soinfo + 464 ) = reinterpret_cast<uint64>( (char * ) start + dd.d_un.d_ptr); } if (dd.d_tag = = 1 ){ mynedd[needed] = dd.d_un.d_val; needed + + ; } |
这样其实如果我们被加固的so如果没有引用外部函数就可以正常使用了(哪个so可能没有外部函数呀),因为我们已经修复了导出表,但是为了追求完整性还需要补依赖,比如我要是在被加壳的so中引用了printf或者__android_log_print就会报错
修正依赖函数地址
由于我上面未实现neededso的装载与链接为了方便所以我下面对于依赖so的加载都采用dlopen和dlsym这种方式。这里可以看安卓源码中的link_image函数他调用了relocate来修复JMPREL Relocation Table表,所以我们跟进去看一下,其实这里就很清楚了,用迭代的方法获得so中引用的地址并且根据类型瑱回去我们的so当中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | bool soinfo::relocate(const VersionTracker& version_tracker, ElfRelIteratorT&& rel_iterator, const soinfo_list_t& global_group, const soinfo_list_t& local_group) { .... ElfW(Word) type = ELFW(R_TYPE)(rel - >r_info); ElfW(Word) sym = ELFW(R_SYM)(rel - >r_info); .... if (!soinfo_do_lookup(this, sym_name, vi, &lsi, global_group, local_group, &s)) { return false; } .... switch ( type ) { ... } } |
由于我没有实现soinfo所以只能另辟蹊径,从原理出发用dlopen和dlsym另写一套方案。首先把上面的符号表和字符串表用起来,然后照着源码实现一个遍历的类(不实现用循环也可以,但是直接ctrl+cv就好了还不用动脑仁何乐而不为呢),而且要用到上面的导入库表,当然不知道安卓源码咋抽风了,就是没有R_SYM和R_TYPE这两个类型的定义我只能自己导入了,其实这两个就是对info的解析十分的简单
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 | class plain_reloc_iterator { public: plain_reloc_iterator(rel_t * rel_array, size_t count) : begin_(rel_array), end_(begin_ + count), current_(begin_) {} bool has_next() { return current_ < end_; } rel_t * next () { return current_ + + ; } public: rel_t * const begin_; rel_t * const end_; rel_t * current_; }; #define ELFW(what) ELF64_ ## what #define R_TYPE(sym) ((((Elf64_Xword)sym) << 32) #define R_SYM(type) ((type) & 0xffffffff)) char * strtab_ = * reinterpret_cast<char * * >((char * ) old_soinfo + 56 ) ; / / 字符串表基址 Elf64_Sym * symtab_ = * reinterpret_cast<Elf64_Sym * * >((char * ) old_soinfo + 64 ); / / 符号表基址 plain_reloc_iterator myit( reinterpret_cast<rel_t * >( * reinterpret_cast<uint64 * >( (char * ) old_soinfo + 104 )), * reinterpret_cast<size_t * >((char * ) old_soinfo + 48 )); __android_log_print( 6 , "r0ysue" , "finish xxx%x" , * reinterpret_cast<size_t * >((char * ) old_soinfo + 48 )); |
最后写一个循环回填就好了
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 | for (size_t idx = 0 ; myit.has_next(); + + idx) { const auto rel = myit. next (); ElfW(Word) type = ELFW(R_TYPE)(rel - >r_info); ElfW(Word) sym = ELFW(R_SYM)(rel - >r_info); ElfW(Addr) sym_addr = 0 ; const char * sym_name = nullptr; const Elf64_Sym * s = nullptr; if ( type = = 0 ) { / / 不处理类型为 0 的部分 continue ; } sym_name = reinterpret_cast<const char * >(strtab_ + symtab_[sym].st_name); / / 根据get_string函数改编 for ( int s = 0 ;s<needed;s + + ) { / / 遍历所有的导入库表用dlopen和dlsym查找是否有我们需要的符号 void * handle = dlopen(strtab_ + mynedd[s],RTLD_NOW); sym_addr = reinterpret_cast<Elf64_Addr>(dlsym(handle, sym_name)); if (sym_addr = = 0 ) continue ; else / / __android_log_print( 6 , "r0ysue" , "finish xxwwwwwwwwwwwwwwwx%p %s" , sym_addr,sym_name); break ; } switch ( type ) { case 1026 : / / 我只有 0x402 类型的部分所以就简化处理了 * reinterpret_cast<uint64 * >((char * ) start + rel - >r_offset) = (sym_addr ); break ; } } |
跟到这里其实就完成了,下面看一下结果
1 2 3 4 5 6 7 8 | / / 插件so当中的代码 extern "C" JNIEXPORT jint JNICALL Java_com_roysue_elfso_MainActivity_add(JNIEnv * env, jobject thiz, jint a, jint b) { printf( "cxzcxzcxz" ); __android_log_print( 6 , "r0ysue" , "i am from 1.so %p" ,a); return a + b; } |
最后日志,这样就完成和art的交互,后面还有执行init_arry函数和Jni_Onload也是十分的简单我就不实现了
总结
本篇文章只是一个基础用于对新手的so加壳入门,我粗略的实现了一个简单的so壳,算是我踩到的许多坑,其中导出表的修复就花费了好久的时间最终才成功,感谢大家观看
附件加壳demo
链接:https://pan.baidu.com/s/1MZSjotH8cs7wrOIAiZM5NQ
提取码:kjvm
赞赏
- [原创]Base64 编码原理 && 实现 23389
- [原创]Lsposed 技术原理探讨 && 基本安装使用 26341
- [原创]Unidbg-Linker部分源码分析(下) 28647
- [原创]Unidbg-Linker部分源码分析(上) 26135