加壳与脱壳
Art脱壳原理
源码分析
InMemoryDexClassLoader
源码分析
- 1、
static jobject CreateSingleDexFileCookie(JNIEnv* env, std::unique_ptr<MemMap> data)
- 2、
static const DexFile* CreateDexFile(JNIEnv* env, std::unique_ptr<MemMap> dex_mem_map)
- 3、
DexFile::Open(location,
- 4、
OpenCommon(map->Begin()
- 5、
DexFile::DexFile(const uint8_t* base,
DexClassLoader
加载dex源码流程分析
- 1、
OpenAndReadMagic(filename, &magic, error_msg);
- 2、
DexFile::OpenCommon(const uint8_t* base,
- 3、
DexFile::DexFile(const uint8_t* base,
Art下实现函数抽取dex的难点在于dex2oat
如果dex2oat对抽取的dex进行编译生成了oat文件,那么动态修改的dex中的smali指令流也不会生效
解决方案
- dex指令抽取早于dex2oat
- 干掉dex2oat的过程
加壳厂商大多数干掉dex2oat
如果在ART下禁用掉dex2oat?
1 | // 调用dex2oat进行编译 |
如果在这个流程中任意一个阶段打断, dex2oat就不会成功
接下来就hookexecve的流程来干掉dex2oat
其中github上有一个项目TurboDex来达到了Hook dex2oat达到了第一次加载的时候快速加载完App
如果不干掉dex2oat第一次就会启动编译, 会非常耗时的, 所以干掉dex2oat就会提高加载dex的效率了
Art函数抽取实现方案
首先利用外部工具对dex的code_item代码指令给nop掉, 然后在从loadmethod中恢复这些指令
第一、抽取指令流程
- 1、解析原始dex文件格式,保存所有方法的代码结构体信息。
- 2、通过传入需要置空指令的方法和类名,检索到其代码结构体信息。
- 3、通过方法的代码结构体信息获取指令个数和偏移地址,构造空指令集,然后覆盖原始指令。
- 4、重新计算dex文件的checksum和signature信息,回写到头部信息中。
第二、指令还原流程
- 1、native层hook系统函数dexFindClass,获取类结构体信息。
- 2、获取类中所有的方法信息,通过指定方法名进行过滤,获取该方法的代码结构体信息。
- 3、获取该方法被抽取的指令集,修改方法对应的内存地址为可读属性,直接进行指令还原。
在这个过程中,抽取指令需要借助编写的工具,当然工具可以进行深入优化。可以留给你们进行完善。对于还原指令中,如何保存抽取指令并
没有介绍,这部分内容可以后续完善优化,比如我们可以将抽取的指令再做一次加密保存到程序中的某个地方,在指令还原的时候去这个文件
进行读取解密即可。
1 |
|
NDK开发
JNI:Java Native Interface的缩写,通常翻译为JAVA本地接口。从Java1.1开始,Java Native Interface(JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代
码进行交互。JNI一开始是为本地已编译语言,尤其是 C和C++而设计的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。
JNI不是Android特有,windows、linux等凡是有JVM的地方都支持JNI。Android的Dalvik/ART虚拟机同样支持JNI标准。
通过JNI,便可以打通Android里的两个世界:JAVA世界和Native世界。
可以说,JNI是Java和Native世界的桥梁。而背后的一切都由Dalvik/ART虚拟机来驱动。
Java语言的特点
特性:简单、面向对象、分布式、编译和解释性、健壮性、跨平台性(Write Once,Run Anywhere)、多线程、动态性等等
但是:性能较低、易于逆向分析等问题
因此,如何能够提高java程序的性能以及如何能够实现对海量已有C/C++代码库的复用问题?
NDK性能分析
java函数运行模式:
- 1、纯解释模式下执行;
- 2、JIT模式
- 3、经过dex2oat编译后在quick模式下运行
注意: Android 7.0(代号 Nougat,简称 N)开始结合使用 AOT、即时 (JIT) 编译和配置文件引导型编译。
因此一个java函数可能运行在解释模式、JIT或者quick模式。
分别比较相同代码逻辑的java函数和JNI函数的时间代价
java函数运行模式:
- 1、在4.4以前的dalvik下运行或者在ART下利用hook禁用掉dex2oat过程强制让其运行在解释模式;
- 2、使用Android6.0测试java函数在quick模式(运行dex2oat编译以后的汇编代码)
JNI函数实现:
- 1、和java函数相同的逻辑,纯C/C++实现;
- 2、和java函数相同的逻辑,经过JNI提供的接口频繁调用java函数;
直观感受使用JNI开发的性能提升和代码安全性比较
分别用java和jni实现一个简单的耗时函数,进行执行效率的比较
1 | 这里特别注明一下 |
Art包含一个编译器(dex2oat)工具和一个启动Zygote而加载运行时(libart.so), dex2oat工具接受一个apk文件, 并生成一个或多个编译工程文件, 然后运行时加载这些文件
文件的个数、扩展名和名称会因版本而异
- .vdex: 包含Apk未压缩的dex代码,另外还有加速验证速度的元数据
- .odex: 包含Apk中已经过AOT编译的方法代码
- .art(optional): 包含Apk中列出的某些字符串和类的ART内部表示, 用于加快应用启动速度
NDK
NDK 即Native Development Kit,因此称为“NDK”。
- NDK是一系列工具的集合。
NDK提供了工具链,帮助开发者快速开发以及调试C(或C++)的动态库。 - NDK提供了一份稳定、功能有限的API库。
不同于linux的glibc,Android采用的是Google Bionic Libc,大部分api是一致的。 - 一些重要逻辑、算法可以采用C/C++、甚至是汇编的形式通过NDK的工具链最终编译生成动态库,最后通过JNI完成和Dalvik/ART虚拟机环境中的Java代码的交互。
使用NDK开发的so不再具有跨平台特性,需要编译提供不同平台支持ABI:ApplicationBinary Interface
首先利用外部工具对dex的code_item代码指令给nop掉, 然后在从loadmethod中恢复这些指令
JNI函数特征一览
extern “C”:由于C++支持重载,C++类中的函数会有name mangling,后面会详细介绍为什么这里必须要有extern “C”
jni函数的参数问题:JNIEnv和jobject以及JNIEnv和jclass
JNICALL:空宏JNIEXPORT:__attribute__ ((visibility ("default")))
,代表当前函数符号需要导出,与之对应为hidden隐藏符号信息
AndroidStudio默认生成的函数名很长:如Java_com_example_jni_1enc_Jni_stringFromJNI,以下划线分割
NDK开发就是C/C++甚至是汇编结合JNI提供的一系列丰富的api来开发!
JNI基础数据
4种基本引用类型、8种基本类型
其中基本引用类型为:jobject, jclass, jstring, jthrowable
数组引用类型为:jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray, jobjectArray
JNI中的其他类型: 成员域ID和方法ID
成员域ID、方法方法ID
1 | struct_jfieldId; |
类描述符
在Java代码中的java.lang.String类的类描述符就是java/lang/String
其实在实践中, 我发现可以直接用该类型的域描述符取代, 也是成功的
例如: jclass intArrCls = env->FindClass("java/lang/String");
等同于: jclass intArrCls = env->FindClass("Ljava/lang/String");
数组类型的描述符则为 [+其类型的域描述符]
例如:1
2
3int[] -> [I
float[] -> [F
String[] -> [Ljava/lang/String
域描述符
Field Descriptor | Java Language Type |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
函数描述符
Java方法 | JNI函数签名 |
---|---|
String test(); | ()Ljava/lang/String; |
int f(int i, Object object); | (I,Ljava/lang/object)V; |
void set(byte[] bytes) | ([B)v |
String Api
处理返回类型是String,或者参数是String, c和c++与Java交互的过程
jni中的字符串操作
jstring NewStringUTF(const char* bytes)
函数使用给定的C字符串创建一个新的JNI字符串(jstring),不能为构造的java.lang.String分配足够的内存,NewStringUTF会抛出一个OutOfMemoryError异常,并返回一个NULL
const char* GetStringUTFChars(jstring string, jboolean* isCopy)
函数可用于从给定的Java的jstring创建新的C字符串(char *)
。
如果无法分配内存,则该函数返回NULL。 检查NULL是一个好习惯。不要忘记检查。因为该函数需要为新诞生的UTF-8字符串分配内存,这个操作有可能因为内存太少而失败。
失败时GetStringUTFChars
会返回NULL,并抛出一个OutOfMemoryError异常,在不使用GetStringUTFChars()
返回的字符串时,需要来释放内存和引用以便可以对其进行垃圾回收
因此之后应始终调用ReleaseStringUTFChars()
。
jsize GetStringUTFLength(jstring string)
用于获取jstring的长度
JNI_OnLoad和JNI_OnUnload
执行时机:当So被加载的时候,比如Java代码里面出现System.loadLibrary或System.load(绝对路径)
就会执行到JNI_OnLoad,它会取so导出符号表,然后调用完成
返回值返回的是JNI的类型
第一个参数就是Javavm指针
1 | JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) { |
JavaVM与JNIEnv
JavaVM是虚拟机在JNI层的代表, 一个进程只有一个JavaVM, 所有线程共用一个JavaVM
- JNIInvokeInterface_结构封装和JVM相关功能函数,如销毁JVM,获得当前线程的Java执行环境
- 在C和C++中JavaVM的定义有所不同, 在C中JavaVM是JNIInovkeInterface类型指针, 而在C++中又对JNIInovkeInterface进行了一次封装
- 推荐使用C++来编写JNI代码,比C中少了一个参数
JNIEnv表示Java调用native语言的环境, 是一个封装了大量JNI方法的指针
JNIEnv只是在创建它的线程生效, 不能跨线程传递, 不同线程的JNIEnv彼此独立
native环境中创建的线程, 如果需要访问JNI, 必须要调用AttachCurrentThread关联, 并使用DetachCurrentThread解除链接
两种代码风格(C\C++)
JavaVM和JNIEnv在C语言环境中和C++环境中调用是有区别的,主要表现在:
- C风格:
(*env)->NewStringUTF(env, "HelloWorld");
- C++风格:
(*env)->NewStringUTF("HelloWorld");
JavaVM的获取
- 1:在JNI_onLoad中作为参数获得,如下
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
该函数由Art负责自动化查找和传入参数进行调用 - 2:通过JNIEnv的GetJavaVM函数获取,如下
JavaVm * thisJvm = nullptr;
env->GetJavaVM(&thisJvm);
结构代码情况
1 |
|
经常使用到的GetEnv
main主线程使用,AttachCurrentThread
新建线程使用
而JniEnv提供了特别多的接口使用
测试案例
Java
1 | package com.kanxue.reflectiontest; |
Cpp
1 |
|
Java反射与NDK开发
利用Java反射可以获取所需私有成员或是方法
而对于public类型的成员变量和方法或属性都可以使用反射来进行访问
Java反射的相关类
类名 | 用途 |
---|---|
Class类 | 代表类的实体,在运行的Java应用程序中表示类和接口 |
Field类 | 代表类的成员变量 |
Method类 | 代表类的方法 |
Constructor类 | 代表类的构造方法 |
JNI新建对象和访问Java属性
MainActivity.Java
1 | package com.kanxue.reflectiontest; |
Test.java
1 | package com.kanxue.reflectiontest; |
native-lib.cpp
1 |
|
test.c
1 | // |
JNI访问Java类函数
MainActivity.java
1 | package com.kanxue.reflectiontest; |
Test.java
1 | package com.kanxue.reflectiontest; |
native-lib.cpp
1 |
|
test.c
1 | // |
全局引用、局部引用、弱全局引用
NDK内存管理:全局引用、局部引用、弱全局引用
Java当中的内存管理:透明的, 当新建类的实例时, 值需要在创建完这个类的实例之后, 拿着这个引用访问它的所有数据成员了(属性、方法)就可以了,事实上对于Java来说, 有一个垃圾回收器线程即GC线程负责将一些不再使用的对象回收
C/C++当中的内存管理,需要编码人员自己进行内存管理, 如在C++中new一个对象, 使用完了还要做一次delete操作, malloc一次同样也要调用free来释放相应的内存, 否则就会有内存泄漏
三种引用简介和区别
- 局部引用:通过NewLocalRef和各种JNI接口创建(FindClass、NewOBject、GetOBjectClass和NewCharArray等)【哪怕用了全局变量指针也是局部引用】,会阻止GC回收所引用的对象, 局部引用只能在当前函数中使用, 函数返回后局部引用所引用的对象会被JVM自动释放, 或者调用DeleteLocalRef手动释放, 因此局部引用不能跨函数调用, 不能跨线程使用
全局引用:调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象, 全局引用可以跨函数、跨线程使用。Art不会自动释放, 必须调用DeleteGlobalRef手动释放DeleteGlobalRef(g_cls_string),否则会出现内存泄露
1
2
3
4
5// 函数内创建出来
jclass tmpjclass = env->FindClass("com/kanxue/reflectiontest/Test");
testjclass = static_cast<jclass>(env->NewGlobalRef(tmpjclass));
// 在其他函数或者其他线程内部手动释放掉
env->DeleteGlobalRef(testjclass);弱全局引用: 调用NewWeakGLoablRef基于内部引用或全局引用创建,不会阻止GC回收所引用的对象, 可以跨方法、跨线程使用,但是与全局引用很重要不同的一点是, 弱引用不会组织GC回收它引用的对象, 但是引用也不会自动释放, 在ART认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放, 或调用DeleteWeakGlobalRef手动释放
1
2
3jclass tmpjclass = env->FindClass("com/kanxue/reflectiontest/Test");
testjclass = static_cast<jclass>(env->NewWeakGlobalRef(tmpjclass));
env->DeleteWeakGlobalRef(testjclass);
局部引用表
局部引用表并不是无限的, 一般在JNI函数调用结束后, art会释放这个引用
但是如果函数里面有大量的JNI循环调用Java函数, 那么因为局部引用太多而出现异常情况
1 | for (int i = 0; i < 2048; i++) { |
6.0的系统局部引用表最大512,会出现溢出的错误
局部引用的管理
- 函数返回时自动释放
- 手动调用DeleteLocalRef释放
- 使用JNI提供的一系列函数来管理局部引用的生命周期
JNI提供了一些函数来管理局部引用的生命周期
- EnsureLocalCapacity:如果需要创建更多的引用,可以通过调用EnsureLocalCapacity确保当前线程创建指定数量的局部引用,如果创建成功返回0,失败则抛出OutOfMemoryError异常
- NewLocalRef
- DeleteLocalRef
- PushLocalFrame
- PopLocalFrame
Push和Pop是局部引用创建一个引用堆栈和销毁栈中所有引用,因此不需要在关注获取一个引用后再调用DeleteLocalRef来释放引用,在调用PopLocalFrame销毁当前frame中的所用引用前,如果第二个参数result不为空, 会由result生成一个新的局部引用,再把这个新生成的局部引用存储在上一个frame中
MainActivity.java
1 | package com.kanxue.reflectiontest; |
Test.java
1 | package com.kanxue.reflectiontest; |
native-lib.cpp
1 |
|
Dalvik动态注册原理
静态注册和动态注册
1 | extern "C" JNIEXPORT void JNICALL |
对于任意一个jni函数来说,在该函数被调用前,必须要完成java函数与so中地址的绑定
这个绑定过程可以是被动的, 即由Dalvik、ART虚拟机在调用前查找完成地址的绑定
也可以是主动即由App自己完成地址的绑定
静态注册
- 1:对应函数名: Java+包名+类名+方法名
其中使用下划线将每部分隔开,包名也使用下划线隔开, 如果名称中本来就包含下划线,将下划线加数字替换
1 | 例如(包名:com.afei.jnidemo 类名:MainActivity) |
- 2:优点: 对于静态注册来说,简单明了, 语义清晰
- 3:缺点: 不够安全
必须遵从注册规则从而导致名称过长,由于保留了符号签名,很容易使用IDA等直接定位地址
动态注册
- 1:定义
通过RegisterNatives方法手动完成native方法和so中的方法的绑定,这样虚拟机就可以通过这个函数映射关系直接找到相应的方法了
- 2: 注册过程示例
假设有两个native方法如下
1 | public native String stringFromJNI(); |
通常我们在JNI_OnLoad方法中完成动态注册,事实上只需要在该jni函数被调用钱的任意时机完成注册即可, 甚至是多次注册到不同地址都可以
1 | JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) { |
env->RegisterNatives
的参数解析
- 第一个参数, 要注册上的Java类
- 第二个参数, 要动态注册的方法
- 第三个参数, 要动态注册的方法数量
JNIEXPORT
对应的就是__attribute__((visibility("default")))
默认展现出符号名
就是IDA直接能够提取到当前函数的原来函数名称
如果想要隐藏的话__attribute__((visibility("hidden")))
纠正这样的情况即可
Dalvik具体RegisterNatives实现的原理
dalvik/vm/Jni.cpp#RegisterNatives
->dvmRegisterJNIMethod
->dvmUserJNIBridge
Art动态注册原理
Art具体的RegisterNatives的实现
art/runtime/jni_internal.cc#RegisterNatives
->RegisterNativeMethods
->JNI::RegisterNativeMethods
->ReportInvalidJNINativeMethod
Java类定义nativeMethod都是ArtMethod
->FindMethod
(在Art虚拟机中找ArtMethod)
ClassLiner::LoadClassMembers
ArtMethod类的定义在art/runtime/art_method.h
文件中
Frida
objection不写代码Hook、Trace、Stack
apt install liblzma-dev
apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev
objection可以不用写代码直接用命令执行hook
- objection与Frida版本匹配安装
- frida 12.8.0
- frida-tools 5.3.0
- objection 1.8.4
- objection内存漫游
https://www.anquanke.com/post/id/197657
- objection -g com.android.settings explore 注入应用
- memory list modules 查看内存中加载的库
- memory list exports libssl.so 查看库的导出函数
- memory list exports libart.so –json /root/libart.json 将结果保存到json文件中
- memory dump all from_base 提取整个(或部分)内存
- memory search –string –offsets-only 搜索整个内存
- android heap search instances com.android.settings.DisplaySettings 在堆上搜索实例
- android heap execute 0x2526 getPreferenceScreenResId 调用实例的方法
- android heap evaluate 0x2526 在实例上执行js代码,进入迷你编辑器
- objection hook、trace、stack
- android hooking list classes 列出内存中所有的类
- android hooking list activities 列出内存中所有的activity
- android intent launch_activity xxxx 进入某个activity
- android hooking search classes display 内存中搜索所有的类
- android hooking list class_methods com.android.settings.DisplaySettings 列出类的所有方法
- android hooking watch class android.bluetooth.BluetoothDevice hook类的所有方法
- android hooking watch class_method android.bluetooth.BluetoothDevice.getName –dump-args –dump-return –dump-backtrace hook方法的参数、返回值和调用栈
- objection插件体系:WallBreaker
https://github.com/hluwa/Wallbreaker
plugin load /root/Desktop/Wallbreaker
- plugin wallbreaker classdump –fullname com.vivo.push.sdk.LinkProxyClientActivity
- objection + DexDump脱壳
- 配合fart-dexdump进行objection的插件, 进行dex导出处理
- plugin dexdump dump 接着寻找关键的Activity存在的dex文件 grep -ril “LoginActivity” *
objection主动调用,heap寻找实例,无法调用静态函数,只能在Frida来主动调用静态函数
Frida上手和逆向三段
- Frida基本操作
参数、返回值
调用栈(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable"),$new())
) - Frida精髓
- 方法重载
使用overload来选择方法重载确定的函数 - 参数构造
构造字符串参数Java.use(‘java.lang.String’).$new(“NIHAOJAVA”) - 动静态处理
动态函数的意思就是需要找到instance实例, 再去调用
静态函数则可以直接找到这个class调用 - 主动调用
- 忽略内部细节,直接返回结果
- 方法重载
- 逆向三段
- hook、invoke、rpc
1 | function main(){ |
Frida构造数组、对象、Map和类参数
1 | package com.r0ysue.a0526printout; |
数组/字符串对象数组/gson/Java.array
1 | Java.openClassFile("/data/local/tmp/r0gson.dex").load(); |
对象/多态、强制Java.cast
1 | public class Water { // 水 类 |
1 | var Waterhandle = null; |
接口Interface、Java.register
1 | package com.r0ysue.a0526printout; |
1 | var beer = Java.registerClass({ |
枚举
1 | package com.r0ysue.a0526printout; |
1 | Java.choose("com.r0ysue.a0526printout.Signal",{ |
泛型、List、Map、Set、迭代打印
1 | Map<String, String> mapr0ysue = new HashMap<>(); // 创建Map集合对象 |
1 | Java.choose("java.util.HashMap",{ |
重要思路:开发时如何打印、Frida中也是如何打印
hook的时机要控制好,比如App启动了,实例创建后马上销毁再hook也无济于事
non-ascii 方法名hook
https://api-caller.com/2019/03/30/frida-note/
比如说一些符号方法名,那就可以通过url编码, 之后再url解码, 就可以hook了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int ֏(int x) {
return x + 100;
}
Java.perform(
function x() {
var targetClass = "com.example.hooktest.MainActivity";
var hookCls = Java.use(targetClass);
var methods = hookCls.class.getDeclaredMethods();
for (var i in methods) {
console.log(methods[i].toString());
console.log(encodeURIComponent(methods[i].toString().replace(/^.*?\.([^\s\.\(\)]+)\(.*?$/, "$1")));
}
hookCls[decodeURIComponent("%D6%8F")]
.implementation = function (x) {
console.log("original call: fun(" + x + ")");
var result = this[decodeURIComponent("%D6%8F")](900);
return result;
}
}
)
Frida综合情景案例
- 调用静态函数和调用非静态函数
设置成员变量
1
Java.use("com.example.androiddemo.Activity.FridaActivity3").static_bool_var.value = true ;
内部类,枚举类的函数并hook, trace原型1
1
2
3
4
5
6
7
8
9
10
11
12var all_methods = InnerClass.class.getDeclaredMethods();
//console.log(all_methods);
var InnerClass = Java.use(class_name);
var all_methods = InnerClass.class.getDeclaredMethods();
for(var i = 0;i<all_methods.length;i++){
var method = all_methods[i];
//console.log(method.toString());
var substring = method.toString().substr(method.toString().indexOf(class_name)+class_name.length+1);
var finalMethodString = substring.substr(0,substring.indexOf("("));
console.log(finalMethodString);
InnerClass[finalMethodString].implementation = function(){return true};
}查找接口,hook动态加载dex,
补充一个找interface的实现"通杀"的方法
1
2
3
4
5
6
7
8
9
10
11
12
13Java.enumerateClassLoaders({
onMatch:function(loader){
try {
if(loader.findClass("com.example.androiddemo.Dynamic.DynamicCheck")){
console.log("Succefully found loader!",loader);
Java.classFactory.loader = loader;
}
} catch (error) {
console.log("found error "+error)
}
},onComplete:function(){"enum completed!"}
})
Java.use("com.example.androiddemo.Dynamic.DynamicCheck").check.implementation = function(){return true};枚举class, trace原型2
1
2
3
4
5
6
7
8
9
10Java.perform(function(){
Java.enumerateLoadedClasses({
onMatch:function(name,handle){
if(name.toString().indexOf("com.example.androiddemo.Activity.Frida6.Frida6")>=0){
console.log("name",name)
Java.use(name).check.implementation = function(){return true}
}
},onComplete(){}
})
})
找hook原则
- 开发视角
- hook点离数据越近越好
- 换安卓版本,换frida版本试试
1 | function main(){ |
RPC远程调用概念和实例
1 | import time |
逆向三段:第三段RPC
Remote Procedure Call 远程调用
利用这段可以进行多主机多手机多端口混连device = frida.get_device_manager().add_remote_device("192.168.1.102:9999")
远程调动,互联互通,动态修改
1 | Java.perform(function(){ |
1 | import time |
Rpc公开外网
通过flask的web框架公布到公网集群中去
nps: 一款轻量级、高性能、功能强大的内网穿透代理服务器。支持tcp、udp、socks5、http等几乎所有流量转发,可用来访问内网网站、本地支付接口调试、ssh访问、远程桌面,内网dns解析、内网socks5代理等等……,并带有功能强大的web管理端。
先通过nps的server段架设到公网
再通过nps的client段架设到Android手机上
建立tcp隧道通信,直接连通android frida server的端口,映射公网的端口
这样python脚本的frida rpc可以直接操作android手机客户端的frida server1
device = frida.get_device_manager().add_remote_device("公网ip:58888")
Zentracer是葫芦娃大佬写的Tracer工具,它基于Pyqt+Frida开发,短小精悍。
https://www.anquanke.com/post/id/196657
child-gating
子进程管理器
解析原先DEXDump的源码以及Zentrace的源码
综合实战
- spawn/attach 时机的选择
- 各种主动调用/直接lu完
- 各种hook及构造函数
- 动态加载自己的dex
- 约束求解/符号执行
1 | function main(){ |
1 | package com.example.lesson9; |
Xposed
一般公司稳定性选用xposed, frida一般都用于逆向测试, xposed用来开发产品并发布
Xposed是一款可以不修改Apk情况下影响程序运行的框架
基于它可以制作许多功能强大的模块,且在功能不冲突的情况下同时运作
在这个框架下,我们可以编写并加载自己编写的插件App,实现对目标Apk的注入、拦截等
Xposed作者出自国外大名鼎鼎的Android论坛Xda, 由作者rovo89进行更新和维护
在8.1的betal版后目前迟迟未见更新
xposed历史悠久, 几乎伴随着Google对每一个Android版本的升级, 上至Android2时代, 到现在的Android8, 在没有AndroidStudio作为Android开发的ide时代
Xposed与edxposed安装
Xposed 原理
控制Zygote进程,通过替换/system/bin/app_process
程序控制zygote进程,使得它在系统启动的过程中会加载Xposed Framework的一个jar文件即XposedBridge.jar,
从而完成对Zygote进程以及其创建Dalvik/Art虚拟机的劫持, 并且能够允许开发者独立的替代任何class, 例如framework本身, 系统UI又或者随意的一个App
Xposed的安装
4.4以下Android版本安装较为简单, 只需两步即可
- 对需要安装Xposed的手机进行root
- 下载并安装xposedinstaller, 之手授予其root权限, 进入app点击安装即可
很遗憾, 当前官网不再维护, 导致无法直接通过xposedinstaller下载补丁包
从Android5.0开始, 谷歌使用Art替换Dalvik, 所以xposed安装有点麻烦, 分为两个部分: xposed*.zip
和XposedInstaller.apk
zip文件是框架主体, 需要进入Recovery后刷入, apk文件是xposed模块管理应用, 主要用于下载、激活、是否启用模块等管理功能
- 完成对手机的Root,并刷入recovery(比如twrp);
- 下载对应的zip补丁包,进入recovery刷入;
- 重启手机, 安装xposedinstaller并授予root权限即可
如果你买了华为、小米,并不是Google亲儿子手机, 虽然说rom可以搜到,并且rom是定制的,这会踩很多的坑
所以还是推荐pixel或者nexus手机
从Android9,10等新版本安装Edxposed
对于Xposed官方不支持的较新的Android版本可以先安装magisk, 接着安装riyu模块, 最后再进行Edxposed的安装即可
Xposed优秀插件
隐私保护:Xprivacy
微信相关模块: 红包助手、防撤回、自动化营销等
QQ相关模块: QQ抢红包、QQ防撤回、记录QQ账号密码模块
开启Debug的
幸运破解器: 应用内购破解, 去广告神器
绿色守护: 切断后台唤醒、推送的神器
抓包神器: justtrustme
等等其他优秀插件
Xposed插件
- 拷贝XposedBridgeApi.jar到新建工程的libs目录
- 修改app目录下的build.gradle文件,在AndroidManifest.xml中增加Xposed相关内容
- 新建hook类, 编写hook代码
- 新建assets文件夹,然后在assets目录下新建文件xposed_init,在里面写上hook类的完整路径
编写准备
- 新建Android Studio工程,选择有无Activity皆可, 并将XposedBridge.jar拷贝到libs目录下, 然后双击App目录下的build.gradle文件, 将
1 | compile fileTree(include:{'*.jar'}, dir:'libs') |
如果使用compile,可以正常编译生成插件apk, 但是当安装到手机上后, xposed会报错无法正常工作
- 修改AndroidManifest.xml文件,在Application标签下增加内容如下
1 | <meta-data |
插件编写
新建hook类,命名为XModule,并实现接口IXposedHookLoadPackage即可, 并实现里面关键方法handleLoadPackage(XC_LoadPackage.LoadPackageParam lpParam)
,该方法会在每个软件被启动的时候回调,所以一般需要通过目标包过滤,内容如下
1 | public class XModule implementes IXposedHookLoadPackage{ |
插件告知
新建assets目录,在其中新建文本xposed_init
里面内容为实现了IXposedHookLoadPackage接口的hook类的完整类名
可以有多个,每一个hook实现类一行
Hook构造函数并修改属性
- 无参构造函数的hook
- 有参构造函数的hook
应用代码
1 | package com.kanxue.xposedhook01; |
Hook代码
1 | package com.kanxue.xposed01; |
xposed不仅实现对App类构造函数Hook,也可以hook系统层面的框架
HookDexClassLoader
1 | package com.kanxue.xposed01; |
HookFlag
1 | package com.kanxue.xposed01; |
Hook一般函数
- HookJava一般函数
HookJava内部类&匿名函数中的函数
1
2寻找ClassParentName$ClassChildName
直接拖到GDA寻找类名就可以了HookJNI函数
应用代码
Student.java
1 | package com.kanxue.xposedhook01; |
MainActivity
1 | package com.kanxue.xposedhook01; |
HookJavaFunction
1 | package com.kanxue.xposed01; |
HookJni
1 | package com.kanxue.xposed01; |
主动调用
- 利用Java反射完成对类函数的调用
- 使用Xposed的Api完成对类函数的调用
对于类静态函数直接调用
对于非静态函数,需要得到类的实例
HookActiveInvoke
1 | package com.kanxue.xposed01; |
加壳App处理
HookJava
1 | package com.kanxue.xposed01; |
之前Xposed的Hook代码只对一些未加壳的App
现在直接从loadPackageParam的classLoader找class是会出现classNotFound
如果加壳了的话, AdnroidManifest.xml中的application会出现android:name="com.stub.StubApp"
https://bbs.pediy.com/thread-252630.htm#msg_header_h2_1
Application的实现类StubApp的attachBaseContext和onCreate要做的事情就是如上图
所以我们要劫持StubApp的OnCreate之后它利用反射设置的ActivityThread里面的相关classLoader
这个classLoader才是加载应用载入的dex代码, 有这个classLoader我们就可以继续寻找应用代码的实例和hook相关功能
还有一点要说明, 可能这些加壳厂商自己定制了classLoader而不是使用了BaseClassLoader,所以这点可能有时候会有问题
HookBelo
1 | package com.kanxue.xposed01; |
除此之外还有一种非加固壳的, 只是利用了DexClassLoader进行了热加载或者外部加载Dex文件
这种也可以Hook, DexClassLoader的构造函数, 在用新出的DexClassLoader实例进行loadClass或者查找DexFile文件内容
so中函数处理
对so库中某个函数进行hook
第一步用Xposed进行hook的是System.loadlibrary
如果能hook成功的话, 接下来就是直接再利用System.load
加载我们的hook代码的so
然后把hook函数写在JNI_OnLoad以前的_init方法或者jni_init方法
System.load打破了原来的ClassLoader, 所以后期又推荐使用Runtime的load
1 | public static void loadLibrary(String libName) { |
所以hook代码又变成了这样
1 | XposedHelpers.findAndHookMethod("java.lang.Runtime", loadPackageParam.classLoader, "loadLibrary", |
MainActivity
1 | package com.kanxue.hookso; |
native-lib
1 |
|
XposedHookSo
1 | package com.kanxue.xposed01; |
so中64位函数处理
上面的hook框架inline hook在nexus5的机器上的Android 6.0是没有问题的
但是在android 8.0 以上的64位手机是有问题的;
主要是把build.gradle
中的部分属性配置注释掉就会有64位的平台ndk(这里默认就是4种平台的)
1 | ndk{ |
默认情况下,cmake会输出 4 种 ABI("armeabi-v7a" , "arm64-v8a", "x86", "x86_64")
64位的inline hook库, 有个EdXposed所使用的SandHook
native-lib
1 |
|
这里有个坑点, sandHook必须早于starthooklibc, 所以starthooklibc必须不能出现在_init,而是放置到JNI_OnLoad
so中函数主动调用
这里的函数偏移都是从IDA中看到的
Ida的Edit->Segments->Rebase program
填写ImageValue
thumb需要奇数 + 1
activecalltesthook
1 |
|
Xposed指纹检测及定制反检测
Xposed都有哪些指纹
- Xposed插件的管理: XposedInstaller
- Xposed对函数Hook的根本原理: Java函数变native
hook住检测方法,使其检测失效掉
- Xposed框架拥有的大量Api
- Xposed框架的特定文件
system/lib/libxposed_art.so
system/xposed.prop
system/framework/xposedBridge.jar
- 等等
Github上检测Xposed的demo: XposedChecker
Xposed的组成
- XposedInstaller:完整的AndroidStudio工程,用于管理Xposed框架的插件
- Xposed: 对Zygote进程定制,能够实现对接下来任何一个App进程的hook
- android_art: 用于支持对Java类函数hook而对Art源码进行的一系列定制
- XposedBridge: Xposed的Java部分, 可单独作为AndroidStudio工程进行编译
- XposedTools: 用于编译和打包Xposed框架
XposedChecker#MainActivity
1 | public class MainActivity extends AppCompatActivity { |
抓包全解
- 抓包的核心原理: 抓包处在明文状态下的一切实际
- Https抓包种类:
Http框架Hook
、系统框架hook
,中间人抓包
(中间人把一段通信切割两段通信) - 搭建环境抓取Http: Charles、BurpSuite
- Http的缺陷
- 通信使用明文(不加密),内容可能会被窃听
- 不验证通信方的身份, 因此有可能遭遇伪装
- 无法证明报文的完整性, 所以有可能已遭篡改
frida_ssl_logger
native的ssl日志
Https抓包详解
- Http + SSL + 认证 + 完整性保护 = Https
- 预共享密钥的非对称加密技术
- Https通信完整流程
- 中间人抓包核心原理
- Charles、BurpSuite开启SSL抓包
抓包环境
虚拟机,网络需要从Nat设置成桥接模式, Nat是一个子网, Nat下的虚拟机可以访问手机, 手机无法访问虚拟机
所以虚拟机先设置成桥接模式
虚拟机->编辑->虚拟网络编辑器
虚拟网络编辑器中设置桥接模式, 已桥接连接到正在上网的网卡上
Hook方案
Hook之sslSocket抓包
Hook之Socket抓包
Hook之网卡和路由器抓包
Arm&C++汇编原理
- Arm可执行程序生成过程
- Arm汇编
- Arm汇编指令集
- thumb、AArch64
Arm可执行程序的生成过程
- 预处理(Preprocessing)
- 编译(Compilation)
- 汇编(Assemble)
- 链接(Linking)
首先环境准备,我们得准备一个android的Ndk, 拿android最新的ndk内部/root/Documents/android-ndk-r21e/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
, 作为主要的clang编译器
至此可以拿clang --target xxx hello.c -o hello
进行各个平台target的编译操作
汇编语言组成结构
- cpu
- section段
.text
.section .rodata.str1.1,"aMS",%progbits,1
.section ".note.GNU-stack","",%progbits
- 符号
- 外部符号
bl printf- 子程序
.globl main- main
.fnstart/.fnend- 标号
字符串+冒号
:main:
这类型都是- 注释
@
1 |
|
预处理(Preprocessing)
clang -E hello.c -o arm_hello.i
切换到具体的目标clang -target arm-linux--android30 -E hello.c -o arm_hello.i
编译(Compilation)
clang -S arm_hello.i -o arm_hello.s
切换到具体的目标clang -target arm-linux--android30 -S arm_hello.i -o arm_hello.s
汇编(Assemble)
clang -c arm_hello.s -o arm_hello.o
(小写c)
切换具体的目标clang -target arm-linux--android30 -c arm_hello.s -o arm_hello.o
链接(Linking)
clang x86_hello.o -o x86_hello
切换具体的目标clang -target amr-linux--android30 x86_hello.o -o x86_hello
寄存器
R0~R7 未分组寄存器
R8~R12 是分组寄存器
R13~R14
R13 SP
R14 LR
R15 PC
CPSR
1 | $r0 : 0x0 |
Arm汇编寻址
数据处理指令的寻址方式
- 立即数(都要
#
符号)
mov r0, #0 @立即数寻址
mov r0, #123 @十进制数
mov r0, #0x12 @十六进制数值
- 寄存器
mov r1, #0 @r1 = 0
mov r0, r1 @r0 = r1
add r0, r1, r2 @r0 = r1 + r2
- 寄存器移位寻址
mov r0, r1, LSL #4 @ r0 = r1 << 4
- LSL 逻辑左移
- LSR 逻辑右移
- ROR 循环右移
- ASR 算术右移
- RRX 扩展的循环右移
Load Store寻址方式
ldr伪指令和mov是比较相似的。只不过mov指令限制了立即数的长度为8位,也就是不能超过512。而ldr伪指令没有这个限制。
如果使用ldr伪指令,后面跟的立即数没有超过8位,那么在实际汇编的时候该ldr伪指令会被转换为mov指令。
1 | 例ldr r0, 0x12345678 |
- 立即数
- 寄存器
ldr r0, [r2, r0, LSL #2] @ r0 = (r2 + (r0 << 2))
ldr r0, [r1] @r0=[r1]
ldr/ str
ldmia r3, [r4-r5] @从r3取个指给r4 , +4给r5, 再+4给r6
- 基址变址
str r0,[r3, #4] @ r3+4 = r0
strb r0,[r3, #4] @存一个字节
- 栈寻址
stmfd
ldmfd
Arm汇编指令
跳转指令
B 强制跳转
BL 带返回的跳转指令 往LR寄存器赋值一个地址,函数结束后回到LR的寄存器的返回地址
BLX 带返回+带状态切换(arm -> thumb, thumb->arm)
BX 带状态切换
数据处理指令
mov, add, sub, and, eor, orr, bic
1
2
3
4
5
6
7mov r0,r1
add r0, r1, r2 @r0 = r1 + r2
sub r0, r1, r2 @r0 = r1 - r2
and r0, r1, r2 @r0 = r1 & r2
eor r0, r1, r2 @r0 = r1 ^ r2
orr r0, r1, r2 @r0 = r1 | r2
bic r0, r1, #0xf @0x12345678 -> 0x12345670乘法指令
MUL r0, r1, r2 @r0 = r1 * r2
MLA r0, r1, r2 @r0 = r1 * r2 + r3
SMULL r0, r1, r2, r3 @ r0 = (r2 * r3)的低32位, r1 = (r2 * r3)的高32位
SMLAL r0, r1, r2, r3 @ r0 = (r2 * r3)的低32位 + r0, r1 = (r2 * r3)的高32位 + r1
UMULL r0, r1, r2, r3 @ r0 = (r2 * r3)的低32位, r1 = (r2 * r3)的高32位
UMLAL r0, r1, r2, r3 @ r0 = (r2 * r3)的低32位 + r0, r1 = (r2 * r3)的高32位 + r1
内存访问指令
ldr 4字节读取
ldrb 1字节读取
ldrh 2字节读取
str 4字节写入
strb 1字节写入
strh 2字节写入
Arm汇编程序开发
print函数
1 |
|
Arm汇编指令集
Arm和Thumb的区别
1
2
3
4
5 thumb-> 16,32
描述新增
.code 16
.thumb_func
Arm与Aarch64的区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 寄存器多了, x0是寄存器, w0是x0的低32位寄存器
64位有x0~x30, x0~x7传递参数, x0经常作为返回结果,
### x8当一个返回值大于x0返回的长度, 那么x8会作为x0的结果地址
x9~x15临时寄存器, x16~x17用来内部过程调用的寄存器
x18临时
x19~x28用来做备份
x29 用来做栈的寄存器 FP
### x30 LR返回地址寄存器
64位有专门的SP、PC、CPSR寄存器
我
还有arm和thumb的返回指是bx lr,但是aarch64都是ret直接返回
如何查看Arm官方文档
Arm汇编指令集
clang -target arm-linux-android30 -S hello.c -o hello.s
Thumb汇编指令集
clang -target arm-linux-android30 -S -mthumb hello.c -o hello.s
Armv8汇编指令集
clang target aarch64-linux-android30 -S hello.c -o hello.s
C程序逆向分析
- 数据类型
- 运算符
- 分支与跳转
- 循环
- 函数
- 数组与指针
- 结构体
- 位操作
数据类型&运算符&分支&循环
char & shourt
- strb:一个字节 byte
- strh:两个字节 short
- str: 四个字节
1 | strb r0, [r11, #-14] |
float
1 | mov r0, #26738688 |
double
- double d1 = 29.0;
- double d2 = 39.0;
1 | @ 0x403d0000 29.0 -> 1.1101 * 2^4 = 100 0000 0011 = 1023 + 4 |
运算符
1 | char ch3 = ch1 + ch2; |
1 | ldrb r0, [r11, #-13] |
分支
1 | if(n5 > 6){ |
1 | cmp r0,#7 # 分支 |
hello.c
1 |
|
hello.s
1 |
|
函数、结构体、数组、位操作
main.c
1 |
|
函数
1 |
|
静态变量
1 |
|
结构体
1 |
|
数组
1 |
|
位操作
1 |
|
C++程序逆向分析
- 类、对象的内存布局
- this指针
- 构造函数、析构函数
- 成员函数、成员变量
- 虚函数
- 继承、重载
- 异常
函数入口__libc_init
,再执行main函数
main_class.cpp
1 |
|
main_class_1.s
1 |
|
main_class_2.s
1 |
|
main_class_3.s
1 |
|
C++11&Art虚拟机开发
LLVM&OLLVM
- llvm简介与llvm编译,调试llvm
- llvm pass
- ollvm
- Control Flow Flattening(fla,控制流程平坦化)
- Bogus Control Flow(bcf, 虚假控制流程)
- Instructions Substitution(sub, 指令替换)
- StringObfuscation(sobf,字符串加密)
- 逆向ollvm的通用方法
- 逆向ollvm的非通用方法
llvm简介与调试
clang -emit-llvm -S hello_clang.c -o hello_clang.ll
将c代码转成IRlli hello_clang.ll
利用lli在本地环境执行这个IR代码llvm-as
将IR转成Bytecodellvm-dis hello_clang.bc -o hello_clang.ll
把Bytecode转成IRllc
将Bytecode转成二进制
调试Clang
使用clion调试的时候llvm/tools/driver/driver.cpp
为代码入口
直接使用clang命令进行一个c的编译工作,即可进入driver.cpp
的流程
具体编译的事项填入program arguments
调试opt
llvm自带的pass是用来精简优化的, ollvm是用来做混淆的
opt --help
自带的passopt --print-bb hello_clang.bc
:打印ByteCode的用途
pass是框架, 在llvm最重要的一部分, 可以完成编译器的转换和优化的工作, pass继承llvm:Pass类https://llvm.org/doxygen/classllvm_1_1Pass.html
一个案例pass
llvm/lib/Transforms/Hello/Hello.cpp
打印一下函数的名字
命令为:opt -load 'xxxx/llvm-project-9.0.1/llvm/cmake-build-debug/lib/LLVMHello.so' -hello hello_clang.bc
执行效果后就是把函数名字打印出来
如何调试
opt -load xxx -xxx hello.c
具体的参数填入program arguments
代码的opt入口在于llvm/tools/opt/opt.cpp
自写llvm pass
函数名称加密Pass
1 | 路径:/llvm/lib/Transforms/EncodeFunctionName |
CMakeLists.txt
1 | add_llvm_library( EncodeFunctionName MODULE |
EncodeFunctionName.cpp
1 |
|
编译命令
1 | ./bin/clang -emit-llvm -S /root/Documents/some_c_cpp/arm02/hello.c -o hello.ll |
在LLvm源码之外开发Pass
1 | 路径:/root/Document/some_c_cpp/arm02/OutPassEncodeFunctionName |
CMakeLists.txt-out
1 | cmake_minimum_required(VERSION 3.18) |
CMakeLists.txt-inner
1 | add_llvm_library(OutEncodeFunctionName MODULE |
OutEncodeFunctionName.cpp
1 |
|
编译命令
1 | /root/Documents/llvm-project-9.0.1/llvm/cmake-build-debug/bin/opt -load ./OutEncodeFunctionName.so -encode /root/Documents/some_c_cpp/arm02/hello.bc |
让Clang使用Pass
命令clang路由so文件
1 | clang -Xclang -load -Xclang /root/CLionProjects/outPassEncodeFunctionName/cmake-build-debug/OutEncodeFunctionName/OutEncodeFunctionName.so -encode hello.ll -o hello |
直接嵌套在clang llvm体系内
include
新增头文件在llvm/include/llvm/Transforms/EncodeFunctionName/EncodeFunctionName.h
1 | // |
source
1 |
|
IPO/PassManagerBuilder
路径:llvm/lib/Transforms/IPO/PassManagerBuilder
- 新增头文件
1 |
- PassManagerBuilder::populateModulePassManager
新增一条1
MPM.add(createEncodeFunctionName());
so动态库转a静态库
- EncodeFunctionName/CMAKELists.txt
1 | add_llvm_library(LLVMEncodeFunctionName |
- /llvm/lib/Transforms/LLVMBuild.txt
1 | [common] |
- /llvm/lib/Transfroms/IPO/LLVMBuild.txt
1 | [component_0] |
- /llvm/lib/Transfroms/EncodeFunctionName
1 | [component_0] |
调用命令
clang -mllvm -encode_function_name /root/Document/some_c_cpp/arm02/hello.bc -o /root/Document/some_c_cpp/arm02/hello
调试Pass
这里直接在外部的代码块打断点, 然后debug执行二进制opt即可
ollvm的简介和移植
OLLVM简介
ollvm是一个开源项目, 主要是llvm里面Transforms的子模块Obfuscator
OLLVM使用、特性
Bogus Control Flow
-mllvm -bcf
: activates the bogus control flow pass-mllvm -bcf_loop=3
: if the pass is activated, applies it 3 times on a function. Default: 1-mllvm -bcf_prob=40
: if the pass is activated, a basic bloc will be obfuscated with a probability of 40%. Default: 30
Control Flow Flattening
-mllvm -fla
: activates control flow flattening-mllvm -split
: activates basic block splitting. Improve the flattening when applied together.-mllvm -split_num=3
: if the pass is activated, applies it 3 times on each basic block. Default: 1
Substitution
-mllvm -sub
: activate instructions substitution-mllvm -sub_loop=3
: if the pass is activated, applies it 3 times on a function. Default : 1.
ControlFlowFlattening
移植ollvm代码到单独的so
请查看本地资料相关代码
调试ollvm pass
clang -Xclang -load -Xclang ../obfuscation.so hello.c -o hello
将相关的so进行debug, 执行以上的命令
调试Flattening
1 | Module |
国内查看评论需要代理~