前言
本地环境依然是6.0.1的系统,这应该是最后一个分析的抽取类型的壳,后面会正式进入VMP的分析,文章没有分析的太透彻,主要还是以脱壳为主。文中ida中出现的字段和函数名可能根据自己的理解被修改过,也可能出现错误,还请各位大佬多多担待,并且指正,由于某些原因最终的脱壳脚本没办法给大家提供,但是会有思路,还请大家多多包涵.
Java层入口
首先,这个壳有点不太一样

大家通过看上面图片可以发现,这个应用的dex抽取过后和壳打到一起了。所以后面就不会有再去加载dex的操作。OK,下面正式开始分析
定位s.h.e.l.l.S

s.h.e.l.l.N->静态块

之后进入so层的分析
So层入口

so脱壳
调试的话,直接定位linker,调试一下.init_proc






过掉UND之后,可以开始dump了。




发现并不是从ELF头开始的,而是0x10000起始的。所以直接修复了
我采用新建一个2进制文件,先将0x10000之前的数据copy进去,后面再拼接此dump出的数据,然后实际大小,是dump出的大小加上前面的数据得出总大小


然后把数据追加到新创建的so中,注意对齐

由于我并不需要把so完美修复好,我本地修复主要是动态调试有个对照,所以这2个segment修复后,就可以看到大致的代码了。

分析INIT_ARRAY
init_array做了保护,被编译器拆分成很多函数,我分析的时候几乎是一个一个看的,总结出来了,init_array主要做2个事情,第一,字符串解密,并保存;第二,反调试,下面简单介绍一下主要流程.

前面若干个函数主要做了一些字符串的解密,类似于下图

定位sub_12BDC:
兼容性处理与函数地址初始化

定位sub_2CB80
这函数主要做反调试



JNI_ONLOAD
这个函数并不是很长,通过分析,核心做了2个事情。(这里说一下)
初始化一些数据:例如: 机型相关的数据:HARDWARE,MODEL,RELEASE,sdk_version等等(这个是为了兼容性考虑,后面逻辑会有一些判断),applicationInfo,processName,sourceDir, 待使用的文件路径,和一些Java层的class名字,最终都会保存在一个全局结构体中,和乐固类似。
Java层的函数动态注册: 其中主要涉及到如下函数
**N:l->sub_3C02C
N:r->sub_3EF5C
N:ra->sub_3F46C**
下面做简单分析,定位sub_3AB48(JNI_ONLOAD核心实现在这里)





JNI_ONLOAD走完了,下面继续回到Java层,看看调用了哪个native函数

sub_3C02C:N->l
这个函数有很多的兼容性的操作.



这里hook了非常多的art的函数,我们比较关注的,定位sub_26BAC, 壳的还原时机就是hook了这个loadMethod函数

后面还有一些逻辑,不过到这里,这个壳已经可以脱了。
opcode填充时机
定位sub_27034



然后进入sub_51CD8开始真正的填充


反调试与环境检测
反调试
init_array已经有3中反调试了





后面还有,但是我idb丢失了一次,这个忘记了,大家到时候自己调试一下在sub_2D6CC
环境检测
检测是否有/data/dexname

尝试env->loadClass("cn/youlor/Unpacker")

检测是否存在“/data/local/tmp/unpacker.config”

检测是否存在fart


检测/data/local/tmp/re.frida.server

然后有个专门的线程,检测maps中的内容,检测了如下字符串
com.android.reverse-
/data/local/tmp/libFupk3.so
xposed.Fdex2
/system/bin/app_process32_xposed
xposed.installer
app_process64_xposed
libxposed_art.so
io.va.exposed
io.virtualapp.sandvxposed
libriru_
com.saurik.substrate
re.frida.server
mapp.rm-
_frida-agent.so
com.example.FunDex-
nop反调试与环境检测
经过下面一段脚本的执行,可以直接F9让应用完美运行起来
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 | base = 0x13FB4
addr_patch1 = 0x2CB90
addr_patch2 = 0x2CC5E
addr_patch3 = 0x2CD48
addr_patch4 = 0x4035C
addr_patch5 = 0x3C336
addr_pathc6 = 0x3C340
addr_patch7 = 0x3C3B2
addr_patch8 = 0x3C3FE
addr_patch1_add = addr_patch1 - base
addr_patch2_add = addr_patch2 - base
addr_patch3_add = addr_patch3 - base
addr_patch4_add = addr_patch4 - base
addr_patch5_add = addr_patch5 - base
addr_patch6_add = addr_pathc6 - base
addr_patch7_add = addr_patch7 - base
addr_patch8_add = addr_patch8 - base
print ( hex (addr_patch1_add))
print ( hex (addr_patch2_add))
print ( hex (addr_patch3_add))
print ( hex (addr_patch4_add))
print ( hex (addr_patch5_add))
print ( hex (addr_patch6_add))
print ( hex (addr_patch7_add))
if addr_patch1_add = = 0x18bdc and addr_patch2_add = = 0x18caa :
addr_patch1_real = base_ea + addr_patch1_add
addr_patch2_real = base_ea + addr_patch2_add
addr_patch3_real = base_ea + addr_patch3_add
addr_patch4_real = base_ea + addr_patch4_add
addr_patch5_real = base_ea + addr_patch5_add
addr_patch6_real = base_ea + addr_patch6_add
addr_patch7_real = base_ea + addr_patch7_add
addr_patch8_real = base_ea + addr_patch8_add
idaapi.patch_dword(addr_patch1_real, 0x0000F04F )
idaapi.patch_word(addr_patch2_real, 0xBF00 )
idaapi.patch_dword(addr_patch3_real, 0x0000F04F )
idaapi.patch_dword(addr_patch4_real, 0xBF00BF00 )
idaapi.patch_word(addr_patch5_real, 0xBF00 )
idaapi.patch_dword(addr_patch6_real, 0xBF00BF00 )
idaapi.patch_dword(addr_patch7_real, 0x0000F04F )
idaapi.patch_dword(addr_patch8_real, 0xBF00BF00 )
|
脱壳
比较抱歉,这边由于某些原因,最终的脚本不能给到大家,下面说一下思路。
1.Dump壳已经准备好的数据
大家要先仔细调试一下sub_27034这个函数,就知道dump哪里了,很明显。
根据上面壳本身的还原逻辑,我们可以直接从内存中dump出3段数据,
第一段是真正的opcode相关信息存放的数据段,例如opcode的长度,以及真正的opcode。
后面2段是存放有关debuginfo相关的数据,例如debuginfo的值,以及当前debuginfo在第一段数据中对应的起始地址。
然后我们还需要保存当前3段数据的起始地址,因为这些数据dump出来的时候,存放的都是实际地址,所以我们需要减去起始地址,纯计算偏移去做。
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 | int getReallyAddr1( int addr) {
int malloc_base = 0xAA640000 ;
int r_addr = addr - malloc_base;
return r_addr;
}
int getReallyAddr2( int addr) {
int malloc_base = 0xAB140000 ;
int r_addr = addr - malloc_base;
return r_addr;
}
/ / 0xAA640000 :第二段数据的起始地址
/ / 0xAB140000 :第三段数据的起始地址
/ / 这个函数计算真正的地址
int getReallyAddr( int addr) {
if (addr > 0xAA640000 and addr < 0xAB140000 ) {
return getReallyAddr1(addr);
} else {
return getReallyAddr2(addr);
}
}
/ / 这个函数计算用哪个mmap的内存去取数据
char * getReallyMp( int addr, char * mp1, char * mp2) {
if (addr > 0xAA640000 and addr < 0xAB140000 ) {
return mp1;
} else {
return mp2;
}
}
|
2.根据原dex提取debuginfo
这里用py脚本解析DexFile就好。把每个code_item的debuginfo保存到文件,这里是否保存到文件取决于大家最终的修复脚本用py还是C,我是用C的,但是我的解析脚本是py,所以保存文件中给C解析.
1 2 3 4 5 6 7 8 9 10 11 12 13 | ff = 'classes.dex'
p = DexParser(ff)
p.parse()
for classDef in p.class_def:
dataOff = classDef[ 'class_data_off' ]
if dataOff ! = 0 :
dataItem = classDef[ 'class_data_item' ]
direct_method = dataItem[ 'direct_methods' ]
vir_methods = dataItem[ 'virtual_methods' ]
for dirMethod in direct_method:
info_off = dirMethod[ 'code_item' ][ 'debug_info_off' ]
for vir_method in vir_methods:
info_off = vir_method[ 'code_item' ][ 'debug_info_off' ]
|
3.还原壳的修复逻辑
这里还原大家一定要直接看指令,不要F5,不要F5,不要F5!
1.还原sub_152D2
2.还原sub_51CD8
4.最终效果
脱壳前:

脱壳后:

CTF训练营-Web篇
最后于 2021-5-9 14:59
被GitRoy编辑
,原因: