主要参考《CTF训练营》
Android是一种基于Linux的开源的操作系统,主要用于移动设备,如智能手机和平板电脑
自2008年以来,Android已更迭了许多版本:
Android系统架构:
Android Studio 是用于开发 Android 应用的官方集成开发环境 (IDE),以 IntelliJ IDEA 为基础构建而成
下载链接:https://developer.android.com/studio
Android Studio项目结构主要包括:
Android SDK是开发Android应用程序所需的软件开发工具和库的集合,谷歌每发布一个新的Android版本或更新版本,就会发布相应的SDK,开发者必须下载并安装。
在导航栏可以打开sdk manager。
sdk manager:
Android SDK包含了从头开始编写程序一直到进行测试所需的所有工具, 这些工具使得从开发、调试到打包的整个开发过程非常顺畅:
Android NDK 是一个工具集,可使用 C 和 C++ 等语言以原生代码(native code)实现应用的各个部分,其主要功能为:
Android NDK构建原生应用时使用的主要组件:
Native层方法有两个特征:
将上面的代码命名为MyJni.java,运行javah MyJni
命令,会在同目录下生成MyJni.h文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class MyJni */
#ifndef _Included_MyJni
#define _Included_MyJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: MyJni
* Method: getPart3
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_MyJni_getPart3
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
包含对函数Java_MyJni_getPart3
的说明:
jstring
编写MyJni.c实现函数:
#include "MyJni.h"
JNIEXPORT jstring JNICALL Java_MyJni_getPart3(JNIEnv *env, jclass obj)
{
return (*env)->NewStringUTF(env, "Just a test!");
}
然后新建两个文件Android.mk和Application.mk修改编译参数:
Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := MyJni
LOCAL_SRC_FILES := MyJni.c
include $(BUILD_SHARED_LIBRARY)
Application.mk:
APP_ABI := all
然后将这些文件放在jni文件夹中,使用ndk-build
编译:
MyJni.getPart3方法调用后,JNI需要连接到库中的相应函数,因此必须知道 Java 声明的Native方法与so库中函数的配对关系,配对的方式主要有两种:
使用JNI Native方法名称解析的动态链接
Java_com_example_mobilenormal_MyJni_getPart3
Java
,一个完整的类名com_example_mobilenormal_MyJni
,原方法名称getPart3
使用Registernative API调用的静态链接
(*env)->RegisterNatives(env, class, method, numMethods)
,调用该函数,即可动态注册Native函数
Application.mk不变,Android.mk中把后缀c换成cpp
Myjni.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
#include
#include
#include
/* Header for class MyJni */
#ifndef _Included_MyJni
#define _Included_MyJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: MyJni
* Method: getPart3
* Signature: ()Ljava/lang/String;
*/
jstring check
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
Myjni.cpp
#include "MyJni.h"
//要动态注册的方法
jstring check(JNIEnv* env, jclass jclass1)
{
return env->NewStringUTF("123");
}
//方法对应表
static JNINativeMethod gMethods[] = {
//第一个参数为native方法名称;第二个参数为native方法参数;第三个参数为lib库中方法名称
{ "check", "()Ljava/lang/String", (void*)check }
};
//为某一个类注册本地方法
static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
//完整类名
#define JNIREG_CLASS "MyJni"
//为所有类注册本地方法
static int registerNatives(JNIEnv* env)
{
if (!registerNativeMethods(env, JNIREG_CLASS, gMethods, sizeof(gMethods) / sizeof(gMethods[0])))
return JNI_FALSE;
return JNI_TRUE;
}
//在系统加载库时调用,如果成功则返回JNI版本,失败则返回-1
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
if (!registerNatives(env)) {//注册
return -1;
}
return JNI_VERSION_1_4;
}
观察加载lib库的过程:
在Android 7.0.0_r1中,关键的调用源码在JavaVMExt::LoadNativeLibrary中:
android::OpenNativeLibrary主要功能是调用Linker(Android系统中的连接器)的dlopen函数加载Lib库
dlopen调用时会搜索目标lib文件代码中是否包含init_array段,如果包含这个段,就会在加载时运行
下面的范例可以在init_array中添加内容:
void my_init(void) __attribute__((constructor));
void my_init(void)
{
//运行(*env)->RegisterNatives
}
dlopen调用完成后,将查找lib库中是否有名为JNI_OnLoad的导出函数
void* handle = android::OpenNativeLibrary(env,
runtime_->GetTargetSdkVersion(),
path_str,
class_loader,
library_path);
sym = library->FindSymbol("JNI_OnLoad", nullptr);
APK文件是Android应用程序包,本质是一个ZIP格式的压缩包,可直接解压
虽然Android平台使用Java语言开发应用程序,但Android程序并非运行在标准的Java虚拟机上,而是运行在谷歌专门为其开发的Dalvik虚拟机上。但是Android4.4以后,为了解决UI卡顿、显示延迟等性能问题,引入了全新的ART(Android RunTime)虚拟机,抛弃了Dalvik虚拟机。
Dalvik虚拟机与Java虚拟机的主要区别:
在编译过程中,将Java代码编译成Dalvik字节码
在逆向过程中,需要将DEX文件中的二进制的Dalvik字节码,先反汇编得到Smali代码(类似于汇编代码),再解析得到Java代码(类似于C++代码)
下图展示了Java代码在JVM虚拟机和Dalvik虚拟机上编译执行的区别:
下图展示了Java代码在Dalvik虚拟机和ART虚拟机上编译执行的区别:
图片参考链接:https://www.youtube.com/watch?v=m9UZnWLLurY
对于下面的Java代码:
public String func(int i, int j) {
return String.valueOf(i + j -i / j * 3);
}
Smali代码为:
.method
开始方法定义.end method
结束方法定义func
为方法名,I
表示整型,String
为返回值类型.registers 5
使用5个寄存器,访问标志位为0,2个局部变量名字为i和jinvoke-static
表示调用静态方法:String(调用者)->valueOf(方法名)(I(参数类型))String(返回值类型), v0(参数)
.method public func(I, I)String
.registers 5
.annotation system MethodParameters
accessFlags = {
0x0,
0x0
}
names = {
"i",
"j"
}
.end annotation
00000000 add-int v0, p1, p2
00000004 div-int v1, p1, p2
00000008 mul-int/lit8 v1, v1, 3
0000000C sub-int/2addr v0, v1
0000000E invoke-static String->valueOf(I)String, v0
00000014 move-result-object v0
00000016 return-object v0
.end method
除了I
表示整型外,smali数据类型还包括:
smali语法 | 类型 |
---|---|
V | void |
Z | boolean |
B | byte |
S | short |
C | char |
F | float |
I | int |
J | long |
D | double |
L | Java类类型 |
[ | array |
在Dalvik的字节码中,寄存器始终为32位,并且可以存储任何类型的值。2个寄存器用于容纳64位类型(Long和Double)。
寄存器有两种命名方案:
假如一个方法有3个参数,总共有5个寄存器:
v命名法 | p命名法 | |
---|---|---|
v0 | the first local register | |
v1 | the second local register | |
v2 | p0 | the first parameter register |
v3 | p1 | the second parameter register |
v4 | p2 | the third parameter register |
非静态方法的第一个参数总是调用该方法的对象。比如方法LMyObject;->callMe(II)V
有2个整数参数的同时,也在整数参数前有一个隐式的LMyObject
参数,所以该方法总共有3个参数。
参考链接:https://tryhackme.com/room/androidhacking101
如果没有root后的手机,可以使用模拟器在PC上测试和运行Android程序。
除了android studio自带的模拟器之外,Genymotion模拟器也很好用,个人可以使用免费版。
下载带VirtualBox的版本容易出错,建议分别下载Genymotion和VirtualBox
Genymotion安装教程: https://blog.csdn.net/changsimeng/article/details/63253582
由于Genymotion的底层为x86架构,为了运行一些APK,需要在模拟器中安装ARM翻译器,在下面的链接中找到相应API版本的翻译器,直接拖入打开的模拟器中,安装完成后重启模拟器即可:
ARM翻译器:https://github.com/m9rco/Genymotion_ARM_Translation
Android Debug Bridge (ADB)是一个开发工具,方便Android设备和个人电脑之间的通信。包括以下三个组件:
当启动某个 adb 客户端时,该客户端会先检查是否有 adb 服务器进程正在运行。如果没有,它会启动服务器进程。服务器在启动后会与本地 TCP 端口 5037 绑定,并监听 adb 客户端发出的命令 - 所有 adb 客户端均通过端口 5037 与 adb 服务器通信。
然后,服务器会与所有正在运行的设备建立连接。它通过扫描 5555 到 5585 之间(该范围供前 16 个模拟器使用)的奇数号端口查找模拟器。服务器一旦发现 adb 守护程序 (adbd),便会与相应的端口建立连接。请注意,每个模拟器都使用一对按顺序排列的端口 - 用于控制台连接的偶数号端口和用于 adb 连接的奇数号端口。例如:
模拟器 1,控制台:5554
模拟器 1,adb:5555
模拟器 2,控制台:5556
模拟器 2,adb:5557
# 查看设备
adb devices -l
# 将命令发送至特定设备
adb -s emulator-5555 install helloWorld.apk
# 将远程的apk拉取到本地
adb install path_to_apk
# 设置从主机端口到设备端口的转发
adb forward tcp:6100 tcp:7100
# 从设备复制文件
adb pull remote local
# 将文件复制到设备
adb push local remote
# 停止 adb 服务器
adb kill-server
# 发出 adb 命令
adb [-d | -e | -s serial_number] command
# 发出 shell 命令(单个)
adb [-d |-e | -s serial_number] shell shell_command
# 启动交互式 shell
adb [-d | -e | -s serial_number] shell
# 退出交互式 shell
exit
# 调用 Activity 管理器 (am)
adb shell am start -a android.intent.action.VIEW
# 调用软件包管理器 (pm)
adb shell pm uninstall com.example.MyApp
参考链接:https://developer.android.com/studio/command-line/adb
常用的逆向分析软件有Android Killer/jadx/APK Studio/JEB,JEB全称JEB Decompiler,由PNF Software公司开发,是一款闭源商业软件,支持对APK、DEX、Jar文件的反编译
打开JEB,将APK文件拖入其中,得到文件的逆向结果。
双击打开MainActivity字节码,右键选择解析/按TAB键,可以看到反编译得到的MainActivity类的Java代码。一般反编译先从MainActivity入手,查看函数逻辑。
JEB逆向AndroidManifest.xml,将其解析为可读的格式,根据该文件找到该APK的启动Activity。
启动Activity的标志为包含
和
属性。
在拿到APK后一般第一个查看这个清单文件,首先要查看APK包含几个Activity,然后找到该APK的启动Activity;留意APK有没有定义其他组件,如Service、Receiver等,它们可能会用来实现不同进程的RPC调用;关注APK所需的权限,寻找可能的攻击面。
解密原resources.arsc文件,得到多个XML文件存放在res/values/目录下
res/values/目录下的XML文件中,重要的是public.xml和strings.xml。
public.xml中存放着Android程序中所使用的的ID与类型、变量名之间的关系,如果反编译后的代码中有”R.id.xxx”或者”find-ViewById(xxx)”形式的代码,只需要到public.xml中查找该ID对应的变量类型和变量名。
然后再到相应的文件如strings.xml中查找相应的值。
Dalvik虚拟机支持调试,实现了JDWP(Java Debug Wire Protocol,Java调试有线协议),可以支持使用JDWP的调试器来调试Android程序。
Dalvik虚拟机为实现JDWP加入了DDM(Dalvik Debug Monitor,Dalvik调试监控器)特性,可以使用DDMS(Dalvik Debug Monitor Server,Dalvik调试监控器服务)查看。DDMS主要用于实现设备截屏、查看线程信息、文件预览、模拟来电、模拟短信、模拟GPS信息等功能。在SDK目录的tools文件夹下,双击monitor.bat就可以启动DDMS。
Dalvik虚拟机在启用调试后都会启动一个JDWP线程,等待打开DDMS或者调试器连接。
为了使Dalvik虚拟机能够启用调试,有以下两种可能:
元素中包含了android:debuggable="true"
在默认情况下,使用Android AVD生成的模拟器的ro.debuggable属性为1,可以直接用于调试。
如果属性为0,可以使用Apktool对APK进行反编译,修改AndroidManifest.xml文件,添加上android:debuggable="true"
属性后再重打包回去。
在使用JEB连接之前,一定将adb添加到环境变量中。adb属于platform-tools,可以在Android Studio中下载,也可以单独下载
adb在电脑中所在位置可以参考:
运行APK,不然找不到进程
打开调试器
检查APP名称是否正确,点击附上
JEB右侧出现VM栏:
在MainActivity的smali代码中,按照需求选择获取所需返回值的下一行,然后ctrl+B下断点:
在手机APP中输入flag,点击提交:
能够在JEB的局部变量窗口看到变量值:
将v4变量的int类型修改成正确的string类型,就可以获取到正确的返回值
log调试是传统的动态调试方法,通过修改反编译后的BakSmali汇编代码,加入自定义的语句,可以实现打印信息、修改执行流程、篡改返回值等功能。
但是使用log调试,需要对APK进行重打包,不适用于使用了完整性校验、签名校验等保护措施的APK。
常用的log静态方法都位于android.util.Log中:
Log.v(String tag, String msg);
Log.d(String tag, String msg);
Log.i(String tag, String msg);
Log.w(String tag, String msg);
Log.e(String tag, String msg);
这些方法都需要两个参数:tag参数和要打印的字符串变量,一般从上下文中选取一个不再使用的局部变量,用来存储tag字符串。
将APK文件拖入Android Killer工具中。
在50行之前输入:
invoke-static {v5, v5}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I
保存文件修改后,点击编译重打包APK,然后在生成路径下找到打包后的文件。
在模拟器上安装APK,使用adb logcat
查看log输出信息,然后模拟器触发事件,就可以看到相应的log。
ProGuard混淆是Android SDK自带的混淆器,能够将类名、方法名、变量名等标识符进行混淆,修改为无意义的字母组合。
ProGuard混淆开启方式非常简单,只要在编译之前将build.gradle配置文件中的minifyEnabled属性设置为true即可。
ProGuard混淆不会混淆所有的类名方法名变量名,如果要查看默认不对哪些文字做修改,可以查看混淆设置,位于Android SDK目录下的tools/proguard/proguard-android.txt文件中。
可以使用JEB的交叉引用和重命名功能,将看懂的方法改为容易辨识的名字。
部分题目会将classes.dex等文件的部分字段改掉,改掉的部分不影响APK在手机中的正常运行,但是会影响Apktool对反编译的处理,使得Apktool进入异常处理流程,最终退出反编译。
两种解题思路:
查看Apktool报错信息,回溯哪些字段出现问题
找到正常的APK文件进行对比,看是否能找到异常的字段
APK文件本质上是一个ZIP文件,将加密字段设置为1,达到伪加密效果,不影响APK运行,但是影响反编译。
原理是修改标记为“P K 01 02”的连续4位字节后的第5位字节,1表示加密,0表示不加密。
去除伪加密,只需要将其设置为0。
APK有标志头和标志尾,在尾部附加信息不影响程序运行但是会影响在电脑中的解压缩。
也称为DEX加壳,将真正执行的DEX隐藏在me讴歌位置。
APK执行时执行解壳程序,将真正的DEX解密出来,再使用DexClassLoader动态加载。
由于DexClassLoader加载DEX文件前需要将其首先保存在文件中,所以可以关注调用DexClassLoader和保存文件的位置。
NDK生成的原生库一般被包含在APK的/lib//lib.so路径下,因为原生代码是针对特定的cpu编译的,如果需要APK运行在超过一种类型的硬件上,就必须在APK中包含原生库的每个编译版本。
IDA查看标准的Native方法命名的函数:
IDA查看JNI_OnLoad函数:
IDA查看init_array段:
首先打开Segments窗口:
双击进入init_array字段:
双击查看函数中隐藏代码:
这里以ISCC2021的LOCKK题目为例,介绍IDA动态调试的流程。
打开题目附件中armeabi-v7a中的libLibs.so,拖入IDA中分析,发现包含了简易的反调试信息:
需要用IDA patch掉这个反调试,才能让IDA之后成功attach到进程上。
在JNI_OnLoad函数中,通过Edit->Patch program->Change byte,输入4个00,将反调试patch掉。
此时再点击F5进行反编译,发现已经没有反调试这句代码了:
然后将修改保存到文件中:
在IDA目录下的dbgsrv文件夹中找到android_server(因为这个适用于32位arm架构,64位arm处理器则需要android_server64文件),然后将该文件传输到手机端的/data/local/tmp文件夹下:
然后输入adb shell,进入模拟器的/data/local/tmp文件夹,将android_server的权限设置为777,然后运行该文件:
刚才的cmd窗口不要关,再打开一个新窗口,输入:adb forward tcp:23946 tcp:23946,完成端口转发:
将patch后的so文件放到运行的app文件夹中,先找到app包所在路径:pm list packages -f | grep lockk
接下来查看原始so库的情况:
然后把打过补丁的so库push到该文件夹中:adb push C:\Users\XXXX\Desktop\libLibs.so /data/app/com.iscclockk-1/lib/arm/。如果报错权限不够,需要用adb root语句。(如果使用了adb root语句,会重启adb,前面的android_server运行会关闭,需要重新运行android_server并转发端口。)
接着查看新so库的情况,设置so库的权限:
运行IDA32 pro并连接调试客户端,首先选择debugger:
然后填写本地IP地址127.0.0.1:
Attach to process,找到lockk:
点击OK,可以成功attach到进程: