这玩意儿实在是有点恶心,各种骚操作来浪费你的时间。比如把jni接口的函数全部重新排列了一遍,你得在他赋值的时候手动去把两百多个函数复制过来。把if-else的分支跳转地址全放在数组里面,f5生成伪代码全是JUMPOUT,你得手动去修复所有地址。。。
自己随便新建个项目,用360加固保去加固。加固后apk包如下所示。原来的class都没了,assets下多了几个so。
通过AndroidManifest.xml可知入口在com.stub.StubApp。
先看attachBaseContext方法,可知是判断cpu,然后将对应的so文件释放到app的data目录下的.jiagu目录,然后用System.load方法加载该so,这里我手机对应的为libjiagu_a64.so。
用ida打开so,查找init_array,里面只有一个函数sub_17CC。
函数sub_17CC调用sub_20B4,其内容如下,先通过mmap分配一段内存,然后通过sub_1F4C将一段数据解密后存在刚刚分配的内存中。再然后调用sub_1ECC对解密后的数据进行重定位。最后通过mprotect修改内存页属性。
然后分析JNI_OnLoad,从刚刚解密的数据中获取函数地址,然后跳转,最终调用到__arm_a_1(地址0x89C0),
__arm_a_1内容如下,sub_8950检查当前so名字是否为libjiagu开头的,不是则生成信号9结束进程。sub_837C读取/proc/net/tcp文件,检查端口0x5D8A,如果存在则kill进程,所以动态调试要把ida的默认端口改了。sub_83B4则是解密出一个so并通过自定义的加载器将它加载。然后找到新so的JNI_OnLoad调用。
sub_83B4通过分析,最终调用到sub_5254的时候,所有数据都解密出来了,可以在此处dump新的so。
sub_5254函数有两个参数,其数据结构分别如下:
参数1中buf_1、buf_2、buf_3、buf_4分别为解密后的程序头表、JMPREL、RELA、DYNAMIC
参数2中的decode_data为中间数据,参数1中的buf_1、buf_2、buf_3、buf_4就是从这里解密出来的,最后还包含一个so,该so被抹除了前面这几部分内容。
在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
init_array内容如下。这些函数都是在初始化一些变量。
然后分析JNI_OnLoad。
先是注册StubApp的各种native方法,内容如下:
然后再解析linker64查找符号rtld_db_dlactivireport保存起来。未被调试时返回0。后面解密vmp方法的时候会用到,如果不为0会导致解密失败。
然后将从dex中解析出附加数据,其包含加固的配置信息和所有原始dex。
解析dex的地方为sub_240C8,该函数主要就是对每个dex开启一个线程进行解密,然后join获取结果。
所以在sub_240C8返回的时候,dump所有dex,返回值结构如下
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,执行完后又清除。
然后到sub_1FF80,该函数将所有dex中的DexPathList.Element添加到原类加载器中
然后将所有dex前8字节清空。
然后解析dex,为vmp方法执行分配空间,并初始化一些数据结构。其中sub_8FD08会分配一个数组,每个vmp方法都会将sub_12D040函数的机器码复制过来。作为调用代理。之所以每个方法都复制,是因为到时调用的时候,会将函数的地址作为参数,这样通过偏移就能知道是调用哪一个方法。
然后再根据配置决定是否dex2oat
再然后就是注册一些类的native方法,就先不管了。到此为止,所有加载工作就完成了。
回到StubApp类中attachBaseContext,在加载so后还调用了interface5,onCreate调用了interface21,通过前面注册是时候找到的方法,分析后,这两个方法只是根据配置做了一些操作,就先不管了。
现在来看之前dump的dex文件。对比下加固前后的内容,可以看到,onCreate方法被改为native方法了,而且类初始语句中多了一行代码StubApp.interface11(1344);
现在来分析interface11,通过前面可知,该方法绑定的函数为sub_327F4。找到该函数,f5生成伪代码,发现有JUMPOUT这玩意儿,这个还不能直接就跳过去按p新建函数,不然一块代码一个函数,流程根本就搞不清楚,而且变量也不对,,,,
跳转地址是通过计算得到的,通过分析,sub_8F5A0返回1就跳到off_12A4D0[0],返回0就跳到off_12A4D0[1],所以可以判断出,这里其实是一个if-else,只是地址在数组里。因为所有dump出来的时候,没有Section Header Table,所以整个文件所有都是可读可写的,我以为是因为这个原因,导致ida不能识别出,于是手动将数组所在的地方建了 一个节,属性改为只读,结果还是识别不出来。于是我就放弃了,手动将跳转地址写入指令中。即将原来的ldr、br等指令改为beq、bne。
原始指令如下:
修改后指令如下:
然后就按照这个套路,遇到就修改,一个函数改了几十个地方,我特么人都要疯了,,,,
改完后就能看到流程了。
该函数其实就是通过参数传进来的那个数字(自定义的class id),在附加数据找到对应的class,将class对应的所有vmp方法进行动态注册。
比如MainActivity中的StubApp.interface11(1344);
1344的十六进制为0x540
而第一个字段为方法id,由此知道,该类的vmp只有id为0x3b8b这一个,通过解析dex,可知该方法为onCreate
现在来看绑定的本地函数是什么,可以看到,每个方法绑定的都是前面复制的那个函数sub_12D040的指令,
所以现在来分析sub_12D040,该函数就是调用了qword_12D0C0处保存的地址,该处是在之前复制指令的时候进行赋值的,为sub_3156C。
现在来看sub_3156C,又是这玩意儿,又是几十处手动改好,人又疯了一遍,,,,
改好后,分析流程如下,先通过第一个参数,判断出当前调用的是哪一个方法。
可以看出,当前调用的时候,第一个参数传了一个地址,
通过汇编指令可以看到,该地址就是函数调用后的返回地址。
所以现在明白了,为什么每一个绑定的都是同一个函数,但是要复制到不同的位置。
然后,一个恶心的事又来了,特么把jni接口的函数顺序重新赋值了一遍,调用jni方法的时候,用的自定义的那个。于是,又只好把这玩意儿复制一遍,两百多个函数,人又疯了一遍,,,,,,
然后根据第五个字段,不为0则复制指令到code_item,然后直接用jni函数调用,调用完成后,再将其清空。
如果是0,则申请寄存器空间,自己将参数解析出来,
然后调用sub_3E620,又是,,,,,人又疯一遍,,,,
改好后,分析流程如下,解释执行的时候,逐条指令开始解密,并且解密后的指令不是加固前的,也是经过替换的。所以要想修复,需要每个指令都去分析一下才行
还原指令这个就先不搞了,以后有时间再弄了。。。
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函数调用,调用完成后再将其清空。另一种方式是自己实现的解释器,边解密边解释执行,且解密后的指令也是替换过的。要想修复,需要把每条指令的流程跟踪一遍,看它实际用途是什么。
10.疯了,疯了,,,,,