本帖最后由 HCJ_V 于 2021-10-04 23:09 编辑
《基于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 void* dlopen(const char* filename, int flag); static soinfo* soinfo_from_handle(void* handle)
’‘’‘’‘
就是如下的这个函数,有些东西不好处理,比如它搞了好多全局变量,所以我们要从maps里面扫描linker的基址,剩下的直接抄就好了 _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;
}
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函数 那么其实就是这里,只需要一一对应即可,也就是说 si_->base = *(si+16) si_->size = *(si+24) si_->load_bias =* (si+256) si_->phnum = *(si+8) si_->phdr = *(si)
那么修正代码就是 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得到的load_bias_映射过来即可 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中有的段类型进行修正 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当中。 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的解析十分的简单 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));
最后写一个循环回填就好了 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;
}
}
跟到这里其实就完成了,下面看一下结果 //插件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
游客你好,如果您要查看本帖隐藏链接需要登录才能查看,
请先登录
|