有次我在调试某Android so库的时候,遇到一个很棘手的问题:如何更方便地对so库进行调试?
常规的Android下so库的调试方法是在Android环境里放一个gdbserver或者IDA的android_server,然后通过adb将端口转出来,使用本机GDB或者IDA进行远程连接调试。但是这种方法本身依赖于完整的Android环境、用起来比较麻烦且笨重不说,更重要的是这种方法无法实现一些比较高级的调试方式。比如在常规情况下,ARM Android环境里是无法设置内存断点watchpoint的,更不用说受限于调试原理无法实现高效的trace记录功能。
想要实现高效的trace,可以使用Frida 之类的DBI工具来进行插桩。Frida还提供了MemoryAccessMonitor API可以做内存监控,不过是通过修改页的权限实现的,粒度比较大,实际情况下并不好用,并且难以对栈上的数据进行监控。
常规调试下watchpoint功能的受限及trace的低效是由于我们是使用软件方式在用户态进行操作,受到了CPU及操作系统的限制。而如果能在CPU层进行操作,便可以进行更加精细的控制。但我们无法修改CPU硬件,一些CPU层的相关功能(如Intel PT )功能上也比较受限并且难以使用。但还有另一个方法,便是使用软件仿真的CPU。
软件仿真的CPU最有名的莫过于QEMU 了,通过它我们可以在一个CPU架构上仿真其他CPU架构的操作系统。但QEMU主要关注于仿真,对于安全分析来说并不友好。基于QEMU的PANDA框架 给我们提供了更多的相关接口,可以方便对特定位置做hook、进行插桩分析等。PANDA曾经的1.x版本还集成了DroidScope可以对Android环境进行调试,不过在最新版中把这个功能去掉了。PANDA因为要用软仿真实现的CPU运行完整操作系统,本身运行非常慢,但在PANDA中进行插桩操作由于相当于是在CPU层做的,带来的额外开销并不高。不过PANDA本身是全系统级的,虽然可以方便地对内核层进行分析,不过对于我们日常对用户态程序分析来说还是太重了。
如果是进行一些简单的仿真,目前最常用的是Unicorn引擎 。Unicorn是把QEMU的CPU功能剥离出来,给我们提供一个裸的CPU运行接口,并在特定的hook位置添加了回调接口。
不过如果想要使用Unicorn运行二进制程序的话还需要考虑程序的内存加载以及系统调用实现的问题,不能直接使用。而对于我们的需求,针对Android so的仿真调试来说,可以使用基于Unicorn的AndroidNativeEmu 、ExAndroidNativeEmu 、以及基于多个后端的unidbg 等工具。这几款工具中,只有unidbg提供了GDB的调试接口,并且主要是为IDA进行连接设计的,在32位情况下无法使用GDB进行连接,也不支持watchpoint。另一方面,这几款工具一个共有的缺陷是JNI中的函数是手工模拟实现的,难免存在实现不完全或者实现与真实情况有出入的情况。
前面这些方法都不能很好地满足我对Android so进行仿真调试的需求,我希望能在X86环境下通过仿真方式启起来一个完整的ARM Android Runtime环境,然后提供GDB接口可以连接上去进行相关调试。结合这个需求,一个新的基于Unicorn的仿真框架映入我的眼帘,那就Qiling仿真框架。
Qiling是一个高级的二进制仿真框架,并且它师出名门,是由Unicorn作者主导,主要解决的就是前面所提到的Unicorn无法直接运行二进制程序的问题。Qiling框架的关键就在于它完成了常规应该由操作系统来完成的一些事,如二进制程序内存加载、syscall功能提供,并且得益于Unicorn本身支持多种CPU架构的特性,使得Qiling成为了一个跨平台、跨架构的相对完善的仿真框架。作为一个框架,Qiling提供了一些接口和相关组件,其中一个很让我激动的功能就是Qiling自带一个GDB的远程调试接口,可以使用本地GDB进行连接调试。
不过Qiling框架支不支持运行Android so呢?Qiling提供了一个样例库 ,包含了不同操作系统、不同CPU架构的测试文件。我很高兴地看到,其中有arm64_android
这个文件夹,说明Qiling能运行ARM64架构的Android程序。不过我仔细研究才发现,这个样例与我的需求还有一些距离。原因在于这个程序只是在控制台打印了HelloWorld,并没有涉及到JNI相关操作。
既然这样的话,我们如果能给Qiling适配上对Android JNI的支持,就能满足前面的需求。按说我们已经有了AndroidNativeEmu这类同样基于Unicorn的Python项目,把其中的相关部分直接照搬进Qiling应该就可以了吧?
不过在我仔细研究后发现,Qiling框架与AndroidNativeEmu这类工具的底层原理是不同的。AndroidNativeEmu这类工具是模拟了Android下linker
的功能,把Android so及相关依赖加载到内存里,并手工实现了JNI的相关接口。但为了做成一个更为通用的框架,Qiling模拟的是操作系统加载ld.so
/linker
之类动态链接器的过程,后续把程序及相关依赖加载进内存完全是由动态链接器来做的。这就导致我们想用Qiling加载程序的话必须通过动态链接器完整启动ELF程序,而不是像AndroidNativeEmu那样铺设好环境加载好so就直接可以去调用so中的指定函数了。Qiling的这种做法,以最小的成本保证了对各类各个版本的系统最大的适配性,并且也保证了程序运行状态与真实环境差异较小。
由于JNI函数的实现都是在JVM里面,所以我们首先要解决的就是如何写一个控制台的ELF程序可以把JVM加载起来。网上有篇Creating a Java VM from Android Native Code 的文章就讲了如何直接加载起来JVM。不过这篇文章还是加载的libdvm.so
里面的Dalvik VM,对于今天来说未免太老了。结合Stackoverflow上的一个提问 及两个网页中的Github链接,最终形成了一个执行在Android命令行下的可以直接调起Android Runtime的程序,测试可以在Android 6.0下运行,并加载起来libart.so
:
可以通过Android NDK下的armv7a-linux-androideabi23-clang
和aarch64-linux-android23-clang
对该文件进行编译:
现在我们要做的就是让这个程序在Qiling下能运行起来。因为Qiling运行需要linker
以及相关的库,所以我们需要把Android的rootfs
复制过来,然后执行脚本:
不过事情怎么会这么简单,不出所料,果然失败了……
然后就是一番艰苦卓绝的分析调试过程,尝试运行,找到运行不下去的地方,然后对Qiling的相关部分进行分析修复,然后程序在这个地方能跑通了,又到了下一个运行不下去的地方,再分析再修复…… 这其中主要发现的问题是Qiling的syscall实现还不完善,以及Qiling本身、甚至是Qiling所依赖的Unicorn引擎中存在着一些bug。
其中的具体细节就不多说了,我把所做的修复都提交到了上游,在这里 可以看到我在这个过程中所有提交的PR。
在这个过程中我发现/dev/ashmem
和/dev/pmsg0
这两个设备文件不好模拟,所以修改了AOSP中的 USE_ASHMEM
和 FAKE_LOG_DEVICE
两个宏,重新编译了不访问这两个文件的版本。
我们可以只编译仿真所需要的部分:
编译生成的所有文件(rootfs):
运行脚本也需要进行一些相应的修改,最终版本脚本如下:
目前所有的改动都已经同步到qiling官方仓库,可以通过脚本test_android.py 进行体验。大家也可以选择我fork的仓库 ,里面自带了运行所需的文件,可以直接进行测试,下载下来之后只需要运行python android.py
便可看到创建完整ART环境的全过程(记得通过pip install -e .
安装相关依赖到最新版本)。我们可以看到运行的过程中共启动了6个线程,进行了上千次syscall调用,最终创建完成JavaVM
和JNIEnv
并调用JNI函数进行相关操作。
我们还可以打印出整个启动过程中的内存加载情况:
完成这些之后,如果想要运行一个Android so,我们可以通过dlopen
动态加载起来,通过dlsym
获取要运行的函数地址,然后将JNIEnv
作为第一个参数传进去运行。如果函数还需要其他什么参数的话,可以通过JNI进行创建。我们这里提供的是一个完整的Android Runtime环境,基本不用担心JNI实现不完全的问题(当然有可能有些JNI函数调用的syscall实现还不完全)。
总结:我这里通过Qiling模拟了启动Android Runtime的全过程,展示了Qiling对复杂系统的仿真能力,也是对Qiling各项功能的充分验证。这里我选择尝试模拟执行Android 6.0环境,一方面是因为手头有设备方便对照测试,一方面这是支持ART的比较早期的Android版本,相对来说代码量比较小运行过程比较简单,后面如果有谁有兴趣的话可以尝试对更高版本的Android环境进行仿真模拟。目前运行Android so的方法还有些笨拙,后面如果有谁有兴趣的话可以试着像Frida一样在此基础上添加Java接口,更加方便地进行相关调用。
前面我们使用Qiling仿真框架运行了一个完整的Android Runtime环境,我们可以利用Qiling的一些功能对整个过程进行细致的分析,从而更加深入地理解Android Runtime的启动过程。我们也可以对想要执行的Android so进行相关的分析。
如前面所说,选用Qiling框架一个比较重要的原因是它自带一个GDB的远程调试接口,那是不是就可以通过这个对Android Runtime进行调试呢?
然后我发现,事情并没有想象的那么美好…… 原因是,Qiling的GDB调试功能只能在单线程模式下使用 ,而运行Android Runtime需要多线程的支持。
那是不是可以对Qiling做一些修改,使得GDB调试功能在多线程下也能使用?在我阅读了Qiling相关代码后,发现这一点并不好实现,因为目前的GDB调试功能与整个执行过程耦合度太高了,难以迁移至多线程模式。
这就有点尴尬了,本来做这些工作的一个很大目的就是能在跑起来之后对Android so进行调试的,费了这么大力气跑起来了,却没办法进行调试。
又想到目前基于Unicorn的项目如果想支持GDB远程调试都要自己实现一套相关逻辑,那有没有办法给Unicorn提供一个通用的GDB调试功能呢?
说干就干!于是我就基于Unicorn自身提供的功能,实现了一套通用的GDB调试接口。不仅支持查看寄存器、查看内存、单步调试等基本操作,还支持了很多调试端都不支持的watchpoint功能,使得对数据的监控更加方便。
为了使得做出的项目更加通用,同时又能让实现比较优雅,代码通过Rust语言进行编写,编译成与C兼容的so文件,同时还提供了对Go、Java、Python等语言的接口。
这个项目的耦合度很低,只需要一个Code Hook便可以集成到已有的项目中。并且支持在正在运行的Unicorn实例的hook中进行调用,可以随时随地把调试接口启起来。
项目的地址在这里:https://github.com/bet4it/udbserver
在安装好so库和python bindings后,只需要在ql.run()
的前面加上两句
便可以在运行程序main函数的时候开启一个gdbserver,可以使用GDB远程挂上去进行调试。
注意因为这个项目是基于Unicorn实现的,所以并不支持Qiling自己实现的多线程功能。不过也是可以用的,开启udbserver的是哪个线程就对哪个线程进行控制。
我们同样可以在其他项目中使用udbserver,比如对于ExAndroidNativeEmu中的example_jni.py ,只需要加上两句:
便可以开启GDB远程调试功能。
还可以搭配使用我之前写的hyperpwn ,获得最佳的调试体验。
怎么样,是不是很方便?觉得我所做的工作对你有帮助的话记得在Github 上点一个Star!
typedef
int
(
*
JNI_CreateJavaVM_t)(void
*
, void
*
, void
*
);
JNIEXPORT void InitializeSignalChain () {}
JNIEXPORT void ClaimSignalChain() {}
int
init_jvm(JavaVM
*
*
m_jvm, JNIEnv
*
*
m_env)
{
JavaVMOption opt[
1
];
opt[
0
].optionString
=
"-Xnorelocate"
;
JavaVMInitArgs args;
args.version
=
JNI_VERSION_1_6;
args.options
=
opt;
args.nOptions
=
1
;
void
*
libart_dso
=
dlopen(
"libart.so"
, RTLD_NOW);
if
(!libart_dso )
return
-
1
;
JNI_CreateJavaVM_t JNI_CreateJavaVM;
JNI_CreateJavaVM
=
(JNI_CreateJavaVM_t)dlsym(libart_dso,
"JNI_CreateJavaVM"
);
if
(!JNI_CreateJavaVM)
return
-
1
;
signed
int
result
=
JNI_CreateJavaVM(&(
*
m_jvm), &(
*
m_env), &args);
if
( result !
=
0
)
return
-
1
;
return
0
;
}
int
main()
{
JavaVM
*
vm
=
NULL;
JNIEnv
*
env
=
NULL;
int
status
=
init_jvm(&vm, &env);
if
(status
=
=
0
) {
printf(
"Initialization success (vm=%p, env=%p)\n"
, vm, env);
}
else
{
printf(
"Initialization failure (%i)\n"
, status);
return
-
1
;
}
jstring testy
=
(
*
env)
-
>NewStringUTF(env,
"Hello world!"
);
const char
*
str
=
(
*
env)
-
>GetStringUTFChars(env, testy, NULL);
printf(
"JNI: %s\n"
,
str
);
return
0
;
}
typedef
int
(
*
JNI_CreateJavaVM_t)(void
*
, void
*
, void
*
);
JNIEXPORT void InitializeSignalChain () {}
JNIEXPORT void ClaimSignalChain() {}
int
init_jvm(JavaVM
*
*
m_jvm, JNIEnv
*
*
m_env)
{
JavaVMOption opt[
1
];
opt[
0
].optionString
=
"-Xnorelocate"
;
JavaVMInitArgs args;
args.version
=
JNI_VERSION_1_6;
args.options
=
opt;
args.nOptions
=
1
;
void
*
libart_dso
=
dlopen(
"libart.so"
, RTLD_NOW);
if
(!libart_dso )
return
-
1
;
JNI_CreateJavaVM_t JNI_CreateJavaVM;
JNI_CreateJavaVM
=
(JNI_CreateJavaVM_t)dlsym(libart_dso,
"JNI_CreateJavaVM"
);
if
(!JNI_CreateJavaVM)
return
-
1
;
signed
int
result
=
JNI_CreateJavaVM(&(
*
m_jvm), &(
*
m_env), &args);
if
( result !
=
0
)
return
-
1
;
return
0
;
}
int
main()
{
JavaVM
*
vm
=
NULL;
JNIEnv
*
env
=
NULL;
int
status
=
init_jvm(&vm, &env);
if
(status
=
=
0
) {
printf(
"Initialization success (vm=%p, env=%p)\n"
, vm, env);
}
else
{
printf(
"Initialization failure (%i)\n"
, status);
return
-
1
;
}
jstring testy
=
(
*
env)
-
>NewStringUTF(env,
"Hello world!"
);
const char
*
str
=
(
*
env)
-
>GetStringUTFChars(env, testy, NULL);
printf(
"JNI: %s\n"
,
str
);
return
0
;
}
/
opt
/
android
-
ndk
/
toolchains
/
llvm
/
prebuilt
/
linux
-
x86_64
/
bin
/
armv7a
-
linux
-
androideabi23
-
clang
-
Wl,
-
-
export
-
dynamic jniart.c
-
o arm_android_jniart
/
opt
/
android
-
ndk
/
toolchains
/
llvm
/
prebuilt
/
linux
-
x86_64
/
bin
/
aarch64
-
linux
-
android23
-
clang
-
Wl,
-
-
export
-
dynamic jniart.c
-
o arm64_android_jniart
/
opt
/
android
-
ndk
/
toolchains
/
llvm
/
prebuilt
/
linux
-
x86_64
/
bin
/
armv7a
-
linux
-
androideabi23
-
clang
-
Wl,
-
-
export
-
dynamic jniart.c
-
o arm_android_jniart
/
opt
/
android
-
ndk
/
toolchains
/
llvm
/
prebuilt
/
linux
-
x86_64
/
bin
/
aarch64
-
linux
-
android23
-
clang
-
Wl,
-
-
export
-
dynamic jniart.c
-
o arm64_android_jniart
from
qiling
import
*
ql
=
Qiling([
"android6.0/bin/arm64_android_jniart"
],
"android6.0"
)
ql.run()
from
qiling
import
*
ql
=
Qiling([
"android6.0/bin/arm64_android_jniart"
],
"android6.0"
)
ql.run()
make framework linker linker_32 libart libart_32 libstdc
+
+
libstdc
+
+
_32
make framework linker linker_32 libart libart_32 libstdc
+
+
libstdc
+
+
_32
├──
bin
│ ├── linker
│ └── linker64
├── framework
│ ├── arm
│ │ ├── boot.art
│ │ └── boot.oat
│ ├── arm64
│ │ ├── boot.art
│ │ └── boot.oat
│ ├── framework.jar
│ └── framework
-
res.apk
├── lib
│ ├── libart.so
│ ├── libaudioutils.so
│ ├── libbacktrace.so
│ ├── libbase.so
│ ├── libbinder.so
│ ├── libcamera_client.so
│ ├── libcamera_metadata.so
│ ├── libcommon_time_client.so
│ ├── libcrypto.so
│ ├── libc
+
+
.so
│ ├── libc.so
│ ├── libcutils.so
│ ├── libdl.so
│ ├── libEGL.so
│ ├── libexpat.so
│ ├── libGLES_trace.so
│ ├── libGLESv2.so
│ ├── libGLESv3.so
-
> libGLESv2.so
│ ├── libgui.so
│ ├── libhardware.so
│ ├── libicui18n.so
│ ├── libicuuc.so
│ ├── libjavacore.so
│ ├── libjavacrypto.so
│ ├── libkeymaster1.so
│ ├── libkeymaster_messages.so
│ ├── libkeystore_binder.so
│ ├── libkeystore
-
engine.so
│ ├── liblog.so
│ ├── libmedia.so
│ ├── libm.so
│ ├── libnativebridge.so
│ ├── libnativehelper.so
│ ├── libnbaio.so
│ ├── libpowermanager.so
│ ├── libprotobuf
-
cpp
-
lite.so
│ ├── librtp_jni.so
│ ├── libsigchain.so
│ ├── libsoftkeymasterdevice.so
│ ├── libsonivox.so
│ ├── libspeexresampler.so
│ ├── libssl.so
│ ├── libstagefright_amrnb_common.so
│ ├── libstagefright_foundation.so
│ ├── libstdc
+
+
.so
│ ├── libsync.so
│ ├── libui.so
│ ├── libunwind.so
│ ├── libutils.so
│ └── libz.so
├── lib64
│ ├── libart.so
│ ├── libaudioutils.so
│ ├── libbacktrace.so
│ ├── libbase.so
│ ├── libbinder.so
│ ├── libcamera_client.so
│ ├── libcamera_metadata.so
│ ├── libcommon_time_client.so
│ ├── libcrypto.so
│ ├── libc
+
+
.so
│ ├── libc.so
│ ├── libcutils.so
│ ├── libdl.so
│ ├── libEGL.so
│ ├── libexpat.so
│ ├── libGLES_trace.so
│ ├── libGLESv2.so
│ ├── libGLESv3.so
-
> libGLESv2.so
│ ├── libgui.so
│ ├── libhardware.so
│ ├── libicui18n.so
│ ├── libicuuc.so
│ ├── libjavacore.so
│ ├── libjavacrypto.so
│ ├── libkeymaster1.so
│ ├── libkeymaster_messages.so
│ ├── libkeystore_binder.so
│ ├── libkeystore
-
engine.so
│ ├── liblog.so
│ ├── libmedia.so
│ ├── libm.so
│ ├── libnativebridge.so
│ ├── libnativehelper.so
│ ├── libnbaio.so
│ ├── libpowermanager.so
│ ├── libprotobuf
-
cpp
-
lite.so
│ ├── librtp_jni.so
│ ├── libsigchain.so
│ ├── libsoftkeymasterdevice.so
│ ├── libsonivox.so
│ ├── libspeexresampler.so
│ ├── libssl.so
│ ├── libstagefright_amrnb_common.so
│ ├── libstagefright_foundation.so
│ ├── libstdc
+
+
.so
│ ├── libsync.so
│ ├── libui.so
│ ├── libunwind.so
│ ├── libutils.so
│ └── libz.so
└── usr
├── icu
│ └── icudt55l.dat
└── share
└── zoneinfo
└── tzdata
├──
bin
│ ├── linker
│ └── linker64
├── framework
│ ├── arm
│ │ ├── boot.art
│ │ └── boot.oat
│ ├── arm64
│ │ ├── boot.art
│ │ └── boot.oat
│ ├── framework.jar
│ └── framework
-
res.apk
├── lib
│ ├── libart.so
│ ├── libaudioutils.so
│ ├── libbacktrace.so
│ ├── libbase.so
│ ├── libbinder.so
│ ├── libcamera_client.so
│ ├── libcamera_metadata.so
│ ├── libcommon_time_client.so
│ ├── libcrypto.so
│ ├── libc
+
+
.so
│ ├── libc.so
│ ├── libcutils.so
│ ├── libdl.so
│ ├── libEGL.so
│ ├── libexpat.so
│ ├── libGLES_trace.so
│ ├── libGLESv2.so
│ ├── libGLESv3.so
-
> libGLESv2.so
│ ├── libgui.so
│ ├── libhardware.so
│ ├── libicui18n.so
│ ├── libicuuc.so
│ ├── libjavacore.so
│ ├── libjavacrypto.so
│ ├── libkeymaster1.so
│ ├── libkeymaster_messages.so
│ ├── libkeystore_binder.so
│ ├── libkeystore
-
engine.so
│ ├── liblog.so
│ ├── libmedia.so
│ ├── libm.so
│ ├── libnativebridge.so
│ ├── libnativehelper.so
│ ├── libnbaio.so
│ ├── libpowermanager.so
│ ├── libprotobuf
-
cpp
-
lite.so
│ ├── librtp_jni.so
│ ├── libsigchain.so
│ ├── libsoftkeymasterdevice.so
│ ├── libsonivox.so
│ ├── libspeexresampler.so
│ ├── libssl.so
│ ├── libstagefright_amrnb_common.so
│ ├── libstagefright_foundation.so
│ ├── libstdc
+
+
.so
│ ├── libsync.so
│ ├── libui.so
│ ├── libunwind.so
│ ├── libutils.so
│ └── libz.so
├── lib64
│ ├── libart.so
│ ├── libaudioutils.so
│ ├── libbacktrace.so
│ ├── libbase.so
│ ├── libbinder.so
│ ├── libcamera_client.so
│ ├── libcamera_metadata.so
│ ├── libcommon_time_client.so
│ ├── libcrypto.so
│ ├── libc
+
+
.so
│ ├── libc.so
│ ├── libcutils.so
│ ├── libdl.so
│ ├── libEGL.so
│ ├── libexpat.so
│ ├── libGLES_trace.so
│ ├── libGLESv2.so
│ ├── libGLESv3.so
-
> libGLESv2.so
│ ├── libgui.so
│ ├── libhardware.so
│ ├── libicui18n.so
│ ├── libicuuc.so
│ ├── libjavacore.so
│ ├── libjavacrypto.so
│ ├── libkeymaster1.so
│ ├── libkeymaster_messages.so
│ ├── libkeystore_binder.so
│ ├── libkeystore
-
engine.so
│ ├── liblog.so
│ ├── libmedia.so
│ ├── libm.so
│ ├── libnativebridge.so
│ ├── libnativehelper.so
│ ├── libnbaio.so
│ ├── libpowermanager.so
│ ├── libprotobuf
-
cpp
-
lite.so
│ ├── librtp_jni.so
│ ├── libsigchain.so
│ ├── libsoftkeymasterdevice.so
│ ├── libsonivox.so
│ ├── libspeexresampler.so
│ ├── libssl.so
│ ├── libstagefright_amrnb_common.so
│ ├── libstagefright_foundation.so
│ ├── libstdc
+
+
.so
│ ├── libsync.so
│ ├── libui.so
│ ├── libunwind.so
│ ├── libutils.so
│ └── libz.so
└── usr
├── icu
│ └── icudt55l.dat
└── share
└── zoneinfo
└── tzdata
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-6-6 22:07
被Bet4编辑
,原因: