Notes-Android

基础

1、配置 ubutnu 模拟器环境

​ AS+genymotion 配置(396kyzdwPwp6b7A)

​ 2、手机/模拟器连接

1、USB线连接

2、wifi线,需要桥接,同一个网段,真机需要 root,打开开发者模式和USB调试

手残,在物理机开雷电模拟器,ubuntu的AS连接(要装ARM吗)

1
2
3
4
5
在ubuntu上,用AS的adb执行
adb connect 192.168.3.175:5555
adb disconnect ip:5555

adb -s 设备名称 //多个设备连接

基础语法自己看去教程

四大组件:活动(activity)、意图(Intent)、广播、服务(Service)

  • 活动(activity) -> 意图(Intent) ->活动(activity)
  • 服务(Service):安卓保活
  • 网络通信——HttpURLConnectio_okhttp
  • JNI
    • 让 Java 调用 C 语言或者 C++代码,让 C 调用 Java 代码
  • NDK
    • Android 的开发包,用于开发 C / C++的动态库,编译成 /so文件供 Java 调用,支持打包成 apk。NDK(Native Development Kit)是一套用于开发Android应用程序的工具集,它允许您在C/C++中编写性能关键的部分代码,并将这些代码与Java代码进行连接。
  • 实战
    • 创建 Native 包
    • 下载 NDK,CMake

参考资料:安卓逆向这档事

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// MainActivity.java
// 启动这个 java 代码,主要逻辑在 cpp 里,cpp代码也可以重新修改 java
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("topday",setMySonName("topday"));
sayHello("topday");
Log.d("topday","加法"+add(1,2));
}
private native String setMySonName(String name);
public native void sayHello(String speak);
public native int add(int a,int b);
}
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
extern "C"
// 此处静态注册方法,长的方法名
JNIEXPORT jstring JNICALL
Java_com_example_lesson16_MainActivity_setMySonName(JNIEnv *env, jobject thiz, jstring name) {
// TODO: implement setMySonName()
LOGE("我是来自Java层的 ->%s",env->GetStringUTFChars(name, JNI_FALSE));
std::string ss= "topdayforjni";
jstring jstring1 = env->NewStringUTF(ss.c_str());
return jstring1;
}

// 后续为动态注册
//public native void sayHello(String speak);
void sayHelloToJNI(JNIEnv *env, jobject thiz,jstring speak){
LOGE("我是来自Java层说话的的函数 ->%s",env->GetStringUTFChars(speak, JNI_FALSE));
}

//public native int sayNum(int i);
jint addToJNI(JNIEnv *env, jobject thiz,jint a,jint b){
return a+b;
}

static const JNINativeMethod nativeMethod[] = {
{"sayHello","(Ljava/lang/String;)V",(void*)sayHelloToJNI},
{"add","(II)I",(void*)addToJNI},
};

jint JNI_OnLoad(JavaVM *vm,void * reserved){
//初始化环境,每一个JNIEnv对应一个线程环境,且JNIEnv不能跨线程使用,如果想要在其他线程中使用JNIEnv则需要给当前线程附加JNIEnv环境
//vm->AttachCurrentThread(&env,0);//给当前线程附加JNIEnv环境
//vm->DetachCurrentThread();//分离当前线程环境
JNIEnv *env = NULL;

//给JNIEnv环境赋值
if(vm->GetEnv((void**)&env,JNI_VERSION_1_4)!=JNI_OK){//从JavaVM(java虚拟机)中获取线程的JNIEnv环境
return JNI_FALSE;
}
//获取class对象,classPathName为java类的完整包名+类名
jclass clazz = env->FindClass("com/example/test/MainActivity");
if(clazz == NULL){
return JNI_FALSE;
}
int result = env->RegisterNatives(clazz,nativeMethod,sizeof(nativeMethod) / sizeof (nativeMethod[0]));
//注册Java方法和c++方法的映射关系
if(result<0){
LOGE("方法映射失败");

}
return JNI_VERSION_1_4;
}

逆向环境 & 安卓基础

真机还没到,先用模拟器+面具+lsp

  • 抓包 Charles
    • 导入证书,手动导入 / chls.pro/ssl
    • 证书移动到系统目录,安装 Move_Certificates 模块,自动把证书移动到系统目录(雷电的 ROOT 怎么不稳定)
  • 数据加密原理
  • 安装 Postern
    • HTTPS= HTTP + SSL ,此处加密在 OSI 7层模型 的表示层
    • VPN 协议大多是作用在 OSI 的第二层和第三层之间。能抓到更多的流量
    • 利用 Postern 转发本地 SOCKS5 流量到 Charles
    • 手机请求 -> Postern(手机代理) ->charles -> 主机代理(charles 本地) -> 服务器

dex 文件就是字节码

  • 编译后的安卓程序代码文件
  • dex 反汇编后是 Smail 代码

反汇编工具

  • Jeb,带自动跳转,./jeb/jeb_linux.sh

基础语法

调试(Jeb 工具)

  • setprop ro.debuggable 1
  • Jeb 工具,CTRL + B 下断点,Tab 跳转到反编译源码(Smail -> Java)
  • 点击 start 附加在运行的程序上
  • 模拟器自带的 Dex 编辑器++能够直接重新打包

LSPosed

xposed -> edxposed -> lsposed

编写插件/组件、便于 Hook 的框架

1、正常创建项目,导入 api-82 包

2、修改 compileOnly files(‘libs/api-82.jar’) //只在编译时有效,不会参与打包

3、AndroidManifest.xml 新增模块

4、新建 assets 文件夹,指定 xposed_init 初始化模块

5、编写代码

参考:https://www.52pojie.cn/thread-1740944-1-1.html

原理:https://www.52pojie.cn/thread-1694093-1-1.html

某手利用代理的方式还是抓不到评论

1
2
3
4
语法:
1、正常类直接看案例
2、jadx 可直接右键复制 xposed 代码,修改 classLoader,指定外部类
3、匿名类,查看 Smail 语法,仿写 com.example.Hookdemo.Person$1

常用语法整理:

1
2
3
4
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(!loadPackageParam.packageName.equals("com.example.hookdemo")){
return;
}

Hook 类的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 包名.类名,参数类型,构造方法的上层类就是 Person
XposedHelpers.findAndHookConstructor("com.example.hookdemo.Person", loadPackageParam.classLoader, String.class, int.class, String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
//
}

@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});

Hook 类的普通方法

1
2
3
4
5
6
7
8
9
10
ClassLoader classLoader = loadPackageParam.classLoader;
Class<?> clazz = classLoader.loadClass("com.example.hookdemo.Person");

XposedHelpers.findAndHookMethod(clazz, "getName", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
//
}
});

Hook 类的内部类的构造方法

1
2
3
4
5
6
7
8
9
10
11
ClassLoader classLoader = loadPackageParam.classLoader;
Class<?> Person_clazz = classLoader.loadClass("com.example.hookdemo.Person");
// jadx 可直接右键复制 xposed 代码,修改 classLoader,指定上层类
XposedHelpers.findAndHookConstructor("com.example.hookdemo.Person$People", Person_clazz.getClassLoader(), Person_clazz,String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
// argument 1 has type com.example.hookdemo.Person
param.args[1] = "xxx";
}
});

Hook 类的内部匿名类

1
2
3
4
5
6
7
8
// 此处用 jadx 查看 Smail 语法,仿写,复制的不太准
XposedHelpers.findAndHookMethod("com.example.hookdemo.Person$1", loadPackageParam.classLoader, "eatFunc", String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
//
}
});

Hook 类的普通成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// private String name;
// 普通变量不能修改,只能获取初始值
// 获取普通属性就要实例化,staic属性不需要实例化
// 此处其实是 hook print 方法,内部逻辑获取成员变量
XposedHelpers.findAndHookMethod("com.example.hookdemo.Person", loadPackageParam.classLoader, "print", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);


//得到对象的两种方法
//1.param.thisObject;
//2.
ClassLoader classLoader = loadPackageParam.classLoader;
Class<?> Person_clazz = classLoader.loadClass("com.example.hookdemo.Person");

// 相当于 Person person = new Proson()
Object obj_person = XposedHelpers.newInstance(Person_clazz);

//获取普通类的 非静态变量
String name = (String) XposedHelpers.getObjectField(obj_person, "name");

// 修改静态成员才有效
//XposedHelpers.setObjectField(obj_person,"name","xxx");

Hook 类的静态成员变量(java反射)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获取并修改普通类的 private static int age
int xp_age = XposedHelpers.getStaticIntField(Person_clazz, "age");
Log.d("topday","获取普通类的 private_age ->"+xp_age);
XposedHelpers.setStaticIntField(param.thisObject.getClass(),"age",100);

//获取并修改普通类的 public static String address
String xp_address = (String) XposedHelpers.getStaticObjectField(param.thisObject.getClass(), "address");
Log.d("topday","获取普通类的 public static String ->"+xp_address);
XposedHelpers.setStaticObjectField(param.thisObject.getClass(),"address","M78星云");


//反射
Field age_id = param.thisObject.getClass().getDeclaredField("age");
age_id.setAccessible(true);
int age_int = (int)age_id.get(null);
Log.d("xxx","获取普通类的 private_age ->"+age_int);
age_id.set(null,100);

Hook 类的类参数的普通成员变量

1
2
3
4
5
6
7
8
// 实例化 Person 类的时候,类成员和匿名类同时也调用对应的构造函数
// 获取内部类的普通属性
// 获取普通属性就要实例化,staic属性不需要实例化
Class<?> PeopleClass = Person_clazz.getClassLoader().loadClass("com.example.hookdemo.Person$People");
// 相当于 Person.People people2 = new Person.People();
Object peoPleobject = XposedHelpers.newInstance(PeopleClass, obj_person);
String name1 = (String) XposedHelpers.getObjectField(peoPleobject, "name");
Log.d("xxx","获取内部类的普通属性 name ->"+name1);

Hook 类的匿名类的普通成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取匿名类的普通属性
// 获取普通属性就要实例化,staic属性不需要实例化
Class<?> niminmgclass = XposedHelpers.findClass("com.example.hookdemo.Person$1", Person_clazz.getClassLoader());
// 相当于 this.person1.nimingfangfa1();
Object nimingduixiang = XposedHelpers.newInstance(niminmgclass, obj_person);
int ceshi = XposedHelpers.getIntField(nimingduixiang, "ceshi");
Log.d("xxx","获取匿名类的普通属性 ceshi ->"+ceshi);

// 获取并修改匿名类的静态属性
int xp_anonymoutInt = XposedHelpers.getStaticIntField(niminmgclass, "anonymoutInt");
Log.d("xxx","获取匿名类的静态属性 xp_anonymoutInt ->"+xp_anonymoutInt);

// 修改静态成员才有效
XposedHelpers.setStaticIntField(niminmgclass,"anonymoutInt",666);

Hook 内主动调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
XposedHelpers.findAndHookMethod("com.example.hookdemo.Person", loadPackageParam.classLoader, "print", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);

Class<?> Person_clazz = XposedHelpers.findClass("com.example.hookdemo.Person",loadPackageParam.classLoader);
//非静态
XposedHelpers.callMethod(XposedHelpers.newInstance(Person_clazz),"print","1",100,"2");
//静态的
XposedHelpers.callStaticMethod(Person_clazz,"print","李四",99);

//内部类
Class<?> PeopleClass = Person_clazz.getClassLoader().loadClass("com.example.hookdemo.Person$People");
Object peoPleobject = XposedHelpers.newInstance(PeopleClass, XposedHelpers.newInstance(Person_clazz));
//非静态
XposedHelpers.callMethod(peoPleobject,"print","1内部",100,"2");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});

hookAllMethods & hookMethod

1
2
3
4
5
6
7
8
9
10
11
12
13
hookAllMethods // Hook所有同类方法(重载)
hookMethod // 替换

Class<?> Person_clazz = XposedHelpers.findClass("com.example.hookdemo.Person", loadPackageParam.classLoader);
Method print_id = Person_clazz.getDeclaredMethod("print", String.class, int.class);

XposedBridge.hookMethod(print_id, new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
Log.d("topday","两个参数 此函数已经被我替换了");
return null;
}
});

加壳 / 多dex导致找不到类

1
2
3
4
5
6
7
测试加固:
1、app 签名
2、上传到 360 加固
3、MT 管理器实现签名

hook attach
attach:https://blog.csdn.net/caoshen2014/article/details/103341690
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
ClassLoader cl= ((Context)param.args[0]).getClassLoader();
Class<?> hookclass=null;
try {
hookclass=cl.loadClass("com.example.hookdemo.Person$People");
}catch (Exception e){
Log.d("xxx","未找到类",e);
return;
}
XposedHelpers.findAndHookMethod(hookclass, "print", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Log.d("xxx","hook打印");
}
});
}
});

Tips:Xposed hook 在 java 层,SO 在 native 层,关键性能代码常在 native 层

Hook native(Dobby )

参考:https://blog.csdn.net/weixin_56039202/article/details/128591978

  • xposed 写一个 so 注入器
  • 新建 native 项目,编写 Dobby 代码,编译出自己的 so 文件(提取 apk 的 so 文件,指定绝对地址)
  • 把 so 放到指定目录
  • 运行注入器

类内有特殊参数

https://bbs.kanxue.com/thread-215039.htm

1
2
3
4
5
6
7
用Xposed自身提供的XposedHelpers的findClass方法加载每一个类

XposedHelpers.findClass // 作为参数传入/实例化
XposedHelpers.findAndHookMethod

final Class<?> ArrayList= XposedHelpers.findClass("java.util.ArrayList", loadPackageParam.classLoader);
final Class<?> Map= XposedHelpers.findClass("java.util.Map", loadPackageParam.classLoader);

lspatch+shizuku 实现 Hook

  • LSPosed框架的免 Root 实现

https://duzhaokun123.github.io/2022/05/06/simple-lspatch-guide.html

1、电脑运行命令激活 Shizuku

2、LSPatch 显示 Shizuku服务可用

3、本地和打包运行(本机运行和单机运行)

XPosed调试流程

1、AS 编写 XPosed 代码,手机 ROOT,安装 LSP,通过 LSP 去指定模块、指定程序

2、免 ROOT 流程,激活 Shizuku,LSPatch 中 Shizuku 服务可用,在 LSPatch 中指定程序,运行后 LSPosed 脚本后,可在指定运行的模块。

Hook 代码交互

https://skyhand.blog.csdn.net/article/details/117674003

https://jasper1024.com/jasper/20220317024954/

frida

基础

  • firda->client 用 python安装即可,安装 frida-tools 自动安装 frida
  • firda 的版本随着 python,server 和 client 版本要对上
  • client ->写代码端、server ->接受代码执行
  • 打开编辑器 code .
  • 14.2.18
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
30
31
32
33
34
35
frida-ps的具体使用方法如下:  https://frida.re/docs/frida-trace/

#连接到USB设备查看进程列表 frida-ps -U
#连接到USB设备查看正在运行的应用 frida-ps -U -a
#连接到USB设备查看所有安装的应用 frida-ps -U -a -i
#指定查看某个设备 frida-ps -D a666666 -a

frida-trace -UF -j '*!*tesucanshu*'

// 设备相关
-D 连接到指定的设备,多个设备时使用。示例:frida-trace -D 555315d66cac2d5849408f53da9eea514a90547e -F
-U 连接到USB设备,只有一个设备时使用。示例fria-trace -U -F

// 应用程序相关
-f 目标应用包名。spawn模式。示例:frida-trace -U -f com.apple.www
-F 当前正在运行的程序。attach模式示例。示例:frida-trace -U -F或frida-trae -UF
-n 正在运行的程序的名字。attach模式。示例:frida-trace -U -n QQ
-N 正在运行的程序的包名。attach模式。示例:frida-trace -U -N com.apple.www
-p 正在运行的程序的pid。attach模式。示例:frida-trace -U -p 2302

// 方法相关,以下参数在一条跟踪命令中可重复使用
-I 包含模块。示例:frida-trace -UF -I "libcommonCrypto*"
-X 不包含模块。示例:frida-trace -UF -X "libcommonCrypto*"
-i 包含c函数。示例:frida-trace -UF -i "strtsr"
-x 不包名c函数。示例:frida-trace -UF -i "*MD5" -x "CC_MD5"
-a 包含模块+偏移跟踪。示例:frida-trace -UF -a 模块名\!0x7B7D48
-j 包含某个Java方法 实列frida-trace -UF -j '*!*tesucanshu*/isu'
!用来分隔MODULE和OFFSET,例如"gdi32full.dll!ExtTextOutW"
*代表匹配任意内容

-J 包含某个Java方法 实列frida-trace -UF -J '*!*tesucanshu*/isu'


// 日志相关
-o 日志输出到文件。示例:frida-trace -UF -j '*!*tesucanshu*/isu' -o run.log

LSPosed 和 Frida hook 流程

1
2
3
4
5
6
7
8
LSPosed:
1、安装面具(管理root权限和安装模块)、安装LSPosed(执行XPosed代码、指定程序、刷新需重启程序)
2、之前案例是编写 java 代码,常用 findAndHookMethod,重写 beforeHookedMethod 和 afterHookedMethod

Frida:
1、服务端在 app 上启动,自动注入 hook 的启动程序
2、客户端在主机启动,执行命令连接,而后编写 js 脚本进行 hook,动态刷新
3、可注入当前运行程序 / 自选包名(Attach/Spwan)

查看修改参数/返回值

文档有许多的例子:因为frida 出来很久了所以百度会有许多他的案例

https://blog.csdn.net/freeking101/article/details/112634649

https://kevinspider.github.io/frida/frida-hook-java/

https://www.anquanke.com/post/id/195869

https://frida.re/docs/examples/android/

https://www.cnblogs.com/angelyan/p/16941444.html 常用frida hook - Maple_feng - 博客园

1
2
3
4
5
6
frida -U -f com.example.hookdemo -l lesson2.js --no-pause
frida -UF -l lesson3.js
Tip:
1、HOOK默认是把对象hook没了,要主动返回或是继续操作(this/return)
2、默认已连接adb的设备会自动连接frida服务端,只有一台的情况下
3、雷电模拟器要下载x86_64版本
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
30
31
32
33
34
35
36
37
38
39
40
41
function main(){
Java.perform(function(){
if(Java.available){
//执行hook代码
// var person_class = Java.use("com.example.hookdemo.Person");
// console.log(person_class);
// person_class.$init.overload('java.lang.String', 'int', 'java.lang.String').implementation = function(a1,a2,a3){
// console.log("a1=>"+a1+" "+"a2=>"+a2+" "+"a3=>"+a3);
// var result = this.$init("fridaxxx",a2,a3)
// return result;

// }
// 查看并修改参数
let Person = Java.use("com.example.hookdemo.Person");
Person.setAddress.implementation = function (address2) {
console.log(`Person.setAddress is called: address2=${address2}`);
let address = "riben";
this["setAddress"](address);
};
// 查看并修改返回值
Person["getName"].implementation = function () {
console.log(`Person.getName is called`);
let result = this["getName"]();
console.log(`Person.getName result=${result}`);
return "lisi";
};
// 重载函数
Person["print"].overload('java.lang.String', 'int', 'java.lang.String').implementation = function (a1,a2,a3) {
console.log(`Person.print is called`);
this["print"](a1,a2,a3);
};
Person["print"].overload('java.lang.String', 'int').implementation = function (a1,a2) {
console.log(`Person.print is called`,a1,a2);
this["print"](a1,a2);
};
}
})
}
setImmediate(main); //立即加载main函数
//setInterval(main,1000);//延时hook
//frida -U -f com.example.hookdemo -l lesson1.js

主动调用静态、普通函数、内部类、匿名类

正式交付用 XPosed

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Java.perform(function(){
// let AnonymousClass1 = Java.use("com.example.hookdemo.Person$1");
// AnonymousClass1["eatFunc"].implementation = function (value) {
// console.log(`AnonymousClass1.eatFunc is called: value=${value}`);
// this["eatFunc"](value);
// };
// let People = Java.use("com.example.hookdemo.Person$People");
// People["$init"].overload('com.example.hookdemo.Person', 'java.lang.String').implementation = function (ins,name) {
// console.log(`People.$init is called: name=${name}`);
// this["$init"](ins,name);
// };
// })
Java.perform(function(){
//主动调用静态方法,jadx复制的默认是被动触发
// var perclass = Java.use("com.example.hookdemo.Person");
// perclass.print("xxx",20);

//调用普通方法
// var perclass = Java.use("com.example.hookdemo.Person");
// var perins = perclass.$new("xxx",20,"zhongguo");
// console.log(perins.getName());
// perins.print();

// //调用匿名类的方法
let AnonymousClass1 = Java.use("com.example.hookdemo.Person$1");
var AnonymousClass1_ins = AnonymousClass1.$new(perins); // perins为默认传参
AnonymousClass1_ins.eatFunc("dayu");

// //调用内部类的方法
// let People = Java.use("com.example.hookdemo.Person$People");
// let People_ins = People.$new(perins,"neibuxxx");
// People_ins.print();

// var perclass = Java.use("com.example.hookdemo.Person");
// console.log(perclass.name.value);

// 动态选择,运行中所有实例化过的都给匹配出来了
// Java.choose("com.example.hookdemo.Person",{
// onMatch:function(instance){
// console.log(instance.name.value);
// // instance.name.value = "11111";
// },onComplete:function(){
// console.log("end");
// }
// })

// ins.print();
})

特殊参数打印

记住几个,用到的时候去查

访问静态属性

1
2
3
4
5
6
7
8
9
10
11
12
13
Java.perform(function(){
var person_class = Java.use("com.example.hookdemo.Person");
// console.log(person_class._print.value); //属性名和方法名重复+_
Java.choose("com.example.hookdemo.Person",{
onMatch:function(instance){
console.log(instance)
instance.name.value = "xiugainame";
console.log(instance.name.value);
},onComplete:function(){
console.log("end");
}
})
})

遍历指定类的所有方法

https://www.jianshu.com/p/c9e6776516e8

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 此种方法会遍历重载、内部、get/set函数
// 理论是遍历已加载的类、但实际没实例化的都列出来了
Java.perform(function () {
Java.enumerateLoadedClasses({
onMatch:function(name,handle){
if(name.indexOf("com.example.hookdemo.Person")!=-1){
// console.log(name,handle);
// 得到class
var TargetClass = Java.use(name);
// 得到方法数组
var methods = TargetClass.class.getDeclaredMethods();
//遍历数组 进行hook
for(var i=0;i<methods.length;i++){
console.log(methods[i].getName())
}
}
},onComplete:function(){
console.log("serach end");
}
})


var classList = Java.enumerateLoadedClassesSync();
var parsedMethods = [];
// console.log(classList)
for (var i = 0; i < classList.length; i++) {
var targetClass = classList[i];
if (targetClass == "com.example.hookdemo.Person") {
// 得到class
var TargetClass = Java.use(targetClass);
// 得到方法数组
var methods = TargetClass.class.getDeclaredMethods();
//遍历数组 进行hook
for (var j = 0; j < methods.length; j++) {
// console.log(methods[j].getName())
var methodName = methods[j].getName();
parsedMethods.push(methodName);
// console.log(TargetClass, methodName, TargetClass[methodName].overloads.length);
}
parsedMethods.forEach(function (targetMethod) {
// traceMethod(TargetClass + "." + targetMethod);
console.log(TargetClass, targetMethod);
// 获取目标重载的方法数量
var overloadCount = TargetClass[targetMethod].overloads.length;
for (var x = 0; x < overloadCount; x++) {
TargetClass[targetMethod].overloads[x].implementation = function () {
console.log(1, targetMethod);
for (var a = 0; a < arguments.length; a++) {
console.log('arguments', a, arguments[a]);
}
return this[targetMethod].apply(this, arguments);
}
}
});
}
}
});

frida 集成工具 —— objection

https://www.anquanke.com/post/id/197657

1、集成各种常用 API,列出某个包的类/方法等
2、启动前 frida 必须正常连接

运行步骤:

1、查找包名 / 应用ID / PID

1
2
3
4
// 根据应用名查找包名
frida-ps -U -a
// 根据进程名查找包名
frida-ps -U

2、指定包名 / 应用ID / PID

1
2
3
4
5
6
7
objection  --help 可以查看命令
objection -g xxx/PID explore
1.如果使用PID/应用名称 那就是attach模式
2.如果使用包名,那就是spwan模式,并且会帮助我们自动启动app

-s "命令"
-c "路径"

3、常用命令

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
1.列出所有已加载的类:
android hooking list classes

2.搜索关键字类
android hooking search classes MainActivity

3.列出类的所有方法
android hooking list class_methods com.example.hookdemo.MainActivity

4.hook方法 可以带上参数 返回值 调用栈,监控
android hooking watch class_method com.example.hookdemo.MainActivity.tesucanshu2 --dump-args --dump-return

5.hook类的所有方法
android hooking watch class com.example.hookdemo.MainActivity

6.查看与取消hook
jobs list
jobs kill 049953

7.搜索实列:
android heap search instances com.example.hookdemo.Person

8.通过实例执行方法:
android heap execute 48164808 print

9.进入编辑器环境
android heap evaluate 48164808
写代码:clazz.print()
esc 键退出环境,enter回车执行

这个功能可以即时编写、出结果、即时调试自己的代码,不用再编写→注入→操作→看结果→再调整,而是直接出结果。

10.启动activity或service
查看可用的:android hooking list activities
启动:android intent launch_activity

也可以先使用android hooking list services查看可供开启的服务,然后使用android intent launch_service com.android.settings.bluetooth.BluetoothPairingService命令来开启服务。

11、枚举内存中的所有模块
memory list modules

12、枚举模块中所有导出函数
memory list exports muyangdemo --json 路径(结果太多时用)

Wallbreak

1、可集成在 objection 使用

2、用于快速分析 Java 类/对象结构

https://bbs.kanxue.com/thread-260110.htm

https://github.com/hluwa/Wallbreaker

attach模式 :Frida会附加app

Spawn模式:重新启动app

1、加载插件

1
plugin load '路径'

2、执行命令

1
2
3
4
plugin wallbreaker classdump '类名'
plugin wallbreaker classsearch '类名'
plugin wallbreaker objectdump '对象名'
plugin wallbreaker objectsearch '对象名'
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
function printStacks(name){
console.log("====== printStacks start ====== " + name + "==============================")

// sample 1
var throwable = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new());
console.log(throwable);

// // sample 2
// var exception = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
// console.log(exception);

console.log("====== printStacks end ======== " + name + "==============================")
}

var jclazz = null;
var jobj = null;

function getObjClassName(obj) {
if (!jclazz) {
var jclazz = Java.use("java.lang.Class");
}
if (!jobj) {
var jobj = Java.use("java.lang.Object");
}
return jclazz.getName.call(jobj.getClass.call(obj));
}

function watch(obj, mtdName) {
var listener_name = getObjClassName(obj);
var target = Java.use(listener_name);
if (!target || !mtdName in target) {
return;
}
// send("[WatchEvent] hooking " + mtdName + ": " + listener_name);
target[mtdName].overloads.forEach(function (overload) {
overload.implementation = function () {
//send("[WatchEvent] " + mtdName + ": " + getObjClassName(this));
printStacks("WatchEvent"+mtdName)

console.log("[WatchEvent] " + mtdName + ": " + getObjClassName(this))
return this[mtdName].apply(this, arguments);
};
})
}

function OnClickListener() {
Java.perform(function () {

//以spawn启动进程的模式来attach的话
Java.use("android.view.View").setOnClickListener.implementation = function (listener) {
if (listener != null) {
watch(listener, 'onClick');
}
return this.setOnClickListener(listener);
};

//如果frida以attach的模式进行attch的话
Java.choose("android.view.View$ListenerInfo", {
onMatch: function (instance) {
instance = instance.mOnClickListener.value;
if (instance) {
console.log("mOnClickListener name is :" + getObjClassName(instance));
watch(instance, 'onClick');
}
},
onComplete: function () {
}
})
})
}
setImmediate(OnClickListener);

实战 1

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

手动思路:

1、反编译源码,通过apk按钮定位到触发的类,以及多个疑似触发方法

2、通过 wallbreak dump出触发时的内存变量,定位到具体的触发方法

3、审视代码,定位密码变量,直接在内存中看

遗留问题:

1、frida 的 hook setOnClickListener 脚本无法手动注入

2、扒第一关源码算法后,打印的值疑似加密,但代码逻辑无加密

坑点:

1、hook equals 到死

2、要用 –no-pause 以免程序暂停

3、编写算法时,类可能会在 onstart 等地方初始化,或是依托特有代码进行处理(用 sendEmptyMessage 发送。handleMessage中处理)

1
2
3
4
5
6
7
8
Java.perform(function(){
var Str = Java.use("java.lang.String");
Str.equals.implementation = function(a1){
console.log(a1);
var re = this.equals(a1);
return re;
}
})

Hook 系统SO & 静态注册SO函数

https://www.anquanke.com/post/id/195215#h2-1

Process / Module / Memory / Interceptor.attach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var module = Process.findModuleByName("libhookdemo.so");
var pattern = "55 41 57 41 56 41 55 41 54 53 48 83 EC 18 49 89 FE 48 BD EF FF FF FF FF FF FF 3F";
console.log(module.base)
Memory.scan(module.base,module.size,pattern,{
onMatch:function(address,size){
//搜索成功
console.log('搜索到 ' +pattern +" 地址是:"+ address.toString());

},
onError:function(resaon){
console.log("搜索失败",resaon);
},
onComplete:function(){
console.log("搜索完成");
}
});
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 定位并读取系统so
var pointer = Process.findModuleByName("libc.so").base; //系统so
const r = Memory.alloc(0x10); // 分配16个字节
r.writePointer(pointer);
var buffer = Memory.readByteArray(r, 0x10);

// 自定义字符写入内存
var arr = [0x74,0x65,0x73,0x74];
const r = Memory.alloc(arr.length);
Memory.writeByteArray(r,arr);
console.log("readCString():"+r.readCString());
const newPtrstr = r.writeUtf8String("haha");
console.log("readCString():"+newPtrstr.readCString());

// 不确定so名,hook其中的导出函数
var arr = [0x74,0x65,0x73,0x74];
var funaddress = Module.findExportByName(null,"strstr");
Interceptor.attach(funaddress,{
onEnter:function(args){
// console.log(args[0].readCString());
// console.log(args[1].readCString());
console.log('Context information:');
//输出上下文因其是一个Objection对象,需要它进行接送、转换才能正常看到值
console.log('Context : ' + JSON.stringify(this.context));
//输出返回地址
console.log('Return : ' + this.returnAddress);
//输出线程id
console.log('ThreadId : ' + this.threadId);
console.log('Depth : ' + this.depth);
console.log('Errornr : ' + this.err);
}, onLeave: function (retval) {
// var r = Memory.alloc(0x16);
// r.writeByteArray(arr);
// retval.replace(r);
retval.replace(0);
}
})


// hook 自实现的静态so函数
var funaddress1 = Module.findExportByName("libhookdemo.so","Java_com_example_hookdemo_MainActivity_signatureTest");
Interceptor.attach(funaddress1,{
onEnter:function(args){
// console.log(hexdump(args[0]));
var env = Java.vm.getEnv();
if(env!=null){
var canshu = env.getStringUtfChars(args[2],false); //第几个参数可以ida看
console.log(canshu.readCString());
}
}, onLeave: function (retval) {
// console.log(retval.readCString())
var env = Java.vm.getEnv();
if(env!=null){
var canshu = env.getStringUtfChars(retval,false);
console.log(canshu.readCString());
}
}
})

// 主动调用自编写的某个so方法,inlinehook 任意函数,不知道具体函数名,根据地址去hook
var hookdemo_add = Module.findBaseAddress("libhookdemo.so");
var add_0D64 = hookdemo_add.add(0x667A0); // 此处指定的是此方法在内存中执行首地址
var newaddString_func = new NativeFunction(add_0D64,"pointer",['pointer','pointer']);
var env = Java.vm.getEnv();
var canshu1 = env.newStringUtf("test");
var canshu2 = env.newStringUtf("nihao");
canshu1 = env.getStringUtfChars(canshu1,false);
canshu2 = env.getStringUtfChars(canshu2,false);
var ret = newaddString_func(canshu1,canshu2);
console.log(ret.readCString());
if(hookdemo_add){
var add_0D64 = hookdemo_add.add(0x667A0);
console.log("add_0D64 =>",add_0D64);
Interceptor.attach(add_0D64,{
onEnter:function(args){
console.log("args[0] =>",args[0].readCString());
console.log("args[1] =>",args[1].readCString());
},
onLeave:function(retval){
console.log(hexdump(retval))
// console.log("retval",retval.readCString());
}
})
}

找到某方法的执行首地址
1、直接在IDA里的导出方法搜索,双击进入到具体的处理逻辑,在开头按Tab跳转汇编查看地址
2、找到某个已注册方法,并调用了目标方法,后续操作如上
3IDA的导出函数可以(静态so方法,及非动态的方法实现)

1、根据内存中的地址 hook 函数
2、不确定 so 名,hook 其中的导出函数
3、hook 自实现的静态 so 函数
4、根据内存地址去主动调用函数

Hook 动态注册SO函数:

https://github.com/lasting-yang/frida_hook_libart

1
2
3
4
5
6
7
8
9
10
原生打印出来的 symbols 需要转换
https://demangler.com/

hook sayhello,不知道包名求出这个注册函数的地址
RegisterNatives 函数专门用于动态注册,hook这个就行
动态注册在启动时就注入,用 attach 模式
Process.findModuleByName("libhookdemo.so") // 得到当前进程内so的地址

1、hook所有的so函数
2、hook动态注册函数

RPC 开放调用Java层和SO层函数

应用场景:https://bbs.kanxue.com/thread-259862.htm

修改默认端口(27042)

1
2
./frida-server -l 0.0.0.0:port
frida -H ip:port -F

用 web 服务(flask)映射手机内的接口

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
30
31
32
function javafunc(name){
var perins = null;
Java.perform(function(){
var perclass = Java.use("com.example.hookdemo.Person");
perins = perclass.$new(name,20,"zhongguo");

})
send(perins.getName())
var recy = recv('poke',function(value){
console.log(value.payload)
})
recy.wait()
return perins.getName();
}

function sofunc(a1,a2){
var hookdemo_add = Module.findBaseAddress("libhookdemo.so");
var add_0D64 = hookdemo_add.add(0x60D54);
var newaddString_func = new NativeFunction(add_0D64,"pointer",['pointer','pointer'])
var env = Java.vm.getEnv();
var canshu1 = env.newStringUtf(a1);
var canshu2 = env.newStringUtf(a2);
canshu1 = env.getStringUtfChars(canshu1,false);
canshu2 = env.getStringUtfChars(canshu2,false);
var ret = newaddString_func(canshu1,canshu2);
return ret.readCString();
}

rpc.exports = {
javafun:javafunc,
sofunc:sofunc,
}
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import frida
import time
import flask,requests
from flask import Flask, request

app = Flask(__name__)

def on_message(message, data):
# print(message)
if message['type'] == 'send':
print(message['payload'])
script.post({"type":"poke","payload":"woshipython"})
elif message['type'] == 'error':
print(message['stack'])
#直接运行python脚本即可,注入程序在Python指定
#进程名称附加
session = frida.get_usb_device(5000).attach('com.example.hookdemo')

#spwan 模式
# devices = frida.get_usb_device(5000)
# pid = devices.spawn(['com.example.hookdemo'])
# session = devices.attach(pid)

#连接非标准端口
# session = frida.get_device_manager().add_remote_device("192.168.10.5:1234").attach('com.example.hookdemo')

# source = """

# """
# 获取js脚本的功能代码
with open("./function.js")as f:
source = f.read()
# print(source)

script = session.create_script(source)
script.on('message', on_message)
script.load()

#spwan模式多出来的
# devices.resume(pid)
# time.sleep(1)
# print(script.exports.javafun("nihao"))
# print(script.exports.sofunc("nihao","muyang"))

#注册路由,并写响应函数index
@app.route("/")
def index():
s1 = request.args.get('a1')
s2 = request.args.get('a2')
return script.exports.sofunc(s1,s2)

if __name__ == "__main__":
app.run(host='0.0.0.0', port=8888, debug=True) # 运行程序 # app.run(host, port, debug, options)

1、js 编写 frida 脚本,把函数导出
2、python 脚本调用 js 导出函数,开启 flask 服务供第三方调用
3、frida Python与js脚本交互

https://blog.csdn.net/Qwertyuiop2016/article/details/114286877

Hook interface接口

搜 implement 关键词

加密算法

URLTOHEX

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
#include <stdio.h>
#include <string.h>

// unsigned char 数组来存储字符串时,内部的字符会以其对应的 ASCII 值进行存储
void encodeToHex(const unsigned char* data,int dataSize,char* encodeData){
const char* hexChars = "0123456789ABCDEF";
int i,j;
for (i=0,j=0; i<dataSize; ++i){
// 此处data[i]已经转换
// data[i] = 109(m) >> 4 = 01101101 >> 4 = 0110 & 0x0f
// 高四位 & 0x0f
encodeData[j++] = hexChars[(data[i] >> 4) & 0x0f];
// 低四位 & 0x0f
encodeData[j++] = hexChars[data[i] & 0x0f];
}
encodeData[j]='\0';
}

int main(){
// 一个字符一个字节
const char* inputData = "muxss";
int inputSize = strlen(inputData);
char encodeOutPut[strlen(inputData)];
encodeToHex(inputData,inputSize,encodeOutPut);
printf("Encode data: %s\n",encodeOutPut);
// 6D75787373
return 0;
}

字符串转换为十六进制的原理是逐个字符读取字符串中的每个字符,获取其对应的 ASCII 值,然后将该整数值转换为十六进制表示。

每个字符占一个字节,两个十六进制

Base64

ASCII 编码用 256(2的8次方)个字符对二进制数据进行编码

Base64 用 64(2的6次方)个字符对二进制数据进行编码,二进制数据到字符的过程

原理:

  • 用 64(2的6次方)个特定的 ASCII 字符来表示 256(2的8次方)个 ASCII 字符,也就是说三个 ASCII 字符经过 Base64 编码后变为四个 ASCII 字符显示(公约数为24),编码后数据长度比原来增加 1/3,不足 3n用”=”补齐。
  • 小写字母a-z、大写字母A-Z、数字0-9、符号”+”、”/“(再加上作为垫字的”=”,实际上是65个字符)有时候+/会变成 - , _
  • 第一步:将每三个字节作为一组,一个是24个二进制位
  • 第二步,将这24个二进制位分为四组,每个组有6个二进制位
  • 第三步,在每组前面加两个00,扩展成32个二进制位,即四个字节
  • 第四步,根据下表,得到扩展后的每个字节的对应符号,就是Base64的编码值
  • Base64编码表自查

111 => MTEx,每个字符占一个字节,每个字节有8位,总共有24位,而base64编码每个字符用6位表示,所以3个字节需要用4个可打印字符来表示。

1
2
3
4
5
6
F T D
70 84 68
01000110 01010100 01000100
(00)010001 (00)100101 (00)010001 (00)000100
17 37 17 4
R I R E
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Base64字符表
static const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

// Base64编码函数
char* base64_encode(const unsigned char* input, int length) {
int output_length = 4 * ((length + 2) / 3); // 计算编码后字符串的长度
char* encoded_data = (char*)malloc(output_length + 1);
if (encoded_data == NULL) {
return NULL;
}

int i, j;
//这一段是编码函数中的循环部分,用于对输入数据进行编码。
for (i = 0, j = 0; i < length; i += 3, j += 4) {
//每次处理3个字节的数据。首先,从输入数据中取出3个字节,分别赋值给byte1、byte2和byte3变量。如果输入数据长度不足3字节,则使用0填充。
unsigned char byte1 = input[i];
unsigned char byte2 = (i + 1 < length) ? input[i + 1] : 0;
unsigned char byte3 = (i + 2 < length) ? input[i + 2] : 0;

encoded_data[j] = base64_table[byte1 >> 2]; //将byte1右移2位得到高6位的值,作为base64_table的索引,获得对应的Base64字符。
encoded_data[j + 1] = base64_table[((byte1 & 0x03) << 4) | (byte2 >> 4)];//将byte1的低2位与byte2的高4位进行组合,得到一个8位的值,再将该值左移4位,得到base64_table的索引,获得对应的Base64字符。
encoded_data[j + 2] = base64_table[((byte2 & 0x0F) << 2) | (byte3 >> 6)];//将byte2的低4位与byte3的高2位进行组合,得到一个8位的值,再将该值左移2位,得到base64_table的索引,获得对应的Base64字符。
encoded_data[j + 3] = base64_table[byte3 & 0x3F];//将byte3的低6位作为base64_table的索引,获得对应的Base64字符。
}

// 添加填充字符
int padding = length % 3;
if (padding == 1) {
encoded_data[j - 2] = '=';
encoded_data[j - 1] = '=';
} else if (padding == 2) {
encoded_data[j - 1] = '=';
}

encoded_data[output_length] = '\0'; // 添加字符串结束符

return encoded_data;
}

// Base64解码函数
unsigned char* base64_decode(const char* input, int* length) {
int input_length = strlen(input);
if (input_length % 4 != 0) {
return NULL;
}

int output_length = input_length / 4 * 3; // 计算解码后数据长度
if (input[input_length - 1] == '=') {
output_length--;
if (input[input_length - 2] == '=') {
output_length--;
}
}

unsigned char* decoded_data = (unsigned char*)malloc(output_length);
if (decoded_data == NULL) {
return NULL;
}

int i, j;
for (i = 0, j = 0; i < input_length; i += 4, j += 3) {
unsigned char ch1 = strchr(base64_table, input[i]) - base64_table;
unsigned char ch2 = strchr(base64_table, input[i + 1]) - base64_table;
unsigned char ch3 = strchr(base64_table, input[i + 2]) - base64_table;
unsigned char ch4 = strchr(base64_table, input[i + 3]) - base64_table;

decoded_data[j] = (ch1 << 2) | (ch2 >> 4);
if (j + 1 < output_length) {
decoded_data[j + 1] = (ch2 << 4) | (ch3 >> 2);
}
if (j + 2 < output_length) {
decoded_data[j + 2] = (ch3 << 6) | ch4;
}
}

*length = output_length;
return decoded_data;
}

int main() {
const unsigned char* original_data = "Hello";
int original_length = strlen(original_data);

// Base64编码
char* encoded_data = base64_encode(original_data, original_length);
printf("Base64 encoded data: %s\n", encoded_data);

// Base64解码
int decoded_length;
unsigned char* decoded_data = base64_decode(encoded_data, &decoded_length);
printf("Base64 decoded data: ");
for (int i = 0; i < decoded_length; i++) {
printf("%c", decoded_data[i]);
}
printf("\n");

free(encoded_data);
free(decoded_data);

return 0;
}

消息摘要算法

消息摘要算法?

算法助手同理

md5

WT-JS

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
const config = {
showStacks: false,
showDivider: true,
}
Java.perform(function () {
// console.log('frida 已启动');
function showStacks(name = '') {
if (config.showStacks) {
console.log(Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Throwable').$new(name)))
}
}

function showDivider(name = '') {
if (config.showDivider) {
console.log(`==============================${name}==============================`)
}
}

function showArguments() {
console.log('arguments: ', ...arguments)
}

const ByteString = Java.use('com.android.okhttp.okio.ByteString')
const Encode = {
toBase64(tag, data) {
console.log(tag + ' Base64: ', ByteString.of(data).base64())
// console.log(tag + ' Base64: ', bytesToBase64(data));
},
toHex(tag, data) {
console.log(tag + ' Hex: ', ByteString.of(data).hex())
// console.log(tag + ' Hex: ', bytesToHex(data));
},
toUtf8(tag, data) {
console.log(tag + ' Utf8: ', ByteString.of(data).utf8())
// console.log(tag + ' Utf8: ', bytesToString(data));
},
toAll(tag, data) {
Encode.toUtf8(tag, data)
Encode.toHex(tag, data)
Encode.toBase64(tag, data)
},
toResult(tag, data) {
Encode.toHex(tag, data)
Encode.toBase64(tag, data)
},
}

const MessageDigest = Java.use('java.security.MessageDigest')
{
let overloads_update = MessageDigest.update.overloads
for (const overload of overloads_update) {
overload.implementation = function () {
const algorithm = this.getAlgorithm()
showDivider(algorithm)
showStacks(algorithm)
Encode.toAll(`${algorithm} update data`, arguments[0])
return this.update(...arguments)
}
}

let overloads_digest = MessageDigest.digest.overloads
for (const overload of overloads_digest) {
overload.implementation = function () {
const algorithm = this.getAlgorithm()
showDivider(algorithm)
showStacks(algorithm)
const result = this.digest(...arguments)
if (arguments.length === 1) {
Encode.toAll(`${algorithm} update data`, arguments[0])
} else if (arguments.length === 3) {
Encode.toAll(`${algorithm} update data`, arguments[0])
}

Encode.toResult(`${algorithm} digest result`, result)
return result
}
}
}

const Mac = Java.use('javax.crypto.Mac')
{
Mac.init.overload('java.security.Key', 'java.security.spec.AlgorithmParameterSpec').implementation = function (key, AlgorithmParameterSpec) {
return this.init(key, AlgorithmParameterSpec)
}
Mac.init.overload('java.security.Key').implementation = function (key) {
const algorithm = this.getAlgorithm()
showDivider(algorithm)
showStacks(algorithm)
const keyBytes = key.getEncoded()
Encode.toAll(`${algorithm} init Key`, keyBytes)
return this.init(...arguments)
}


let overloads_doFinal = Mac.doFinal.overloads
for (const overload of overloads_doFinal) {
overload.implementation = function () {
const algorithm = this.getAlgorithm()
showDivider(algorithm)
showStacks(algorithm)
const result = this.doFinal(...arguments)
if (arguments.length === 1) {
Encode.toAll(`${algorithm} update data`, arguments[0])
} else if (arguments.length === 3) {
Encode.toAll(`${algorithm} update data`, arguments[0])
}

Encode.toResult(`${algorithm} doFinal result`, result)
return result
}
}
}

const Cipher = Java.use('javax.crypto.Cipher')
{
let overloads_init = Cipher.init.overloads
for (const overload of overloads_init) {
overload.implementation = function () {
const algorithm = this.getAlgorithm()
showDivider(algorithm)
showStacks(algorithm)

if (arguments[0]) {
const mode = arguments[0]
console.log(`${algorithm} init mode`, mode)
}

if (arguments[1]) {
const className = JSON.stringify(arguments[1])
// 安卓10以上私钥是有可能输出不了的
if (className.includes('OpenSSLRSAPrivateKey')) {
// const keyBytes = arguments[1];
// console.log(`${algorithm} init key`, keyBytes);
} else {
const keyBytes = arguments[1].getEncoded()
Encode.toAll(`${algorithm} init key`, keyBytes)
}
}

if (arguments[2]) {
const className = JSON.stringify(arguments[2])
if (className.includes('javax.crypto.spec.IvParameterSpec')) {
const iv = Java.cast(arguments[2], Java.use('javax.crypto.spec.IvParameterSpec'))
const ivBytes = iv.getIV()
Encode.toAll(`${algorithm} init iv`, ivBytes)
} else if (className.includes('java.security.SecureRandom')) {
}
}

return this.init(...arguments)
}
}

let overloads_doFinal = Cipher.doFinal.overloads
for (const overload of overloads_doFinal) {
overload.implementation = function () {
const algorithm = this.getAlgorithm()
showDivider(algorithm)
showStacks(algorithm)
const result = this.doFinal(...arguments)
if (arguments.length === 1) {
Encode.toAll(`${algorithm} update data`, arguments[0])
} else if (arguments.length === 3) {
Encode.toAll(`${algorithm} update data`, arguments[0])
}

Encode.toResult(`${algorithm} doFinal result`, result)
return result
}
}
}

const Signature = Java.use('java.security.Signature')
{
let overloads_update = Signature.update.overloads
for (const overload of overloads_update) {
overload.implementation = function () {
const algorithm = this.getAlgorithm()
showDivider(algorithm)
showStacks(algorithm)
Encode.toAll(`${algorithm} update data`, arguments[0])
return this.update(...arguments)
}
}

let overloads_sign = Signature.sign.overloads
for (const overload of overloads_sign) {
overload.implementation = function () {
const algorithm = this.getAlgorithm()
showDivider(algorithm)
showStacks(algorithm)
const result = this.sign()
Encode.toResult(`${algorithm} sign result`, result)
return this.sign(...arguments)
}
}
}
})

对称加密算法 & 非对称加密算法

DES/CBC/PKCS5Padding 原理自查

代码如上

其他乱七八糟的

图解 HTTP

okhttp框架(学习hook绕过证书单向校验、证书双向校验)

https://github.com/WooyunDota/DroidSSLUnpinning?tab=readme-ov-file

https://codeshare.frida.re/@akabe1/frida-multiple-unpinning/

https://ch3nye.top/Android-HTTPS认证的N种方式和对抗方法总结

下载导入 httpstest,apk源码

SSL PINNING 更新公钥和证书:

1、修改百度公钥,from hex,tobase64

2、修改bing.com的证书校验,导出baidu的证书,校验的网站也改成百度

对抗VPN和代理

ProxyDroid

iptables——在app限制代理和vpn抓包下的另类 解决办法

r0capture

flutter (app开发框架) 抓包

IDA 搜索 ssl_client,选择所在函数头的特征码,operation-General-Number of opcode bytes (4)

用 frida hook app 内部 SSL 验证,把校验关了就能抓到包了。用在挂了代理/VPN抓不到包的场景

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
function hook_ssl_verify_result(address) {
Interceptor.attach(address, {
onEnter: function(args) {
console.log("Disabling SSL validation")
},
onLeave: function(retval) {
console.log("Retval: " + retval);
retval.replace(0x1);
}
});
}

// lib目录下
function hookFlutter() {
var m = Process.findModuleByName("libflutter.so");
var pattern = "FF 03 05 D1 FD 7B 0F A9 9A E3 05 94 08 0A 80 52 48 00 00 39 16 54 40 F9 56 07 00 B4 C8 02 40 F9 08 07 00 B4";
var res = Memory.scan(m.base, m.size, pattern, {
onMatch: function(address, size){
console.log('[+] ssl_verify_result found at: ' + address.toString());
hook_ssl_verify_result(address);
},
onError: function(reason){
console.log('[!] There was an error scanning memory');
},
onComplete: function() {
console.log("All done")
}
});
}

动态加载 dex,dex文件是apk的二进制包?

三代加壳技术

frida_dexdump,用objection加载插件,python启动

grep -ril “xxx” * //在dex目录搜关键词

一代壳直接dump,内存即完整

二代壳动态加载,youpk

第一次启动 frida 时的 venv 环境切换

抓取海外app的包

Charles_Proxy_External Proxy Settings_打勾前两个,用于设置 charles 的上游出口

app ->VPN -> Charles -> 本地proxy ->服务器

用在线工具把请求头直接转换成py代码、右键copy cURL Request

https://curlconverter.com

分析 x-xxx-devices

1、apk直接拖进 jadx

2、搜索 / 自吐脚本或者算法助手 / 手动定位

3、jadx 选中字段按 x,查找调用

4、默认手机里安装的apk在,/data/data/包名/shared_prefs

5、清除应用信息,缓存的文件就没了

6、有个a方法

https://boyyongxin.github.io/2021/06/10/%E5%A6%82%E4%BD%95%E6%8A%93%E5%8F%96%E5%9B%BD%E5%A4%96%E7%BD%91%E7%AB%99%E3%80%81app%E7%9A%84%E5%8C%85

1、搜索字段 x-xxx-signature
2、密码学基础:(这个key就是加密值)
2.1、mac.update(key)
2.2、doFinal(key)

(根据digest往上找,根据空字符串去做SHA256加密->FROM HEX->to base64)

HmacSHA256 update data Utf8:  digest:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
host:openapi.xxx.com
key-id:gf665hj0kpsdkpllgek11zrz
target:get /xxxapps/v3/bespoke/member/search-with-ads/mmx?keywords=iphone case mockup&currency=CNY&buyer_features[app_foreground_time]=1730735563258&buyer_features[app_initial_start_time]=1730304497092&buyer_features[device_model]=OnePlus GM1910&buyer_features[network_type]=WWAN&with_static_filters=true&include_additional_listing_images=true&ship_to=CN&instant_download=null&category_prolist=1&sort_on=score&sort_order=down&include_featured_categories=false&with_deep_facets=1
timestamp:1730736403
version:1
x-xxx-device:1k5BCmYBShylRczcYkN5HQ
HmacSHA256 update data Hex:  6469676573743a3437444....
HmacSHA256 update data Base64:  ZGlnZXN0OjQ3REVRcGo4S....

HmacSHA256 doFinal result Hex:  a1dd6501b449be5b....
HmacSHA256 doFinal result Base64:  od1lAbRJvlvA794Un2/vXWnM6L0DWHriXmWEwENNiD4=
AES/CTR/NoPadding update data Utf8:  oauth2_access_token
AES/CTR/NoPadding update data Hex:  6f61757468325f6163636573735f746f6b656e
AES/CTR/NoPadding update data Base64:  b2F1dGgyX2FjY2Vzc190b2tlbg==
AES/CTR/NoPadding doFinal result Hex:  2def734bbf607b03703dbf58ca6ded8ebf578d
AES/CTR/NoPadding doFinal result Base64:  Le9zS79gewNwPb9Yym3tjr9XjQ==

3、算法还原(cyberchef / java代码)
hmac (key、SHA256)
FROM HEX
TO BASE64

1
2
3
4
5
6
7
device_id.py

UUID randomUUID = UUID.randomUUID();
ByteBuffer wrap = ByteBuffer.wrap(new byte[16]);
wrap.putLong(randomUUID.getMostSignificantBits());
wrap.putLong(randomUUID.getLeastSignificantBits());
return Base64.encodeToString(wrap.array(), 11);

xBox

1、frida-dump 脱壳多个dex,利用其他 frida 脚本 youpk / aprt / FART脱壳

1
2
3
4
5
6
7
8
frida脚本脱壳
1、dex写入目录需要权限
2、mkdir box_fart / mv *.dex box_fart/
3、adb pull xxx/box_fart
grep -ril "xxxMainActivity" //当前目录的dex搜索

Youpk
logcat

https://bbs.kanxue.com/thread-259854.htm

脱下来也是多个dex

疑问:为什么这里要用Youpk脱壳呢,用frida-dump不行嘛

2、过 root 检测

1
2
3
4
5
6
7
8
9
10
11
12
1、jadx会重命名,要hook原来的包名
2、js脚本
function xxx(){
Java.perform(function(){
xxx
})
}
function main() {
xxx()
}
setInterval(main, 1000);
// 有壳 frida 脚本要延迟加载

绕过后没数据,有VPN检测

利用 AS 的 logcat 可以查看当前手机运行的 app 打印的日志

用 AS 查看方便点,直接 adb logcat 也行

3、flutter框架的抓包方式

https://bbs.kanxue.com/thread-261941.htm

https://juejin.cn/post/7106300111927377956

1
2
3
4
5
6
7
8
9
10
11
12
13
直接按压缩包方式解压找到 libflutter.so,分析 SSL 入口点
1、IDA 打开 libflutter.so
2、Shift+F12 搜索 ssl_client
3、找引用
4、反汇编
5、找开头 sub_xxx



shift+f12:可以打开string窗口,一键找出所有的字符串,右击setup,还能对窗口的属性进行设置
x:对着某个函数、变量按该快捷键,可以查看它的交叉引用
g:直接跳转到某个地址
f5:一键反汇编

HttpCanary

wToken

4、RPC远程调用

PANDA(8.33.0)

md5可能是16位也可能是32位

1、算法助手和自吐脚本都是hook某个操作过程的算法操作,可用于分析加密

2、自吐脚本是通用的嘛

https://www.secpulse.com/archives/177572.html

request_pkcs12

Charles 抓包小tips & 问题汇总:

1、开启 Enabke SSL Proxy就是为了抓https的包,证书有问题的情况下,开启后无法访问

2、模拟器放到 /storage/emulated/0/Pictures/ 目录下

3、模拟器和主机重启,不行就再重启(postren走的是socket5)

4、真机每次切换证书可以试试重启,抓不到包换个浏览器,真机有时候只能用默认浏览器抓包,可能是证书安装有问题,其他浏览器不信任系统证书。但APP是可以抓包的