安卓逆向入门-基础知识
安卓逆向入门-基础知识
adb
常用命令:
1 | adb devices |
其他常用:
1 | adb shell pm list packages |
APK文件
APK根目录下目录文件有:
名称 | 简介 |
---|---|
META-INF | 元数据,如签名、证书等 |
AndroidManifest.xml | 全局配置文件 |
assets | 资源文件夹,不编译不占APK内存 |
classes.dex | 编译并打包后源码 |
lib | 二进制共享库文件夹 |
res | 资源文件夹,被映射到R.java中,访问资源R.id.filename |
resources.arsc | 编译res中的文件 |
kotlin | Kotlin相关 |
original | 反编译产生,保存一些文件备份 |
Unknown | 反编译产生,暂时无法被处理的文件 |
Apktool.yml | Apktool描述文件,记录反编译信息 |
res中可能的目录有:
名称 | 简介 |
---|---|
anim | 编译后动画xml文件 |
color | 编译后选择器xml文件 |
layout | 编译后布局xml文件 |
menu | 编译后菜单xml文件 |
raw | 不编译的资源文件,音视频等 |
xml | 编译后自定义xml文件 |
drawable-* | 按分辨率存放的图片 |
APK打包时首先处理Java文件、Java代码和Android代码,整体项目编译为class文件,再将class文件转换为Dex文件,再把资源文件、Dex文件和其他文件通过APKbuilder进行合并,打包为一个APK,完成签名后发布。
DEX即Dalvik Executable,Dalvik是Android系统的可执行文件,包含应用程序全部操作指令及运行时数据,结构为:
1 | struct DexFile{ |
实战
签名
有些工具自己去Android Studio的Android SDK目录找,没有的自己下载。
对APK解包:
1 | apktool d ./crackme02.apk |
对反汇编结果进行修改,再重新打包:
1 | apktool b ./crackme02 |
接下来是签名:
1 | apksigner verify ./crackme02.apk #先检查是否有签名 |
对于apksigner其他参数:
1 | --v1-signing-enabled 使用jar包签名方式 |
v1版本签名方案便利apk中左右条目,提取处包中文件消息摘要并写入MANIFEST.MF中,对后者二次摘要生成CERT.SF并用私钥对后者签名,仨文件一块打包保存到META-INF文件夹中。v2签名方案验证归档中所有字节,并在原apk块中增加一个新签名块用于存储签名、摘要、签名算法、证书链等属性信息。v2支持将apk分割成小块,分别计算小块摘要,再计算最终摘要。v1和v2可共存,有v2签名块则必须通过v2校验流程,否则降级v1签名降级流程。
对于Apktool的解包时,-r或--no-res在反编译时不处理resource.arsc和AndroidManifest.xml,适用于只需要Dex或资源文件解码错误,二次打包时资源文件将原封不动打回去。-s或--no-src指定反编译时Dex不被反编译为Smali,只解码resource.arsc和AndroidManifest.xml。
Dalvik字节码生成
例如:
1 | //Hello.java |
编译为Java字节码:
1 | javac Hello.java #编译为Hello.class文件 |
结果有:
1 | Compiled from "Hello.java" |
查看Dalvik字节码:
1 | dexdump -d ./classes.dex |
结果有删减:
1 | Processing './classes.dex'... |
还有用BakSmali进行反汇编:
1 | baksmali d ./classes.dex #输出在out文件夹中 |
结果有:
1 | .class public LHello; |
除了BakSmali还有Dedexer:
1 | ddx -d . ./classes.dex #输出Hello.ddx 用记事本打开就行 |
结果有:
1 | .class public Hello |
再编写HelloWorld.smali:
1 | .class public LHelloWorld; |
将HelloWorld.smali编译为out.dex:
1 | smali assemble ./HelloWorld.smali |
NDK开发入门
不需要Android Studio啥的,直接新建MainActivity.java:
1 | package test.example; |
编译:
1 | javac --class-path=D:\AndroidSDK\platforms\android-34\android.jar -d . ./MainActivity.java #生成class文件 生成test文件夹 |
此时在./test/example/jni目录下生成头文件test_example_MainActivity.h,并在该目录下新建jni.c文件:
1 |
|
同目录下再新建Android.mk文件,声明如何编译动态链接库:
1 | LOCAL_PATH:=$(call my-dir) |
在同目录下编译:
1 | cd test/example/jni |
此时在./test/example/libs目录会生成各种架构的动态链接库。
当某个.so文件在运行时发生异常,adb logcat
将会记录下来。通过查看日志找到.so发生错误的PC寄存器值和.so文件offset后,可用addr2line来定位对应c语言行数:
1 | llvm-addr2line -C -f -e *.so XXXXXXXXXXXXXXXX #后面参数为pc寄存器+动态链接库offset |
可用readelf查看ELF格式文件信息,能看好多信息,这里举例文件头信息:
1 | > llvm-readelf -h ./libjni_test.so |
Dalvik汇编/Smali字节码
入门
Smali/bakSmali是Android的Java VM实现dalvik使用的Dex格式的汇编/反汇编程序。对应Java基础类型有:
Java | Smali |
---|---|
void | V |
boolean | Z |
byte | B |
short | S |
char | C |
int | I |
long | J |
float | F |
double | D |
语法关键词有:
关键词 | 含义 |
---|---|
.class |
定义Java类名 |
.super |
定义父类名 |
.source |
定义Java源文件名 |
.field |
定义字段 |
.method |
定义方法 |
.end method |
定义方法结束 |
.annotation |
定义注释 |
.end annotation |
定义注释结束 |
.implements |
定义接口指令 |
.local |
方法内局部变量个数 |
.registers |
方法内使用寄存器的总数 |
.prologue |
方法中代码开始处 |
.line |
Java源文件中指定行 |
.parameter |
方法参数 |
Smali字段表示方法如Ljava/lang/String
。表述参数类型时若有多个,如int,int,String可表示为IILjava/lang/String
。对于数组String[]、int[][]可表示为[Ljava/lang/String
、[[I
。多维数组维数最大为255个。
常见参数语法约定有:
参数 | 描述 |
---|---|
vX | 寄存器 |
#+X | 常量数字 |
+X | 相对指令的地址偏移 |
kind@X | 常量池索引值 |
上述“kind”表示常量池类型,可以是字符串常量池索引string、类型常量池索引type、字段常量池索引field、方法常量池索引meth。
例如方法格式如下:
1 | // methd(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; |
BakSmali版在.method
指令前可能有# virtual method
表示虚方法,# direct methods
表示直接方法。BakSmali字段以.field
开头,# instance field
表示实例字段,# static field
表示静态字段。
实例如下:
1 | # BakSmali版 |
常用指令
Dalvik虚拟机中每个寄存器都32位的,64位常规类型添加-wide
后缀。特殊类型字节码根据具体类型添加后缀,可以是-boolean
、-byte
、-char
、-short
、-int
、-long
、-float
、-double
、-object
、-string
、-class
、-void
。宽度值中每个大写字母表示4位。如:
1 | move-wide/from16 vAA,vBBBB |
move位基础字节码。wide位名称后缀。from16位字节码后缀,标识源位一个16位寄存器引用变量。vAA和vBBBB分别为目的寄存器和源寄存器。
1 | nop #空操作指令 值00 |
文件格式
smali文件头格式为:
1 | .class <访问权限> [修饰关键字] <类名> |
访问权限可以是public、private、protected之一,修饰关键字可以是synthetic等。例如MainActivity.smali前3行为:
1 | .class public Lcom/droider/crackme0502/MainActivity; |
两种实例声明格式为:
1 | #static fields |
例如:
1 | # instance fields |
方法声明格式为:
1 | #direct methods |
虚方法声明则开头改为“virtual methods”。.locals
指定局部变量个数,每一个.parameter
表明使用一个参数,有几个参数则有几条.parameter
。.prologue
指定代码开始处。
接口格式为:
1 | #interfaces |
注解格式如下。
1 | #annotations |
注解的作用范围可以是类、方法或字段。若作用范围是类,该指令直接定义在smali文件中。若是方法或字段,该指令包含在方法或字段定义中,如下:
1 | # instance fields |
转为Java代码为:
1 | MyAnnoField(info="Hello my friend") .droider.anno |
类
例如下面的类,反编译后生成Outer.smali和Outer$Inner.smali,内部类文件名格式为“[外部类]$[内部类].smali”。
1 | class Outer{ |
有时遇到实例字段定义如下,synthetic属性表明是被编译器合成的、虚构的,而不是代码作者声明的。
1 | # instance fields |
当内部类层数过多时,有自动保留的指向所在外部类的引用:
1 | public class Outer { //this$0 |
对于一个非静态方法而言,若有两条.parameter
语句,则实际上使用了p0~p2共3个参数寄存器。此时隐含使用p0寄存器当作类的this引用。例如:
1 | # direct methods |
监听器一般用匿名内部类形式实现,例如:
1 | # virtual methods |
有MemberClass注解如下,该注解为一个系统注解,为父类提供一个MemberClass子类成员列表,即内部类列表。
1 | # annotations |
有EnclosingMethod注解如下,说明整个MainActivity$1匿名类的作用范围,注解value表明它位于MainActivity的onCreate
中。
1 | # annotations |
例如还有EnclosingClass注解,value标识MainActivity$SNChecker作用于MainActivity类。
1 | # annotations |
EnclosingClass后跟着InnerClass注解,其中accessFlags访问标志位一个枚举值,name位内部类名。
1 | enum { |
若注解类在生命是提供了默认值,则程序中会用到AnnotationDefault注解:
1 | # annotations |
Signature注解用于验证方法签名:
1 | .method public onItemClick(Landroid/widget/AdapterView;Landroid/view/View;IJ)V |
若方法声明中使用throws
关键字抛出异常,则生成相应Throws注解:
1 | .method public final get()Ljava/lang/Object; |
其对应Java代码为:
1 | public final Object get() throws InterruptedException, ExecutionException { |
此外Android SDK还自动生成一些类,有R、BuildConfig等类。R类如下:
1 | package com.droider.crackme0502; |
BuildConfig类有:
1 | package com.droider.crackme0502; |
反编译
例如迭代器for方法:
1 | .method private iterator()V |
反编译结果为:
1 | private void iterator() { |
例如传统for方法:
1 | .method private forCirculate()V |
反编译结果为:
1 | private void forCirculate() { |
switch语句有两种不同的Smali表现形式,分别为packedSwitch和sparseSwitch。例如packedSwitch形式:
1 | .method private packedSwitch(I)Ljava/lang/String; |
packed-switch指令前面讲过,结构格式如下,用二进制编辑器可分析.dex文件,这里不展开。
1 | struct packed-switch-payload { |
反汇编结果为:
1 | private String packedSwitch(int i) { |
sparseSwitch形式:
1 | .method private sparseSwitch(I)Ljava/lang/String; |
sparse-switch指令上面也讲过,格式为:
1 | struct sparse-switch-payload{ |
对于try/catch语句有:
1 | .method private tryCatch(ILjava/lang/String;)V |
反汇编结果为:
1 | private void tryCatch(int drumsticks, String peple) { |
ARM入门
概述
ARM有31个通用寄存器,6个状态寄存器。其中通用寄存器分为16种,分别为R0到R15,状态寄存器分为CPSR和SPSR两种。
通用寄存器:
寄存器 | 描述 |
---|---|
R0~R3 | 传入函数参数,传出函数返回值 |
R4~R11 | 函数局部变量 |
R12 | 内部调用暂时寄存器,被调用函数返回前不必恢复 |
R13 | 栈指针寄存器SP,存放堆栈栈顶地址 |
R14 | 链接寄存器LR,存放子程序返回地址 |
R15 | 程序计数器PC,保存当前正在执行的指令在内存中的地址 |
状态寄存器:
寄存器 | 描述 |
---|---|
CPSR一个 | 状态寄存器。保存程序当前状态 |
SPSR五个 | 备份程序状态寄存器,异常返回后回复异常发生时的工作状态 |
汇编基础
1 | .text:000082E0 ; int __fastcall main(int argc, const char **argv, const char **envp) |
ARM处理器有ARM和Thumb两种状态,处理器可在这两种之间随意切换。处于ARM状态时32位字对齐,Thumb状态16位对齐。Thumb状态下FP、IP、SP、LP和PC对应ARM状态下R11、R12、R13、R14和R15。IDA动调时没法区分这两种,得手动切换。
指令条件码cond:
条件码 | 标志 | 含义 |
---|---|---|
EQ | Z\=1 | 相等 |
NE | Z\=0 | 不相等 |
CS/HS | C\=1 | 无符号大于等于 |
CC/LO | C\=0 | 无符号小于 |
MI | N\=1 | 负数 |
PL | N\=0 | 正数或0 |
VS | V\=1 | 溢出 |
VC | V\=0 | 无溢出 |
HI | C\=1,Z\=0 | 无符号大于 |
LS | C\=0,Z\=1 | 无符号小于等于 |
GE | N\=V | 符号大于等于 |
LT | N!\=V | 符号小于 |
GT | Z\=0,N\=V | 符号大于 |
LE | Z\=1,N!\=V | 符号小于等于 |
AL | 任何 | 无条件 |
后缀S表示影响CPSR的值,操作数据大小type有:
type | 含义 |
---|---|
B | 无符号字节 |
SB | 有符号字节 |
H | 无符号半字 |
SH | 有符号半字 |
递增含义addr_mode:
addr_mode | 含义 |
---|---|
IA | 执行后增加 |
IB | 执行前增加 |
DA | 执行后减少 |
DB | 执行前减少 |
FD | 满递减堆栈 |
FA | 满递增堆栈 |
ED | 空递减堆栈 |
EA | 空递增堆栈 |
常用汇编:
1 | B{cond} label @cond满足则跳到label继续执行 |
Android原生逆向
常见函数结构:
1 | ;函数头 |
例如展示了两种for循环的调用:
1 | .text:000085FC ; int __fastcall main(int argc, const char **argv, const char **envp) |
源码为:
1 |
|
这里看第二种for:
1 | .text:000085BC ; int __fastcall sub_85BC(int) |
再如if语句:
1 |
|
if1为:
1 | .text:000085C8 sub_85C8 ; CODE XREF: main+8↓p |
if2为:
1 | .text:00008570 sub_8570 ; CODE XREF: main+10↓p |
再例如while循环:
1 |
|
dowhile有:
1 | .text:00008570 ; int __fastcall sub_8570(int) |
switch语句如:
1 |
|
结果为:
1 | .text:000085A0 ; int __fastcall sub_85A0(int, int, int) |
再例如C++的类:
1 |
|
结果为:
1 | .text:00008600 ; int __fastcall main(int argc, const char **argv, const char **envp) |
静态链接STL:
1 |
|
结果如下,C++符号是自己手补的,一般IDA只能恢复动态链接符号,静态链接不行。
1 | .text:0000998C ; int __fastcall main(int argc, const char **argv, const char **envp) |
Android NDK的jni.h中声明了所有可用的JNI接口函数,其中有JNINativeInterface和JNIInvokeInterface两个结构体。JNINativeInterface为JNI本地接口,实质是个接口函数指针表,每一项都为JNI接口函数指针,所有原生代码可调用这些接口函数。JNIInvokeInterface为JNI调用接口,用于访问全局JNI接口,用于原生多线程程序开发。可以在IDA导入Android SDK目录下的\\ndk\\28.0.12433566\\toolchains\\llvm\\prebuilt\\windows-x86_64\\sysroot\\usr\\include\\jni.h文件,但需要先将开头包含的其他库语句删掉。例如:
1 |
|
动态调试
可修改Smali,用android.util.Log类输出调试信息,如:
1 | Log.v("com.droider.jnimethods","jni test a void subclass method, this run in java"); |
除了v还有d、i、w、e方法,分别为VERBOSE、DEBUG、INFO、WARN、ERROR类型信息。第一个字符串为Tag标记,第二个为调试信息。命令行查看输出为:
1 | adb logcat -s com.droider.jnimethods:V |
此时添加的反汇编代码为:
1 | const-string v3,"SN" |
也可以用异常来进行栈跟踪:
1 | new Exception("print trace").printStackTrace(); |
对应反汇编代码为:
1 | new-instance v0,Ljava/lang/Exception; |
栈跟踪信息的Tag为System.err,WARN级别,运行时需要:
1 | adb logcat -s System.err:V *:W |
当调试Android原生程序时,需要:
1 | adb push .\debugnativeapp /data/local/tmp |
然后设置IDA远程参数,Application为/data/local/tmp/debugnativeapp,Directory为/data/local/tmp,Hostname为localhost。
当要调试动态链接库时,先开启端口转发与服务端,IDA再用Attach方法附加一个进程。可以在Debug options中设置各种初始化断点。但Android 5.0以上强制开启PIE,得找对应链接库所在的段,再计算函数相对偏移量去找。
IDA还可以Dump Android应用内存,即.so文件。IDA动调附加并设置断点后,在Modules窗口或下面命令查看.so文件起始地址和大小:
1 | cat /proc/pid/maps |
在IDA的Execute script中输入:
1 | auto file,fname,i,address,size,x; |
还可以用JDB搭配Android Studio进行动调。下载https://bitbucket.org/JesusFreke/smali/downloads/下的smalidea\-0\.05\.zip插件并安装到Android Stdio中,把用Apktool反编译的Smali源码导入Android Studio并Mark Directory as->Resources Root,标记为资源根目录。然后Run/Debug Configurations下新建一个Remote调试配置,填写localhost和5005,然后在Android Device Monitor中查看pid并设置端口转发:
1 | adb shell ps | grep com.example |
Android入门
简介
Android系统启动时,第一个启动的是init进程,后者根据读取/init.rc中配置创建并启动App_process进程,也就是Zygote孵化器进程。Zygote启动后创建SystemServer进程,开始创建应用程序进程。Zygote是Android中所有应用的父进程,它开启Socket接口来监听请求,执行一个Android应用程序时,SystemServer通过Binder IPC发送命令给Zygote,后者通过自身的Dalvik虚拟机实例来启动应用程序。
SystermServer是Android核心进程之一,提供大部分Android基础服务,如AMS、WMS、PMS等。AMS为调度器,负责系统组件和应用程序的启动、切换、调度等工作。
Android应用启动时,手机屏幕是一个Activity,也叫做Launcher。Android应用一般通过触发来产生启动条件,Launcher进程接收事件并通知AMS,后者收到消息后,差u女鬼剑Taks启动指定Activity,此时AMS与Zygote通信,去Fork一个实例来执行应用。
Zygote提供三种创建进程的方法:fork()
创建一个Zygote进程,forkAndSpecialize()
创建一个非Zygote进程,forkSystemServer()
创建一个系统服务进程。Zygote进程可再fork()
处其他进程,而非Zygote进程不能fork其他进程,系统服务进程再终止后它的子进程也必需终止。
进程fork成功后,执行的工作交给Dalvik虚拟机,后者通过loadClassFromDex()
完成类的装载,每个类成功解析后会拥有一个ClassObject类型的数据结构存储再运行时环境中。虚拟机用gDvm.loadedClasses
全局哈希表来 存储与查询所有装载进来的类。随后字节码验证器用dvmVerifyCodeFlow()
对装入的代码进行校验,接着虚拟机用FindClass()
查找并装载main方法类,随后用dvmInterpret()
初始化解释器并执行字节码流。
对于Android源码的下载,这里下载Android 4.1,Linux内核3.4。下载需要安装repo,这里略,安装后更新.bashrc为:
1 | export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo' |
然后初始化并同步:
1 | repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-4.1.2_r1 |
完全拉下来得好几十个GB,这里也建议直接在线阅读https://cs.android.com/android/platform/superproject/+/android-4.1.2_r2.1:。
关键代码定位
一个Android程序由一个或多个Activity以及其他组件组成,每个Activity都是相同级别的。每个Android程序有且只有一个主Activity,隐藏程序除外。
例如找到AndroidManifest.xml:
1 |
|
找到该主Activity后去找它的OnCreate
方法即可。
有时可能出现Application类,用于程序组件间传递全局变量、在Activity启动前做初始化工作等。使用Application类时需要在程序中添加一个类继承自android.app.Application,并重写它的OnCreate
。Application类启动比其他类要早,某些商业软件将授权验证代码转移到该类中。
破解基础
方法createPackageContext
创建其他程序的Context,通过该Context可访问其他软件包资源,甚至执行其他软件包代码。该方法可能抛出java.lang.SecurityException安全异常,因为一个软件不能创建其他程序Context,除非两个软件拥有相同的用户ID与签名。用户ID在AndroidManifest.xml的manifest标签中指定,格式为android\:sharedUserId=\”xxx.xxx.xxx\”。当两个程序指定相同用户ID时,这两个程序运行在同一个进程空间,他们之间资源可相互访问,签名相同还可相互执行软件包之间代码。
抓包可用Shell自带的tcpdump,记录:
1 | tcpdump -p -vv -s 0 -w /sdcard/Download/capture.pcap |
用Ctrl+C结束,用adb拉下来,再用Wireshark分析。
有时Dalvik层定义的原生函数无法在Native层找到相同的函数名,可能是在JNI_OnLoad
方法中注册其他函数与Java层方法关联了,找到__data_start
即可。
APK可能在AndroidManifest.xml中Application标签加入android:debuggable="false"
,但实测没啥用。判断该值是否被修改过:
1 | if((getApplicationInfo().flags&=ApplicationInfo.FLAG_DEBUGGABLE)!=0){ |
还有种方法检测调试器是否已经连接:
1 | android.os.Debug.isDebuggerConnected() |
可通过以下命令查看Android属性值,其中一些差异如下,不过现在模拟器都隐藏地很好了:
属性值 | 模拟器 | 正常手机 |
---|---|---|
ro.product.model | sdk | 手机型号 |
ro.build.tags | test-keys | release-keys |
ro.kernel.qemu | 1 | 没有该值 |
例如检测ro.kernel.qemu属性:
1 | package com.droider.checkqemu; |
用PackageManager进行签名校验:
1 | package com.droider.checksignature; |
CRC校验classes.dex文件:
1 | package com.droider.checkcrc; |