前言
Q&A
Q:为什么写这篇文章?
A:因为一方面,目前还没有使用A64dbg工具进行实战并附有详细分析和使用的文章,此文算是新品开箱的尝鲜文章,分享给各位看雪用户。另一方面,作为安卓逆向爱好者,拿到新的逆向工具还是比较激动的,在进行了不少尝试后,还是希望分享给大家一些有用的东西的。
Q:文章所涉及的功能,是A64dbg不同于其他逆向工具的全部了吗?
A:当然不是,涉及的只是我个人最感兴趣的功能——即A64dbg的UnicornVM调试模式下的ADCpp脚本系统。
Q:看完文章后去下载A64dbg可以复现这篇文章吗?
A:可能不行,我是拿到了A64dbg的测试授权,在授权情况下拥有完整功能后才进行的测试,未授权的话可能会有些限制,但我并没有具体地测试此文使用到的A64dbg功能被限制到了什么程度,如果感兴趣的话可自行尝试。
Q:文中写的内容好像用unidbg也可以做到,有什么区别吗?
A:unidbg很强大,但A64dbg的UnicornVM模式并不同于unidbg,怎么简洁明了地区别理解它们呢,那就是unidbg需要模拟linux系统和java环境,而A64dbg的UnicornVM模式并没有模拟这些,是在设备机中启动了lidadbg-server然后客户端调试器进行对接,也就是说是运行在真实的环境中的。
关于A64dbg的ADCpp脚本系统
最近上手了A64dbg的unicornVM模式的下调试,分析样本为so文件动态加解密的CrackMe一文中进行了360加固的crackme。
先要说明的是,在了解A64dbg之后,首先吸引到我的是A64dbg的ADCpp脚本系统,先看一下介绍。

IDA有IDC这样的类C脚本系统,Frida嫁接了一层JavaScript脚本系统。虽然用起来还行,但始终不能让人满意。因为与Native打交道,就应该像C/C++那样直白,毕竟操作void *才是Native的精华。受限于C/C++是编译型的静态语言,想要实现像Python/JavaScript那样的便捷性着实不易。
LLDB的表达式倒是可以使用C/C++语句,但是高度依赖类型系统,否则难以写出有效的C/C++表达式。即便如此,还是太弱了,不能写出复杂的完整C/C++函数。
但是,这一切即将成为过去时,我们将在A64Dbg v1.6专业版中引入UnicornVM的时候同时引入ADCpp这个C/C++脚本系统,它是把C/C++定义为了编译型动态语言,使用解释执行的方式运行代码。C/C++最终编译为机器相关的arch.adc字节码,由UnicornVM直接加载执行。不需要手动指定头文件、不需要手动指定链接库文件、不需要手动加载动态库,你只需要code,剩下的就是交给ADCpp了。
你还可以在A64Dbg端定义数据接收的Python3函数,然后在C/C++端发送给它,类似于Frida JavaScript与Python的交互,提供ADCpp接口可以主动调用内存模块中的函数,或者是只要有一个指针即可。
——A64Dbg-ADCpp脚本系统简介
就是说通过ADCpp脚本系统你可以实现使用C/C++代码来完成对内存中模块函数的注入hook,或者是进行主动调用,甚至只要有一个指针地址就可以实现,并且不管是系统空间还是用户空间。
基于对此功能的好奇,我进行了简单的测试,尝试使用A64dbg主动调用socrackme的解密函数,随后完成dump解密的so文件,以测试A64dbg在安卓逆向中内存操作的实战效果,这里将测试结果分享给大家。
360旧版加固的vm与反调试
在开始之前,先来说下so文件动态加解密的CrackMe一文所做的分析,作者是看雪用户genliese 。genliese在文章开头说的很清楚,因为360加固的反调试所以so文件采用全静态分析,这是全文的基调。
关于反调试,笔者简单分析了下此版本的360加固,发现是典型的旧360壳,判断标志是libjiagu.so中存在__fun_a_18
函数,而反调试就在里面的case当中,在论坛上几年前就已经有相关的分析文章了,最早的应该是某数字公司VMP脱壳简记。
__fun_a_18
函数的流程图看上去像是被加了ollvm控制流平坦化,但其实是一个基于堆栈的vm,找了很多资料,国内应该没有详细分析此vm的文章,但是论坛里有一篇翻译老外分析360加固的文章(老外挑战360加固--实战分析(很详细) ),里面有详细说这个vm,并且在github有相关还原代码。

众所周知,旧版360加固的学习价值很高,是入门者练手分析很好的素材,这里我整理了些相关文章,列出来方便大家学习:
现在回过头来说下反调试,旧版360加固主要有时间反调试、rtld_db_dlactivity反调试、traceid反调试和IDA端口反调试。
其中rtld_db_dlactivity反调试和traceid反调试是针对ptrace方式调试的反调试,在使用A64dbg的UnicornVM模式调试的测试过程中,笔者并没有发现触发反调试,说明A64dbg-UnicornVM模式的调试机制并不依赖于ptrace,也就是能直接无视掉此壳的反调试。
如果大家想尝试使用IDA进行调试分析,过反调试的话可以参考这篇文章360加固保分析。
so文件分析
文章中genliese使用FART定制ROM进行了脱壳,笔者进了测试,这个版本的360壳并没有进行指令抽取及vmp,使用dex整体dump即可脱壳。
dex的代码逻辑很简单,输入传进native层的test函数进行验证。

首先libnative-lib.so的.init_array中的函数进行了字符串解密,

我们跳过,直接看下JNI_OnLoad函数,可以看到字符串还没有被解密出来,以及java层的test方法应该是关联到了native层的ooxx函数。

我们来看ooxx函数。
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 49 50 51 52 53 | .text: 00008DC4 EXPORT ooxx
.text: 00008DC4 ooxx ; CODE XREF: j_ooxx + 8 ↑j
.text: 00008DC4 ; DATA XREF: LOAD: 000007C0 ↑o ...
.text: 00008DC4
.text: 00008DC4 var_2C = - 0x2C
.text: 00008DC4 var_28 = - 0x28
.text: 00008DC4 var_24 = - 0x24
.text: 00008DC4 var_18 = - 0x18
.text: 00008DC4 var_14 = - 0x14
.text: 00008DC4 var_10 = - 0x10
.text: 00008DC4
.text: 00008DC4 ; __unwind {
.text: 00008DC4 PUSH {R4 - R7,LR}
.text: 00008DC6 ADD R7, SP,
.text: 00008DC8 SUB SP, SP,
.text: 00008DCA MOV R3, R2
.text: 00008DCC MOV R12, R1
.text: 00008DCE MOV LR, R0
.text: 00008DD0 STR R0, [SP,
.text: 00008DD2 STR R1, [SP,
.text: 00008DD4 STR R2, [SP,
.text: 00008DD6 STR R3, [SP,
.text: 00008DD8 STR .W R12, [SP,
.text: 00008DDC STR .W LR, [SP,
.text: 00008DE0 BL sub_8930
.text: 00008DE4 MOV R0, R0
.text: 00008DE6 MOV R0, R0
.text: 00008DE8 MOV R0, R0
.text: 00008DEA MOV R0, R0
.text: 00008DEC MOV R0, R0
.text: 00008DEE MOV R0, R0
.text: 00008DF0 MOV R0, R0
.text: 00008DF2 MOV R0, R0
.text: 00008DF4 MOV R0, R0
.text: 00008DF6 MOV R0, R0
.text: 00008DF8 MOV R0, R0
.text: 00008DFA MOV R0, R0
.text: 00008DFC MOV R0, R0
.text: 00008DFE MOV R0, R0
.text: 00008E00 BX R0
.text: 00008E00 ; End of function ooxx
.text: 00008E00
.text: 00008E00 ; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
.text: 00008E02 DCW 0x4502
.text: 00008E04 DCD 0x41064304 , 0x4D0A4F08 , 0x490E4B0C , 0x55125710 , 0x51165314
.text: 00008E04 DCD 0x5D1A5F18 , 0x591E5B1C , 0x65226720 , 0x61266324 , 0x6D2A6F28
.text: 00008E2C DCD 0x692E6B2C
.text: 00008E30 DCD 0x75327730
.text: 00008E34 DCD 0x71367334
.text: 00008E38 DCD 0x7D3A7F38 , 0x793E7B3C
.text: 00008E40 DCD 0x5420740
.text: 00008E44 DCD 0x1460344
.text: 00008E48 DCD 0xD4A0F48
|
可以看到初始化后跳到了sub_8930,而sub_8930函数中,我们看到了十分明显的内存指令解密标志mprotect
以及cacheflush
函数。
至于如何具体进行的内存指令解密操作,so文件动态加解密的CrackMe一文中十分详细的讲解了,这里我就不多说了,此crackme是一个很好的指令解密学习素材,大家感兴趣的话可以动手分析下。可以猜到,sub_8930进行内存指令解密后,便是开始执行真正的代码了,就是对输入进行校验。

到这一步可以知道,想要拿到flag就需要知道被加密的代码是什么。genliese采取的方式是使用fridad动态dump内存中的加密指令的解密密钥,然后再使用密钥静态解密文件中的指令加密部分,然后IDA即可反编译出解密后的ooxx
函数,以及不得不称赞的是genliese的代码写的很漂亮,让人看的很舒服。
A64dbg-ADCpp测试
好了,下面就开始进入此文的关键的部分,对A64dbg的ADCpp脚本系统的内存操作功能进行测试,这里测试的主要是ADCpp脚本系统的主动调用和内存dump特性,我们的测试思路如下所述。
尽管我们可以在指令解密完后设下断点,然后输入flag点击触发断点随后dump内存。但是我们并不这样做,而是选择让程序保持运行,使用ADCpp脚本系统主动调用指令解密函数ooxx
,然后让断点停在指令解密完后,随后进行dump so文件。
也就是说程序一方面正在运行等待我们的输入,光标还在闪动,而另一方面,我们已经悄无声息地开辟了一条新的战线,执行了内存里的函数,并停在了我们设下的断点,然后进行了dump操作。
至于为什么在解密完的时刻设下断点,让调试器在这里停在这里,因为ooxx
函数解密出来的指令在对输入进行校验后,还会将ooxx
再加密回去。
启动lidadbg-server
我们在设备机里面启动lidadbg-server
后,然后打开A64dbg,Options->Preferences里客户端进行设置。
填好Remote Android栏下的ip和port,接着填好adb的路径,以及最重要的是Default Platform选择Remote UnicornVM Android,这样才能进行UnicornVM模式下的调试。

然后运行Apk,点击File->attach,然后搜索包名attach即可。

设置断点
第一次加载会比较慢,等待加载好后,点击Symbols
窗口,左下角搜索模块名称libnative-lib.so
,右下角搜索函数ooxx
,双击。

进入汇编窗口,可以看到跳到了sub_8930
函数,双击。

然后找到指令后解密完后,调用cacheflush
的地方,设下断点。

ADCpp脚本系统主动调用解密函数ooxx
现在断点已经设置好了,按照我们的计划,要开始进行主动调用了。
ADCpp脚本运行有两种方式,一种是直接载入脚本,另一种则是通过最下方的命令行窗口,这里我们都尝试一下。
然后主动调用函数也有两种方式,一种适用于有符号的函数,我们通过extern "C" void ooxx();
就可以引用A64dbg解析好的有符号的函数,可以直接调用;另一种则是知道函数在内存中的地址后,我们将绝对地址转化为函数指针然后进行调用,这时候要注意汇编是否在thumb模式下。
方式一
方式一,从解析出来的模块符号表中找到ooxx
函数,然后进行调用。
具体操作是将如下代码保存文件,后缀名为.cc
,然后File->ADCpp Script,找到保存的文件,双击。
1 2 3 4 5 | extern "C" void ooxx();
void adc_main_thread() {
ooxx();
}
|
方式二
比如你知道了ooxx
函数在内存的起始地址为0x859EDC4
,thumb
模式下进行|1
,然后将这个绝对地址转换为函数指针,即可进行调用。
具体操作是在最下面的Command窗口,选择ADCpp
模式,输入((void(*)())(0xD859EDC4 | 1))();
,然后回车。

效果

最后它们都会在ooxx
里设置的断点停下来。
Python脚本Dump内存中已解密so文件
在这时候内存中的so文件已经完成了解密,我们dump下来即可,我们该如何dump呢?还记文章开头那张图的右上角是什么吗,我们来看一下。

没错,我们是可以使用python3来处理ADCpp脚本系统的返回结果的,实际上,ADCpp脚本系统也提供给python丰富的接口去做很多事情,诸如操作内存、断点等,如果感兴趣的话下载A64dbg后可以查看A64dbg/python3/adp.py
文件。

这里我们只需要dump内存,使用readMemory一个接口就足够了。
1 2 3 4 5 | def readMemory(addr, size):
return api_proc_result( 'readMemory' , (addr, size))
|
然后我们要确定so文件在内存中的分布。
首先我们执行ADCpp的commd,即printf("pid: %d",getpid())
,获取到pid,这里使用到了ADCpp的内置接口getpid()
。

然后切换TarShell模式,这里就相当于android设备的shell了。

然后执行cat /proc/10138/maps | grep libnative-lib.so
,查看应用在内存中的分布。

我们可以看到so文件在内存中分布在两块区域中,0xd861d000-0xd8636000
和0xd8637000-0xd863a000
。
接着我们写好python脚本,使用readMemory
接口对so文件进行dump。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | start1 = 0xd861d000
end1 = 0xd8636000
start2 = 0xd8637000
end2 = 0xd863a000
def dump(start,end):
page_bytes = b''
for i in range ((end - start) / / 0x1000 ):
page_bytes = page_bytes + readMemory(start + i * 0x1000 , 0x1000 )
return page_bytes
so_bytes = b''
so_bytes = so_bytes + dump(start1,end1)
so_bytes = so_bytes + dump(start2,end2)
with open ( "D:\\so_dump.so" , 'wb' ) as f:
f.write(so_bytes)
f.close()
|
接着点击Files->Python script,然后找到保存的python脚本,点击即可。
此时装载到内存中的so文件就已经被dump下来了,我们使用二进制对比工具进行下对比。

可以看到中间的是解密部分不同,以及内存中dump下来的so文件缺少section表,如果感兴趣的话可以手动修复。
效果验证
然后就到了验证效果的时刻了,我们把dump下来的so文件拖进IDA,IDA会提醒section表解析错误,我们略过继续打开,然后搜索ooxx
函数,可以看到ooxx
已经被解密了。

好了,到现在我们的测试已经按预想中的完成了。
后记
怎么评价A64dbg?
整体评价目前我不太好说,原因是目前我还只是尝试了使用A64dbg的UnicornVM模式调试下的ADCpp脚本系统,进行了简单的安卓逆向测试。
但是如果单是讲ADCpp脚本系统的话,我只能说A64dbg给我带来了焕然一新的so层逆向体验,在测试另一个加固apk时候,只写了几行C代码进行主动调用字符串解密函数,就输出了所有加密字符串,这无疑给静态分析带来了极大的便利,是使用frida操作native层所不能及的。当然也可能是我的水平火候不够,不能站在一个更高的角度上去对比分析。
以及目前我只是测试了主动调用和内存操作的特性,使用C/C++进行so注入hook还在探索中,但是这几句话你能想到什么呢?我想到了可以直接把安卓源码中的原生头文件包含进来,然后使用ADCpp脚本系统去注入hook,只需要少量并且简洁的C/C++代码就能实现一个脱壳机插件。
当然这只是个猜想,因为目前A64dbg还不太稳定成熟,不依赖ptrace的断点机制多少有些不稳定,以及A64dbg仍然缺少大量实战的洗礼,很多bug都在埋伏隐藏中,只有在实战中短兵相接才会暴露出来。
但是我还是很期待有天A64dbg能成熟稳定到支撑起一个脱壳机插件的运行,也希望到时候我有足够强的实力水平来驾驭这个工具,因为我也是在不断使用A64dbg进行尝试时想明白了一个道理,工具永远是次要的,最重要的一直都是使用工具的人。
论坛一种通过后端编译优化脱混淆壳的方法帖子下有个评论让我印象一直很深刻,在这里分享给大家。

[培训]科锐逆向工程师培训 48期预科班将于 2023年10月13日 正式开班
最后于 2021-4-7 10:36
被0x指纹编辑
,原因: