什么是脱壳的最高境界 莫过于非dump脱壳之后替换进apk中 依然正常运行 这对ELF功底无疑是一种挑战
今天就献丑拿某分类第一的APK样本做一下静态脱壳分析。无需dump 自己去实现它解密的操作并且修改elf头实现脱壳并且完美运行,还有我们追求的是完美脱壳 而不是脱壳 完美脱壳再去动态调试会更清晰的调试出来函数路径 这个大家应该都能理解把!
献上对比图,因为没有对比就没有伤害
1 | 大家可以清晰的看到这里是同一块地址的脱壳前后的状态
|
当然还有替换进去的运行图
讲完效果就开干呗!
简单分析
分析一下,没有InitArray节 但是so中存在init_proc函数 熟悉linker流程的小伙伴肯定知道,那么肯定是这个函数进行脱壳的操作的
初始化(init)和终止(fini)过程是由动态链接器(linker)执行的。 动态链接器负责解析共享库的依赖关系,找出并加载所需要的库,然后解析和重定位符号。在完成这些任务之后,它会调用每个已加载模块的初始化函数。
由上文可知,函数在开头必然要分配函数所需的栈空间以及保存调用者保存寄存器因此,当你调用 dlopen() 时,实际上是动态链接器在背后完成的大部分工作。dlopen() 会通知动态链接器去加载指定的库,然后链接器会找到并执行这个库的 init 函数
很简单的两个函数,首先通过svc mmap 分配个足够大的内存空间并且把原始经过加密前的opcode复制进内存中,用于后面的解密释放
其中还有个防止inlinehook的函数 check_maps 当检测到调试的时候直接触发 然后结束程序 当然老规矩 直接NOP掉
通过_NR_mprotect修改代码段权限为 PROT_WRITE PROT_EXEC PROT_READ
拿到三个值 基址 需要解密的头地址 以及数据长度 还有解密后应有的长度 这些值都是存在于一个数组中 当解密完成_NR_munmap
主要解密函数是
ok 不纠结 照着还原一下
一比一还原后,模拟so的操作从指定位置patch到内存中,当然 so是patch到内存,可是我们是文件啊,熟悉ELF结构的大佬应该知道,文件偏移地址和相对虚拟地址是需要转换的,涉及到解析ELF头的操作
当然这里有个简单的方案了,因为IDA加载so会自动的识别出虚拟地址所以我们直接在IDA中patch就好了
1 2 3 4 5 6 7 8 9 10 11 12 | import ida_bytes
def hex_to_bytes(filename):
with open (filename, 'r' ) as file :
hex_data = file .read().strip()
return bytes.fromhex(hex_data)
filename = r 'D:\Codes\Python\******\IDApy\*****\code.txt'
byte_data = hex_to_bytes(filename)
print ( len (byte_data))
addr = 0x12368
ida_bytes.patch_bytes(addr, byte_data)
|
这样就可以了吗?
虽然IDA已经能解析成功了,其实IDApatch也只能静态看看,跟玩具一样,没啥大不了的,我们需要做到的可是能放到真机运行啊!!!!!
我们通过ida patch的方案如果需要保存会出现以下错误,错误的原因还是在ELF中
- tip: 在可执行文件中,某些部分(例如,动态生成或修改的数据)可能没有直接映射到原始的二进制文件。这些区域在运行时可能由程序动态分配或修改。这些区域在IDA中可以看到,但由于它们没有在原始的二进制文件中有相应的部分,因此无法被patch。
Elf64_Xword p_filesz_SEGMENT_FILE_LENGTH 400064 60h 8h Fg:0xFF8080 Bg: Segment size in file
Elf64_Xword p_memsz_SEGMENT_RAM_LENGTH 993628 68h 8h Fg:0xFF8080 Bg: Segment size in ram
用010 ELF模版打开 找到报错的段 发现文件长度只到0x61AC0 但是内存中的长度确有0xF295C 所以我们如果patch大于0x61AC0地址的opcode自然会失败了
那我们手动把文件中的长度也改成993628就好了,当然这里涉及到文件是否足够容纳的问题,以后再跟大家探讨!
经过一系列的修改,当我们把所有段都修改好之后替换到真机上成功运行,并且执行了该so应该执行的功能!完活
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2023-7-13 11:54
被至尊小仙侠编辑
,原因: