Android_Reverse_Engineering

Android_Reverse_Engineering

Learning Link

Ubuntu安装并管理多个版本的Python

Android逆向

APKLAB

Android Pentest是关于安卓渗透测试的,但是有一部分还是可以借鉴的

Android Hacking101

Android Pratice

Frida Guide

Android App reverse101

Android Application Framework

安卓是基于Linux内核和其他开源项目的修改版本的移动操作系统

安卓操作系统

硬件

Android的主要硬件平台是ARM,在以后的版本中也支持X86架构和X86-64架构

内核

截至2020年,Android使用Linux内核的4.4,4.9或4.14版本。Android Kernel 基于 Linux Kernel 的长期支持 (LTS)分支

文件系统

之前的Android使用YAFFS2文件系统,在Android2.3之后使用EXT4文件系统,虽然很多的OEM(原始设备制造厂商)已经尝试了F2FS,但以下的目录在任何Android都存在

  • Boot:包含内核,虚拟硬盘等

  • System:包含操作系统文件,其中包括Android UI和预安装的应用程序

  • Recovery:引导到操作系统的替代选项,允许恢复和备份分区

  • Data:保存用户数据,其子文件夹包括

    • Android:默认用于应用程序缓存和保存的数据
    • Alarms:警报的自定义音频文件
    • Cardboard:包含VR文件的数据
    • DCIM:相机拍摄的照片的视频
    • Downloads:存放在互联网上下载的文件
    • Notifications:某些应用通知的自定义提示音
    • Musics,Movies:存储第三方的音乐和视频
    • Pictures:存储第三方的图片
    • Podcasts:使用播客应用时存储播客文件
    • Videos:存储从第三方下载的视频
  • Cache:存储常用数据和应用组件

  • Misc:包含其他重要的系统设置信息

安卓架构

img

Kernel

它为用户提供了与硬件通信的接口。它包含程序用来指示硬件组件执行特定功能的基本驱动程序。这些驱动程序是音频,显示器,蓝牙等

Hardware Abstraction Layer

硬件抽象层HAL) 是代码的逻辑划分,用作计算机物理硬件与其软件之间的抽象层。它提供了一个设备驱动程序接口,允许程序与硬件进行通信

Libraries

位于内核的顶部,库为开发人员提供开发应用程序,资源文件甚至清单的支持。有一些原生库,如SSL,SQLite,Libc等,是原生代码有效执行任务所必需的。

Android Runtime

ART是Android操作系统使用的应用程序运行时环境,Runtime Environment是程序可以向计算机处理器发送指令并访问计算机主存(RAM)的状态。JAVA编写的Android应用程序,在编译期间首先转换为字节码,打包为APK和运行运行时

Android使用虚拟机来执行应用程序,以便将程序的执行和操作系统隔离开来,并免受恶意代码的侵害

在Android4.4之前,程序的运行是由DVM(Dalvik Virtual Machine)执行的,后来被Android Runtime替代

Application Framework

安卓由四大组件:Activity、Service、Broadcast Receiver、Content Provider

Android操作系统的整个功能集可以通过Java编写的API提供给开发人员,这些API是Android应用所需要的最重要的组件

  • View System:主要用于构建应用程序的UI。包括列表、文本框和按键
  • Resource Manager:提供对布局文件、图形等非代码资源的访问
  • Notification Manager:允许应用在状态栏显示自定义警报
  • Activity Manager:管理应用的生命周期
  • Content Providers:使应用程序能够访问其他应用程序

System Applications

预装的核心应用程序集,用于基本功能,如短信,日历,互联网浏览,联系人等

编译与反编译

ART的主要编译过程如下

img

APK文件只是一个包含XML文件,dex代码,资源文件和其他文件的ZIP压缩包,需要反编译的时候需要先解包然后再反编译,使用APKLab即可

APK构建流程和执行过程

  1. .java文件中的java源代码通过javac转换为字节码(.class文件)
  2. 所有的.class文件都通过dx编辑器转换为.dex文件(Dalvik可执行文件)。DEX字节码独立于设备架构,需要转换为本机机器代码才能在设备上运行
  3. AAPT将资源(res文件夹)编译为二进制文件(resources.arsc),并且将已经编译的资源、非编译的资源、.dex文件打包到apk文件中
  4. 对应用程序进行签名,然后才能发布

Dalvik JIT和ART的区别

如果Android使用的是Dalvik JIT编译器,那么每次运行程序时,他都会动态地将Dalvik字节码(也就是.dex文件)地一部分转换为字节码然后执行,随着程序的执行,将编译和缓存更多的字节码

如果是Android使用的是ART,那么在应用程序的安装阶段,他就会静态地将DEX字节码转换为机器代码,并且存储在设备的内存中,这是一次性事件

Smali代码

Smali是Dalvik VM内部执行的核心代码,是Dalvik自己的语法规范

smali语言学习

Smali代码就是dex文件反编译之后的代码,所以说Smali语言是Android虚拟机的反汇编语言

我们可以通过修改Smali代码来修改APK运行逻辑,再重新编译打包成新的APK

APK的大致内容

1662380130833

  • AndroidMainfest.xml:二进制XML格式的清单文件,存储应用程序的软件包名称,版本组件和其他元数据

    包含的内容

  • META_INF:清单,用于存储有关应用程序的元数据,它还包含APK的证书和前面

  • classes.dex:以dex格式编译的应用程序代码,Dalvik VM(相当关于Java中的JVM)可以识别和执行

  • res/:包含未编译成resources.arsc中的资源的文件夹

  • lib/:包含本地已编译代码文件-即本机代码库

  • assets/:应用程序的资产

  • resources.arsc:提前编译好的资源文件

APK程序的活动和入口

活动

活动是用来承载用户界面的容器,是Android的四大组件之一,我们再APP里面看到的页面就需要一个Activity,而页面之间的跳转就是Activity之间的跳转。比如,登陆页面是一个LoginActivity,注册页面是一个 RegisterActivity,当我们需要从登陆页面跳转到注册页面时,也就是 LoginActivity 通过 Intent 跳转到 RegisterActivity

入口

我们新建一个Android项目时会默认生成一个Activity,叫做 MainActivity,MainActivity就是这个项目的唯一页面,也就是APP的启动页面。每一个Activity都需要在AndroidManifest.xml文件中配置。每创建一个Activity都需要在这个文件中国注册

在AndroidManifest.xml文件中android.intent.action.MAIN会将Mainactivity注册为最先启动多个Activity,同时我们也可以在其中配置Activity的其他属性

Oncreate函数

Oncreate函数通常配置需要的信息一个Activity启动回调的第一个函数就是Oncreate,Oncreate函数做一些Activity启动的一些必要的初始化的工作。有点像Java中的构造函数

Smali

学习链接

字节码格式

smali详解

AS调试时Smali下不了断点

下不了断点可能是版本不对,下载最新版即可

AS中调试smali

直接使用APKLab解包(如果没有debuggable属性则需要先在application标签中加入android:debuggable=“true”,然后重新打包),然后以调试方式启动最后附加上去即可(这里我直接开启调试是失败的)

adb shell am satrt -D -n 包名/.主活动

smali学习

Java编译器将.java源文件编译为.class字节码文件,然后JVM将字节码解释为机器代码在目标机器上执行。DVM指的是DalVIk VM,在Android中,java类被打包为DEX字节码文件(.dex),DEX字节码经过Dalvik或者ART转为机器码进行执行,而smali就是dex文件反编译之后的汇编代码

JVM是基于栈帧的,也就是Stack-based,而DVM基于寄存器,也就是Register-based

smali代码

smali和java的对比

注释

1
2
在java中使用//
而smali中使用#

类声明

1
2
3
4
5
6
7
8
//在java中
public class class_name{}

//在smali中
.class 权限修饰符 类的全包名路径,使用L开头,以;结尾

.super Ljava/lang/Object;#声明父类,默认为Object
.implement L/java/lang/CharSequence;#如果实现了接口需要添加接口代码

方法声明

1
2
3
4
5
6
7
//在java中
public static void Method(){};

//smali中
.method 属性 方法名(参数的签名)返回值签名

.end method;#成对出现

这里表示Onclick的属性是public、参数为View(因为View是对象,需要使用全包名路径),返回值为void(V)

全包名路径时java中的.被修改为/,并且使用L开头,以;结尾

字段声明

1
2
3
4
5
6
7
8
9
//java中
public String a;

//smali中,
.filed public a:com/lang/String;#声明了一个字段
.filed 权限修饰符+静态修饰符+变量名:变量全类名路径

//常量声明
.filed public a:com/lang/String;="hello"

字段取值赋值

1
2
//smali中
iget iput sget sput#i表示instance,s表示static

方法调用

1
2
//smali中,以invoke开头
在方法中必须声明方法中寄存器的数量

方法取值

1
//获取返回值首先需要调用方法invoke,然后接收返回值move

smali和java基础数据类型对比

smali java
B byte
S short
I int
J long
F float
D double
C char
Z boolean
V void
[ 数组
L+全类名路径,用/分割 object

smali源码结构分析

先自己编写一个简单的APK

源代码如下

声明

类方法的声明

.super表示继承的类,.source是java源文件

实现的类

构造方法

java中自动生成无参构造器

.method和.end method一起使用,construct 是构造器特有的关键字,()V表示构造方法为无参构造且返回值为void

.line 14表示在源码中的行数为14行,可以删除

invoke-direct是方法的调用,凡是私有方法或者构造方法统统使用invoke-direct,这里的invoke-direct其实就是调用父类的初始化方法

invoke-direct表示将p0参数传入后面的方法中,p0这里就是this指针,其实存在于构造器的参数列表中,将this传入后面的方法进行Object的初始化操作

invoke-direct {参数},方法所对应的全包名路径类; -> 方法名称(方法参数签名)方法返回值签名

return-void表示返回值为void

当返回值为String时,返回Object

const-string v0,“hello” 声明一个常量字符串

在方法声明之后的.locals可以理解为调用该方法需要使用到的变量

main方法

main函数的参数是[Ljava/lang/String;表明其参数为String数组

.param表明参数对应的名称为args

.line两个之间的smali代码表示java源代码中的一行代码

对应java中

smali代码中的for循环

首先使用const初始化两个常量,然后进行比较if-ge代表如果p1>=v0,则跳转到con_0分支,否则add-int/lit8 p1,p1,0x1表示将p1+0x1的值然后赋值给p1,即p1=p1+1,然后使用goto语句回到判断处

Toast在smali中的代码

调用实例方法/一般方法一般使用invoke-virtual,invoke-static调用静态方法

先获取参数,然后存储到{}中,接着使用->(这里->相当于Toast.makeText(p1,v1,v0))将参数传入到makeText方法中,makeText(参数签名)返回值签名

接收方法的返回值传递到p1然后show

关于方法返回的关键字

smali 数据类型
return byte
return short
return int
return-wide long
return float
return-wide double
return char
return boolean
return-void void
return-object 数组
return-object String

静态代码块的smali代码

初始化

smali各种方法的调用

关键字

1
2
3
4
5
invoke-virtual#非私有(private)实例方法的调用
invoke-direct#用于构造方法以及私有方法
invoke-static#调用静态方法,static方法不需要传入this实例
invoke-super#调用父类方法
invoke-interface#调用接口方法,interface为接口

编写一个类

1
2
3
4
5
6
7
8
public class Test{
public Test(String a){
getname();
}
public String getname(){
return "hello"
}
}

smali中对象的创建

1
2
3
4
5
#声明实例
new-instance+变量名,对象包名路径

#调用构造方法(即构造器)(如果该构造方法还定义了成员变量,那么在函数调用前需要提前声明,然后在invoke时当作参数一并传入)
invoke-direct{变量名},对象全包名路径;-><init>(参数)返回类型
1
2
3
4
5
6
class Test{}

new Test();
//相当于
new-instance v0,LTest;
invoke-direct{v0},LTest;-><init>()v#构造器默认无参返回void

数据的定义

主要有字符串数据、字节码数据、数据类型数据

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
//字符串.smali中不能直接返回,需要先存储在容器中
const-string v0,"hello"
return-object v0

//字节码对象
const-class V0,GoActivity;
startActivity在smali中分为两个步骤
首先声明要启动的class,以及当前Activitythis对象
const-class v0,SecondActivity
然后创建Intent对象,v1用于接收init的返回对象
new-instance v1,Landroid/content/Intent;
invoke-direct{v1,p0,v0},Landroid/content/Intentl-><init>(Landroid/content/Intent;Ljava/lang/class;)V
最后调用StartActivity方法
invoke-virtual{p0,v1},LTestActivity->StartActivity(Landroid/content/Intent;)V

//数值型数据的定义
const//占用一个容器
const-wide//占用两个容器,long
const v0,30
const-wide v0,30#占用v0和v1容器,会默认占用目标寄存器和目标寄存器的下一个容器,64
const/4#最大只允许四个二进制位,也就是1111(有符号)(char)
const/16#word
const#占用一个寄存器,32位,int
const/high16#最大只允许存放高十六位二进制位数值

const-wide/16
const-wide/32
const-wide
const-wide/highssss32

字段的取值与赋值

1
2
3
.filed
static对应sget\sput
instance对应iput\sput

同样的不同的数据类型也对应不同的类型

1
2
3
4
5
6
7
.class public LTest;#声明类
.field private static a:Ljava/lang/String;

.method
const-string v0,"hello"
sput-object v0,LTest;->a:Ljava/lang/String;#进行赋值
.end method

条件跳转if

寄存器

内部寄存器声明,在Dalvik中,每个寄存器都是32位的,2个寄存器用于存储long和double

1
2
3
4
5
.registers 数量#声明于方法内部
.locals 数量#都是表明寄存器数量
上面两者的区别在于
.locals指明了这个非参寄存器的数量,而寄存器的总数包括保存方法参数的寄存器(存储局部变量寄存器的数量),如v0、v1等,没有p0
.registers指定了在这个方法中有多少个可用的寄存器(局部+参数),如p0、v0、v1

寄存器的两种命名方法-p命名法和v命名法主要是使用p命名法

1
2
3
//如果是v命名法,优先对局部变量进行声明
比如v0和v1已经被使用,那么方法中第一个参数存入v2寄存器,依次往后
//p命名法,方法的参数使用p寄存器表示

smali语法关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.line N#表示与java源文件代码的映射关系,可删除

:cond_0#条件分支,配合if使用

.prologue#表示程序的开始,可删除

.goto_0#goto跳转分支

.local#显示局部变量(寄存器中的值)别名信息,类比.line

.locals N#寄存器数量声明

.Param p1,"a":Ljava/lang/String;#也是起别名


smali代码注入

IDA Dump Android应用内存

  1. 首先先将应用程序附加到ida上
  2. 然后配置调试器在不同事件触发的地方设置断点
  3. 加载完目标so文件之后
  4. 首先获取应用程序的PID,adb shell ps,然后在adb shell内cat /proc/PID/map获取so文件起始地址可以使用|grep "so"过滤
  5. 然后编写IDAPython dump即可

在IDA中的Modules中能看到Odex文件,接下来可以从内存中Dump下来Dex文件,这是对抗动态加载壳的常用思路

Root

Magisk Hide替代品

Pixel 3xl Root

Xposed和Magisk

Objection

Objection可以快速完成诸如内存搜索、类和 模块搜索、方法Hook以及打印参数、返回值、调用栈等常用功能

Objection依托Frida完成了对应用的注入以及对函 数的Hook模板,使用时只需要将具体的类填充进去即可完成相应的 Hook测试

安装好之后,通过命令objection -g 包名 explore注入进程后即可进入REPL界面

在REPL界面中,按空格键就会提示可用的命令,出现提示之后通过上下选择键及回车键便可输入命令

  1. help命令:在当前命令前加help之后再回车即可查看当前命令的解释信息

  2. jobs命令:用于查看和管理当前所执行的Hook的任务,可以同时运行多项Hook任务

  3. frida命令:查看Frida相关信息

  4. 内存漫游相关命令,Objection可以快速便捷地打印出内存中各种类地相关信息

    • android hooking list classes
    • android hooking search classes 关键字
    • **android hooking search methods **来从内存中获取所有包含关键字key的方法
    • 搜索到我们感兴趣的类后可以使用android hooking list class_methods来查看类的所有方法
    • android hooking list activities列出进程中所有的活动
    • android hooking list services列出进程所有的service,对于其余两个组件,只需要修改为receivers和providers即可
  5. Hook命令:**通过android hooking watch class_method **对指定类进行Hook

    还可以使用**–dump-args–dump-backtrace–dump-return**来打印函数的参数、调用栈以及返回值,默认会Hook对应方法的所有重载方法

  6. Hook结束之后可以使用jobs kill pid来删除作业,jobs list列出所有作业

  7. 主动调用:基于最简单的Java.choose的实现,**android heap search instances **来搜索实例,HashCode作为实例句柄来调用和执行函数

    然后使用android heap execute HashCode Method,注意这里只能是无参的实例方法

  8. 主动调用有参实例方法:输入android heap evaluate HashCode Method之后需要自己编写脚本,其中clazz是该类的实例

  9. 启动活动命令:android intent launch_activity 活动

当我们无法使用USB进行连接时,还可以使用Objection进行网络模式连接

MT管理器

MT管理器的使用

AS动态调试APK

AS动态调试APK

通过资源id找到所在位置

Java

反射

反射是框架的设计灵魂,反射就是将类的各个组成部分封装为其他对象

首先我们先来看java代码文件在计算机中的经历的阶段

Java源文件首先通过javac编译为class文件,class文件中存储成员变量、构造方法、普通方法然后将硬盘中的class类通过类加载器(ClassLoader)加载到内存中,而Java中有class类对象来存储字节码文件中的信息,字节码文件中主要的内容有成员变量、构造方法、成员方法,因为上述内容可能存在多个,所以使用数组进行存储。最后在运行时阶段构造成员。这样就将类中的各个部分封装为其他对象

反射的好处:可以在程序运行时阶段操作这些对象,同时可以解耦,提高程序的可扩展性,就像下面的输入提示,当我们输入"a.",程序会提示输入,这就是将String类的方法进行了封装Method[],然后遍历数组将所有的方法进行展示

反射的好处

我们可以通过在配置文件中写入要加载的类和需要调用的方法(使用集合存储),然后在程序中加载类和调用方法。在框架中需要经常地通过配置外部文件,在不修改源码的情况下来控制程序

类加载

分为静态加载和动态加载

  1. 静态加载:在编译时加载相关的类,如果没有则报错,即使我们在程序中可能不会用到这个类,但也必须编写类(在switch……case中创建对象,这样可能不会用到该类)。依赖性太强
  2. 动态加载:运行时加载需要的类,如果运行时不用该类则不报错

类加载时机

  • 当创建对象时(new)静态加载
  • 当子类被加载时 静态加载
  • 调用类中的静态成员时 静态加载
  • 通过反射 动态加载

获取Class对象的方式

  1. Class.forName(“全类名”):将字节码文件加载进内存,返回Class对象,多用于配置文件,将类名定义在配置文件中,读取文件、加载类
  2. 当类已经加载进内存中 类名.class:通过类名的属性class获取,多用于参数的传递
  3. 当创建好对象,对象.getclass():getclass方法在object中定义着,所有对象都有这个方法。,多用于对象的获取获取字节码方式——常用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Super01 {
public static void main(String[] args) throws Exception {
Class cls1=Class.forName("com.hspedu.super01.B");//必须是全类名路径
System.out.println(cls1);

Class cls2=B.class;
System.out.println(cls2);

B b=new B();
Class cls3=b.getClass();
System.out.println(cls3);

}
}
//因为每一个Class只会在内存中创造一个Class类对象,所以这三者是一样的

Class类对象的功能

  1. 获取成员变量们

    • Field[] getFields()//获取多个
    • Field getField(String name)//获取一个
    • Field[] getDeclaredFields(),获取所有的成员变量,不管修饰符,此时我们就可以操作私有的成员变量
    • Field getDeclareField(String name)
  2. 获取构造方法们

    同样也有getconstructor等方法

  3. 获取成员方法们

    同上

  4. 获取类名:String getName()

获取Field

1
2
3
4
5
6
7
8
9
10
11
import java.lang.reflect.Field;

public class Super01 {
public static void main(String[] args) throws Exception {
Class bClass=B.class;
Field fields[]=bClass.getFields();//这个方法是用来获取所有public的成员变量
for(Field field:fields){
System.out.println(field);
}
}
}

获取到了两个对象

getFields()用来获取所有public的成员变量

获取到字段之后使用get和set对成员变量的值进行操作,参数为对象,因为成员变量是在对象内的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Super01 {
public static void main(String[] args) throws Exception {
Class bClass=B.class;
Field fields[]=bClass.getFields();
for(Field field:fields){
System.out.println(field);
}
Field a=bClass.getField("name");//首先通过Class对象获取到字段
B b=new B();
Object fin=a.get(b);//创建对象后传入
System.out.println(fin);

a.set(b,"11111");//将b对象中的字段的值设置为11111
System.out.println(b.name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Super01 {
public static void main(String[] args) throws Exception {
Class bClass=B.class;
Field[] field=bClass.getDeclaredFields();
for(Field field1:field){
System.out.println(field1);
}
Field m=bClass.getDeclaredField("m");
B b=new B();
m.setAccessible(true);//因为是私有的,需要设置暴力反射
Object obj=m.get(b);
System.out.println(obj);
}
}

获取Constructor

1
2
3
4
5
6
7
8
9
10
11
12
public class Super01 {
public static void main(String[] args) throws Exception {
Class bClass=B.class;
Constructor con=bClass.getConstructor(String.class,int.class);
System.out.println(con);//获取构造器,可以指定参数获取不同的构造器,空参构造器可以直接使用bClass.newInstance()来创建对象

//constructor可以用于创建对象
Object text=con.newInstance("Jack",12);
//使用B的父类Object接收
System.out.println(text);
}
}

获取Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Super01 {
public static void main(String[] args) throws Exception {
Class bClass=B.class;
Method method=bClass.getMethod("eat");
B b=new B();//无参函数
method.invoke(b);//直接调用

Method method1=bClass.getMethod("eat",String.class);//参数类型
System.out.println(method1.getName());//获取方法名
method1.invoke(b,"apple");

Class aClass=Class.forName("com.hspedu.super01.A");
//Object父类方法也会被获取到,方法也支持暴力反射
}
}

整个过程的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Super01 {
public static void main(String[] args) throws Exception {
Class bClass=B.class;
Constructor con=bClass.getConstructor(String.class,int.class);//获取构造器
//通过构造器创建对象,因为在java中,调用方法是通过p.Method()来进行的,所以需要通过实例来进行
Object b=con.newInstance("jack",12);
Method method=bClass.getMethod("eat");

method.invoke(b);//直接调用
Method method1=bClass.getMethod("eat",String.class);//参数类型
System.out.println(method1.getName());//获取方法名
method1.invoke(b,"apple");

Class aClass=Class.forName("com.hspedu.super01.A");
//Object父类方法也会被获取到,方法也支持暴力反射
}
}

泛型

多态

解决代码复用性不高且不利于代码维护的问题

所谓的多态就是一个对象同时具备多种属性,比如小明既是学生也是人

下面以一个例子来看多态的好处,我们要实现主人喂动物这个操作,此时需要在master这个类中定义两种Feed方法

而是用多态只需要一个方法即可

父类类型 引用名=new 子类类型(),即为多态,也是多态中的向上转型 也就类似先在堆中new一个子类对象,接着使用父类的引用指向该对象地址,编译类型(编译时)看等号左边、运行类型(程序运行时)看等号右边,编译类型在定义对象时就确定了,运行类型是可以变化的。一个对象的编译类型和运行类型可以不一样,比如上面的dog定义为Animal类(编译类型),但在运行时指向Dog类(运行类型),成员变量是编译类型,方法是运行类型

此时可以访问父类的所有成员以及调用子类中重写父类的方法、但是不能访问子类的特有方法,如果要访问,只能再向下转型,要注意的是向下转型对象的类型必须一致,这个操作就相当于重写使用一个cat引用名指向new出来的Cat对象,但是不能使用cat引用名指向new出来的Dog对象

多态的前提是两个对象存在继承关系

接口

抽象

如果父类方法不确定如何进行方法体实现,那么这就是一个抽象方法。将图形作为父类,子类为长方形、圆形、三角形,我们可以通过不同的面积公式来求得各个图形的面积,但是我们没有办法直接求解图形的面积

此时就需要抽象方法来定义,父类定义抽象方法(不需要函数体,因为每个图形对应的方法都是不同的),然后子类继承父类后必须重写父类的抽象方法,但不用声明为abstract

抽象方法所在的类必须是抽象类,抽象类不能直接new对象,抽象方法的调用:先通过实现类完成对抽象方法的实现,再通过实现类的对象调用

测试代码

1
2
3
4
5
6
7
8
9
10
public class Super01 {
public static void main(String[] args) throws Exception {
//抽象类不能new对象
Rabbit rabbit1=new Rabbit();
rabbit1.print();

Wolf wolf=new Wolf();
wolf.print();
}
}

父类,print是抽象方法

1
2
3
public abstract class Animal {
public abstract void print();//定义一个抽象方法,不需要方法体,需要子类自己重写
}

两个子类,使用@Override对父类的抽象方法进行重写,也可以说是实现,将其具体化

1
2
3
4
5
6
public class Wolf extends Animal {
@Override
public void print() {
System.out.println("This is Wolf");
}
}
1
2
3
4
5
6
7
public class Rabbit extends Animal {
//必须对父类的抽象方法进行实现
@Override
public void print() {
System.out.println("This is Rabbit");
}
}

接口

接口就是一种公共的规范标准,相当于模板

比如USB接口,只要符合USB的标准就可以使用USB,打印机、U盘等,接口没有静态代码块和构造方法

抽象方法

1
//接口中默认声明为public abstract
1
2
3
//接口中定义抽象方法
public abstract void Method01();
void Method02();

导入接口并使用implement表示对该接口进行实现,然后重写接口中所有的抽象方法,如果没有重写所有,则需要将实现类定义为抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.test.InterFace;

public class Test implements InterFace {
@Override
public void Method01() {
System.out.println("This is Method01");
}

@Override
public void Method02() {
System.out.println("This is Method02");
}
}

最后创建实现类的对象后,通过实现类的对象调用即可

1
2
3
Test test=new Test();
test.Method01();
test.Method02();

默认方法

1
2
3
4
5
6
7
//由于接口中的抽象方法都需要实现,当我们需要在接口中添加方法时就需要修改使用了该接口的类
//这时候就需要添加默认方法即可,此时就不需要在类中重写,关键字为default,需要加入方法体
//默认方法也可以被覆盖重写
public default void Method03(){

System.out.println("This is Method03");
}

静态方法

静态方法:接口中不希望被被实现类使用的方法,关键字为static,只能通过接口名称调用,只能是public static

1
2
3
public static void Method04(){
System.out.println("This is Method04");
}

静态方法的调用

私有方法

Java9开始接口中允许定义私有方法private的方法只有接口自己可以调用,不能被实现类或别人调用

1
2
3
4
5
//普通私有方法
private 返回值类型 方法名(参数列表){方法体};

//静态私有方法
private static 返回值类型 方法名(参数列表){方法体};

常量

常量关键字为public static final,final表明为这个值不可被修改

一个类实现多个接口

一个类的直接父类是唯一的,但是一个类可以同时实现多个接口

注意事项

  1. 如果实现类所实现的多个接口中存在多个重复的抽象方法,只需要覆盖重写一次即可
  2. 如果实现类没有覆盖重写所有抽象方法,则需要将实现类定义为抽象类
  3. 如果实现类所实现的多个接口中存在重复的默认方法,那么需要对默认方法进行覆盖重写
  4. 如果实现类的父类方法和接口中的默认方法冲突时,优先调用父类的方法

移动端攻防技术

LLVM

Xposed框架介绍

Xposed更适用于长久化的使用,但是每次安装框架之后都需要重启,这也是其麻烦的一点

AS编写Xposed框架

Xposed框架本质上也是APK,但是我们需要让其被Xposed识别,所以我们先要安装好环境,由于Xposed很久没有发布了,不支持较高版本的Android8.0、8.1以上,但是Magisk有Edxposed来代替

Root手机刷入Edxposed

抓包详解

在安卓App的逆向分析中,抓包通常是指通过一些手段来获取App与服务器之间传输的明文网络数据信息,我们可以通过获取到的信息快速定位关键接口函数的位置

主要有Hook抓包和中间人抓包

  1. Hook抓包:Hook抓包实际上是通过对发包函数的Hook来达到抓包的目的
  2. 中间人抓包:将一段完整的客户端-服务器的通信方式割裂为两段客户端-服务器通信

抓包的主要工具有Wireshark、BurpSuite、Charles、Fiddler,Fiddler不推荐使用

Charles抓包

Charles破解

其中手机中输入的代理主机名应为下面这个地址,使用ipconfig获取

安卓攻防技术

面试问题

  1. 动态加载方案:将u需要保护的代码单独编译成一个二进制文件,将其加密后保存在一个外部的二进制文件中。在外部程序运行的过程中再将保护的二进制文件解密并使用ClassLoader类加载器来动态加载和运行被保护的代码Android中每个Java类都是由ClassLoader类加载器加载和运行的
  2. App加固:最难绕过的保护手段就是App加固
  3. Root检测
  4. NDK:将关键代码写入native层,Java层只作为加载器和调用端
  5. 云端存储数据
  6. 反调试运行时检测和事先阻止反调试
    • 运行时检测:如果调用ptrace()函数进行进程附加,/proc//status文件中的TracePid变量会在进程被附加后由0变为附加进程的pid,如果此时代码本身单开一个线程对这个文件的TracePid值进行循环检测,异常时则退出进程,就做到了阻止进程被破解者调试
    • 时间差检测:调试的时候指令执行时间较长,我们可以基于此进行检测
    • 双进程保护:主要是基于一个进程最多只能被一个进程ptrace附加的特性,实现fork一个子进程ptrace,然后ptrace自己
  7. 代码混淆
    • 符号混淆:Google自带的混淆器ProGuard,主要是将有意义的名称改为a、b这种无符号的名称
    • 压缩文件大小:只要修改App/build/grale文件,将buildTypes中的minifyEnabled对应的值改为true即可
    • DexGuard:收费商业软件,是ProGuard的升级版,支持字符串加密、花指令、资源加密等

攻击:

  1. 静态分析和动态分析结合:IDA、GDB对so文件进行调试,Jeb、AS调试smali
  2. Hook和Trace
  3. 反反调试:手动patch检测代码逻辑后重新打包

App加固

App加固,类似动态加载,用加固厂商的壳程序包裹真实的App,在真实动态运行时再通过壳程序执行释放出来的真正的App

App加固的发展主要可以分为三个阶段

DEX整体加固

这个阶段的App加固的核心原理就是将DEX整体加密后动态加载,在对加密的文件解密之后调用DexClassLoader或者其他类加载函数来加载解密后的文件,由于对文件的操作过于明显,进阶为将加密的DEX在内存中进行加载的加固技术但还是可以通过在内存中搜索DEX文件头或在加载DEX的函数上下断点、进行Hook就可以找到解密数据

由于DEX整体加固总是将代码数据完整地存储在一段内存中,只要绕过反注入和反调试技术即可获取到数据

Frida脱壳

主要是基于Hook libart.so导出的OpenMemory函数,只在Android8.0以下才有

代码抽取保护

这个阶段App加固的关键在于真正的代码数据并不与DEX的整体结构数据存储在一起,就算DEX被完整地dump出来,也无法看到真正的代码逻辑

核心原理是利用私有函数,通过对其自身进程的Hook来拦截函数被调用时的路径,在抽取的函数被真实调用之前,将无意义代码数据填充到对应的代码区中(将Dex文件中的指令编码部分与Dex文件主体分离并独立执行加密操作,而原先的指令转为NOP指令,这样加载进内存中的Dex反编译后代码部分就是空的)

代码抽取技术并不会对App中所有的函数进行抽取保护,特别是第三方库。并且,代码抽取技术通常在函数被第一次调用后就不再将函数内容重新置空,因此只需要在App运行时多处发几次程序逻辑,然后再进行DEX的dump即可得到更加完整的DEX文件

FART脱壳

对抗指令抽取的首要目标就是要获取正确的被抽取的方法指令,而方法指令被执行前一定会被解密,可以借助Android调用类方法的机制,通过系统加载指令的函数访问到内存中解密的指令,进而导出

ART环境中常用的脱壳点:脱动态加载壳的本质是要获取在内存中处于解密状态的Dex文件,因此需要准确定位Dex文件在内存中的位置和大小,ART加载链接类时,Android会先调用LoadClass()函数去加载Dex文件中的类,然后调用LoadClassMembers()函数去初始化类的所有变量以及函数对象

1
void ClassLinker::LoadClassMembers(Thread* self, const DexFile& dex_file,const uint8_t* class_data,Handle<mirror::Class> klass,const OatFile::OatClass* oat_class)

其中第二个参数就是对当前处理的dex对象的引用,在这个引用中我们可以得到Dex对象,从而获取Dex文件在内存中的地址以及长度

Fart脱壳源码

DexHunter脱壳

通过主动加载DEX中的所有类并dump处所有方法对应的代码,最后将代码重构再填充回被抽取的DEX中

VMP与Dex2C

将所有的Java代码变成最终的native代码

区别

Ollvm

https://jev0n.com/2022/07/08/ollvm-1.html

https://blog.quarkslab.com/deobfuscation-recovering-an-ollvm-protected-program.html

https://www.52pojie.cn/thread-1488350-1-1.html

https://mrt4ntr4.github.io/MODeflattener/

OLLVM环境配置与编译so文件

OLLVM环境配置

参数详细说明

首先先配置好前面安装的NDK进行编译

然后创建jni目录,在jni目录下创建Android.mk,Application.mk,C/C++源文件

添加如下内容

Android.mk文件

1
2
3
4
5
6
7
8
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello
LOCAL_LDLIBS := -lm -llog
LOCAL_SRC_FILES := hello.cpp #自己的.c or .cpp文件
LOCAL_CFLAGS := -mllvm -sub -mllvm -bcf -mllvm -bcf_loop=3 -mllvm -bcf_prob=40 -mllvm -fla -mllvm -split_num=10

include $(BUILD_SHARED_LIBRARY)
1
2
3
4
5
6
7
8
//不同命令对应的混淆方式
-mllvm -fla:控制流扁平化

-mllvm -sub:指令替换

-mllvm -bcf:虚假控制流程

-mllvm -sobf: 字符串加密

Application.mk文件

1
2
3
4
5
6
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
APP_ABI := all

APP_PLATFORM=android-19
include $(BUILD_EXECUTABLE)

hello.cpp

1
2
3
4
5
6
7
8
9
10
#include <jni.h>
//需要注意函数的命名
extern "C"
{
jstring Java_com_example_myapplication_MainActivity_getStringFromJni(JNIEnv* jni, jobject obj)
{
return jni->NewStringUTF("Hello jni!");
}

}

接下来cd到jni目录中,执行ndk-build即可

然后在jni同路径下的libs目录即可找到编译完成的so文件

混淆效果展示

指令替换混淆

控制流平坦化

伪造控制流

Android反调试

init段的反调试