《基于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壳,算是我踩到的许多坑,其中导出表的修复就花费了好久的时间最终才成功,感谢大家观看