转载

某App加固保免费版分析

某App加固保免费版分析

本文为看雪论坛精华文章

看雪论坛作者ID:卧勒个槽

详细分析

自己随便新建个项目,用某App加固保去加固。加固后apk包如下所示。原来的class都没了,assets下多了几个so。

某App加固保免费版分析

通过AndroidManifest.xml可知入口在com.stub.StubApp。

先看attachBaseContext方法,可知是判断cpu,然后将对应的so文件释放到app的data目录下的.jiagu目录,然后用System.load方法加载该so,这里我手机对应的为libjiagu_a64.so。

某App加固保免费版分析

用ida打开so,查找init_array,里面只有一个函数sub_17CC。 

某App加固保免费版分析

函数sub_17CC调用sub_20B4,其内容如下,先通过mmap分配一段内存,然后通过sub_1F4C将一段数据解密后存在刚刚分配的内存中。

再然后调用sub_1ECC对解密后的数据进行重定位。最后通过mprotect修改内存页属性。

某App加固保免费版分析

然后分析JNI_OnLoad,从刚刚解密的数据中获取函数地址,然后跳转,最终调用到__arm_a_1(地址0x89C0)

某App加固保免费版分析

__arm_a_1内容如下,sub_8950检查当前so名字是否为libjiagu开头的,不是则生成信号9结束进程。sub_837C读取/proc/net/tcp文件,检查端口0x5D8A,如果存在则kill进程,所以动态调试要把ida的默认端口改了。

sub_83B4则是解密出一个so并通过自定义的加载器将它加载。然后找到新so的JNI_OnLoad调用。

某App加固保免费版分析

sub_83B4通过分析,最终调用到sub_5254的时候,所有数据都解密出来了,可以在此处dump新的so。

sub_5254函数有两个参数,其数据结构分别如下:

参数1中buf_1、buf_2、buf_3、buf_4分别为解密后的程序头表、JMPREL、RELA、DYNAMIC

某App加固保免费版分析

参数2中的decode_data为中间数据,参数1中的buf_1、buf_2、buf_3、buf_4就是从这里解密出来的,最后还包含一个so,该so被抹除了前面这几部分内容。

某App加固保免费版分析

在sub_5254入口处dump解密的so,脚本如下。

auto buf_ptr;

buf_ptr=qword(x1+0x10);

msg("%x/n",buf_ptr);


auto buf_1_len_ptr,buf_1_len,buf_1_ptr;

buf_1_len_ptr=buf_ptr+1;

buf_1_len=dword(buf_1_len_ptr);

buf_1_ptr=buf_1_len_ptr+4;

msg("%x,%x/n",buf_1_ptr,buf_1_len);


auto buf_2_len_ptr,buf_2_len,buf_2_ptr;

buf_2_len_ptr=buf_1_ptr+buf_1_len;

buf_2_len=dword(buf_2_len_ptr);

buf_2_ptr=buf_2_len_ptr+4;

msg("%x,%x/n",buf_2_ptr,buf_2_len);


auto buf_3_len_ptr,buf_3_len,buf_3_ptr;

buf_3_len_ptr=buf_2_ptr+buf_2_len;

buf_3_len=dword(buf_3_len_ptr);

buf_3_ptr=buf_3_len_ptr+4;

msg("%x,%x/n",buf_3_ptr,buf_3_len);


auto buf_4_len_ptr,buf_4_len,buf_4_ptr;

buf_4_len_ptr=buf_3_ptr+buf_3_len;

buf_4_len=dword(buf_4_len_ptr);

buf_4_ptr=buf_4_len_ptr+4;

msg("%x,%x/n",buf_4_ptr,buf_4_len);


auto buf_5_len,buf_5_ptr;

buf_5_ptr=buf_4_ptr+buf_4_len;

buf_5_len=dword(buf_5_ptr+0x28)+word(buf_5_ptr+0x3a)*word(buf_5_ptr+0x3c);

msg("%x,%x/n",buf_5_ptr,buf_5_len);


auto new_buf_1_ptr,new_buf_2_ptr,new_buf_3_ptr,new_buf_4_ptr;

new_buf_1_ptr=qword(x0+0x50);

new_buf_2_ptr=qword(x0+0x80);

new_buf_3_ptr=qword(x0+0x88);

new_buf_4_ptr=qword(x0+0x90);

msg("%x,%x,%x,%x/n",new_buf_1_ptr,new_buf_2_ptr,new_buf_3_ptr,new_buf_4_ptr);



auto ph_off,jmprel_off,rela_off,dynamic_off;

ph_off=qword(buf_5_ptr+0x20);


auto ph_index,ph_size,ph_num,temp_ptr;

ph_index=0;

ph_size=word(buf_5_ptr+0x36);

ph_num=word(buf_5_ptr+0x38);

while(ph_index<ph_num)

{

temp_ptr=new_buf_1_ptr+ph_size*ph_index;

if(dword(temp_ptr)==2)

{

dynamic_off=qword(temp_ptr+8);

break;

}

ph_index++;

}


auto dyn_index,dyn_size,dyn_num;

dyn_index=0;

dyn_size=0x10;

dyn_num=buf_4_len/dyn_size;

while(dyn_index<dyn_num)

{

temp_ptr=new_buf_4_ptr+dyn_size*dyn_index;

if(dword(temp_ptr)==0x17)

{

jmprel_off=qword(temp_ptr+8);

}

else if(dword(temp_ptr)==7)

{

rela_off=qword(temp_ptr+8);

}

dyn_index++;

}

msg("%x,%x,%x,%x/n",ph_off,jmprel_off,rela_off,dynamic_off);



msg("start/n");

auto dump_so ;

dump_so =fopen("D:// dump.so","wb");


savefile(dump_so, 0, buf_5_ptr, buf_5_len);

msg("save so(buf_5)/n");


savefile(dump_so, ph_off, new_buf_1_ptr, buf_1_len);

msg("save Phdrs((buf_1))/n");


savefile(dump_so, jmprel_off, new_buf_2_ptr, buf_2_len);

msg("save DT_JMPREL(buf_2)/n");


savefile(dump_so, rela_off, new_buf_3_ptr, buf_3_len);

msg("save DT_RELA(buf_3)/n");


savefile(dump_so, dynamic_off, new_buf_4_ptr, buf_4_len);

msg("save PT_DYNAMIC(buf_4)/n");



fclose(dump_so);


msg("end/n");


将dump出来的so用ida打开,该so因为没有Section Header Table,所以要自己到动态节中去找到init_array。

动态节内容如下。可以知道init_array在0x1277c0。

某App加固保免费版分析

init_array内容如下。这些函数都是在初始化一些变量。

某App加固保免费版分析

然后分析JNI_OnLoad。

先是注册StubApp的各种native方法,内容如下:

某App加固保免费版分析

然后再解析linker64查找符号rtld_db_dlactivireport保存起来。未被调试时返回0。后面解密vmp方法的时候会用到,如果不为0会导致解密失败。

然后将从dex中解析出附加数据,其包含加固的配置信息和所有原始dex。

解析dex的地方为sub_240C8,该函数主要就是对每个dex开启一个线程进行解密,然后join获取结果。

某App加固保免费版分析

所以在sub_240C8返回的时候,dump所有dex,返回值结构如下:

某App加固保免费版分析

dump脚本如下。

msg("start/n");

auto start,end,index,dex_ptr,file_size,dump_name,dump_dex;

start=qword(x0);

end=qword(x0+8);


index=0;

while(index*8+start!=end)

{

dex_ptr=qword(qword(index*8+start));

file_size=dword(dex_ptr+0x20);


index=index+1;

if (index==1)

{

dump_name="classes.dex";

}

else

{

dump_name="classes"+ltoa(index,10)+".dex";

}


msg("%s %08x %08x/n",dump_name,dex_ptr,file_size);

dump_dex=fopen("D://"+dump_name,"wb");

savefile(dump_dex, 0, dex_ptr, file_size);

fclose(dump_dex);


}

msg("end/n/n");

然后通过DexFile::OpenCommon加载所有的dex文件。

然后通过循环调用sub_8B6E4,从dex中解析出附加数据,该附加数据包含所有被vmp方法的信息,如下所示,每个item包含5个字段,每个4字节。

第一个字段为方法在dex中的method_id,

第二个字段为方法类型,0为实例方法,1为静态方法,

第三个字段为方法对应的code_item在dex中的偏移。

第四个字段为360自定义的clas_id,注册vmp方法的时候用到。

第五个字段为后面紧跟着的指令数,如果为0表示直接解密原来code_item中的指令执行,不为0则执行的时候,把该处的指令复制到原来的code_item,执行完后又清除。

某App加固保免费版分析

然后到sub_1FF80,该函数将所有dex中的DexPathList.Element添加到原类加载器中, 然后将所有dex前8字节清空。

然后解析dex,为vmp方法执行分配空间,并初始化一些数据结构。 其中sub_8FD08会分配一个数组,每个vmp方法都会将sub_12D040函数的机器码复制过来。

作为调用代理。之所以每个方法都复制,是因为到时调用的时候,会将函数的地址作为参数,这样通过偏移就能知道是调用哪一个方法。

某App加固保免费版分析

然后再根据配置决定是否dex2oat。

再然后就是注册一些类的native方法,就先不管了。到此为止,所有加载工作就完成了。

回到StubApp类中attachBaseContext,在加载so后还调用了interface5,onCreate调用了interface21,通过前面注册是时候找到的方法,分析后,这两个方法只是根据配置做了一些操作,就先不管了。

现在来看之前dump的dex文件。对比下加固前后的内容,可以看到,onCreate方法被改为native方法了,而且类初始语句中多了一行代码StubApp.interface11(1344);

某App加固保免费版分析

某App加固保免费版分析

现在来分析interface11,通过前面可知,该方法绑定的函数为sub_327F4。找到该函数,f5生成伪代码,发现有JUMPOUT这玩意儿,这个还不能直接就跳过去按p新建函数,不然一块代码一个函数,流程根本就搞不清楚,而且变量也不对。

某App加固保免费版分析

跳转地址是通过计算得到的,通过分析,sub_8F5A0返回1就跳到off_12A4D0[0],返回0就跳到off_12A4D0[1],所以可以判断出,这里其实是一个if-else,只是地址在数组里。

因为所有dump出来的时候,没有Section Header Table,所以整个文件所有都是可读可写的,我以为是因为这个原因,导致ida不能识别出,于是手动将数组所在的地方建了 一个节,属性改为只读,结果还是识别不出来。

于是我就放弃了,手动将跳转地址写入指令中。即将原来的ldr、br等指令改为beq、bne。

原始指令如下:

某App加固保免费版分析

修改后指令如下:

某App加固保免费版分析

然后就按照这个套路,遇到就修改,一个函数改了几十个地方,我人都要疯了, 改完后就能看到流程了。

某App加固保免费版分析

该函数其实就是通过参数传进来的那个数字(自定义的class id),在附加数据找到对应的class,将class对应的所有vmp方法进行动态注册。

比如 MainActivity中的StubApp.interface11(1344);

1344的十 六进制为0x540。

某App加固保免费版分析

而第一个字段为方法id,由此知道,该类的vmp只有id为0x3b8b这一个,通过解析dex,可知该方法为onCreate:

某App加固保免费版分析

现在来看绑定的本地函数是什么,可以看到,每个方法绑定的都是前面复制的那个函数sub_12D040的指令。

某App加固保免费版分析

所以现在来分析sub_12D040,该函数就是调用了qword_12D0C0处保存的地址,该处是在之前复制指令的时候进行赋值的,为sub_3156C。

某App加固保免费版分析

现在来看sub_3156C,又是这玩意儿,又是几十处手动改好,人又疯了一遍。

某App加固保免费版分析

改好后,分析流程如下,先通过第一个参数,判断出当前调用的是哪一个方法。

可以看出,当前调用的时候,第一个参数传了一个地址:

某App加固保免费版分析

通过汇编指令可以看到,该地址就是函数调用后的返回地址。

所以现在明白了,为什么每一个绑定的都是同一个函数,但是要复制到不同的位置。

某App加固保免费版分析

然后,一个恶心的事又来了,特么把jni接口的函数顺序重新赋值了一遍,调用jni方法的时候,用的自定义的那个。于是,又只好把这玩意儿复制一遍,两百多个函数,人又疯了一遍。

某App加固保免费版分析      某App加固保免费版分析

然后根据第五个字段,不为0则复制指令到code_item,然后直接用jni函数调用,调用完成后,再将其清空。

某App加固保免费版分析

如果是0,则申请寄存器空间,自己将参数解析出来, 然后调用sub_3E620,又是,人又疯一遍。

某App加固保免费版分析

改好后,分析流程如下,解释执行的时候,逐条指令开始解密,并且解密后的指令不是加固前的,也是经过替换的。所以要想修复,需要每个指令都去分析一下才行。

某App加固保免费版分析

还原指令这个就先不搞了,以后有时间再弄了。

总结

1. 开始的时候,第一个so有部分代码是动态解密的。

2. 然后会解密出第二个so,该so的程序头表、JMPREL、RELA、DYNAMIC是抽离的,通过自定义的linker加载。并且节表也被清空了。

3. 所有dex都被加密隐藏在的classes.dex后面。

4. DexFile::OpenCommon加载所有dex,并设置类加载器的pathList.dexElements。将dex前8字节清空。

5. 类初始化的时候注册当前类的vmp方法。

6. vmp化方法绑定的本地函数执行的时候,根据当前函数地址确定执行的是哪一个方法。

7. 绑定的本地函数中if-else的分支地址被保存在变量中,通过br跳转,需要修复后才能看清流程。

8. jni接口中的函数被重新排列,需要创建一个对应的结构进行赋值操作。

9. vmp方法对应两种执行模式,一种是直接将指令复制到原地方,然后用jni函数调用,调用完成后再将其清空。另一种方式是自己实现的解释器,边解密边解释执行,且解密后的指令也是替换过的。要想修复,需要把每条指令的流程跟踪一遍,看它实际用途是什么。

疯了,疯了。

某App加固保免费版分析

看雪ID: 卧勒个槽

https://bbs.pediy.com/user-745332.htm  

*本文由看雪论坛 卧勒个槽 原创,转载请注明来自看雪社区。

某App加固保免费版分析

原文  http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458304538&idx=1&sn=68b98cf20b8ceb7b4abb794e4a0cd948
正文到此结束
Loading...