Learn Learn Android Reverse
安卓基础
NDK开发
jni调用
-
什么是jni?
jni是Java Native lnterface的缩写。从java1.1开始,jni标准成为Java平台的一部分,允许Java代码和其他语言写的代码进行交互
-
GetStringUTFChars();将java字符串转换为c字符串.java的字符串在虚拟机中,通过硬编码调用,cstring在内存中.
-
env->functions->NewstringUTF(env, Result);将c字符串转化为java字符串返回
-
这写都是hook的关键位置,例如直接将返回的字符串输出
-
jni静态注册规则,而且跟java对接的函数会有两个默认的参数
Java_com_example_myapplication_MainActivity_stringFromJNI(JNIEnv* env, jobject /* this */) -
jobject与jclass区别:
jobject在动态声明时使用如public native String stringFromJNI();
jclass在静态声明时使用如public static native String stringFromJNI();
-
extern "C" JNIEXPORT jstring JNICALL的含义
-
extern "C"表示这个函数要用C的标准实现
-
JNIEXPORT的实现
#define JNIEXPORT __attribute__ ((visibility ("default")))如果设置为hidden,则此函数不会出现在export table中,对外部不可见,就不能被jni调用
-
jstring是返回值类型,返回一个jstring类型
-
-
如下c层调用java层代码,其中调用该函数时a1传参为JNIEnv* env
-
查看层调用java层那个函数,用arm架构的so文件打开,将第一个参数类型改为JNIEnv*,ida会自动识别函数

so加固
- SHT table规定了so文件中的一些内容从什么位置取,ida解析时与运行时调用可能不同,导致解析失败

- 运行时dump,fix
- 工具:SoFixer(readme中有使用方法,包含了dump与fix):https://github.com/F8LEFT/SoFixer
- 工具不适用的情况
自定义Linker,不按照原先标准so文件结构来写和调用自己的so文件
NDK介绍
-
什么是NDK?
一种交叉编译工具链,可以在pc端编译Android的程序,并且需要cmakelist指导进行build.
使用:https://developer.android.com/ndk/guides?hl=zh-cn -
so中的输出:__android_log_print(ANDROID_LOG_DEBUG,"tag","mes");
-
so输出函数的封装
#include <android/log.h> #define TAG "my_tag" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG,__VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_INFO, TAG,__VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_ERROR, TAG,__VA_ARGS__)...对应__VA_ARGS__表示可变参数
NDK多线程
-
创建多线程
int pthread_create(pthread_t* _Nonnull __pthread_ptr, pthread_attr_t const* _Nullable __attr, void* _Nonnull (* _Nonnull __start_routine)(void* _Nonnull), void* _Nullable);其中的pthread_t其实就是long,注意这个*,我们应该传入一个phread_t的地址,二四两个参数都穿nullptr就行,第三个参数传入一个函数的地址
pthread_create(&thread_pid, nullptr, reinterpret_cast<void *(*)(void *)>(p_fun), nullptr); -
等待进程执行结束
pthread_join(thread_pid, nullptr);
JNI_OnLoad
-
so中各种函数的执行时机
- 顺序: init,initarray,JNI_OnLoad
- 当so被加载(System.loadLibrary("my____");)时就会执行JNI_Onload,而不是调用so中函数时执行
- 返回值为jint,需要返回版本号
-
JNIEnv是一个结构体,里面有很多函数,可以实现java与c的交互,一般是so中使用java的函数.通过JavaVm *vm可以获得JNIEnv
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) { LOGD("this is from JNI_OnLoad"); JNIEnv *env = nullptr; if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { LOGD("GetEnv fail"); } return JNI_VERSION_1_6; }
JavaVm
- JavaVm是一个结构体(typdef _JavaVm JavaVM),里面有很多函数
- GetEnv()函数在主线程获得JNIEnv
- jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)在子线程中获取JNIEnv
- JavaVm分为c版本(JNIInvokeInterface)和c++版本
- c++版本其实就是封装了c语言的版本,区别是第一个参数JavaVm*被封装用this指针默认调用了
- 由上面一点就可以知道,看到调用c++的函数名,但是进入后发现多传了一个参数(一般是第一个参数)
- JavaVM的获取方式
- JNI_OnLoad的第一个参数
- JNI_OnLoad的第一个参数
- env->GetJavaVM
- JavaVm每个线程只有一个
JNIEnv
- 与JavaVm一样也分为c与c++版本,c++封装c,省略调用JavaEnv*
- JNIEnv也是一个结构体,有很多可用def(后续说)
- JNIEnv的获取方式
- 函数静态/动态注册,传的第一参数
- vm->GetEnv适用于主线程
- globalVM->AttachCurrentThread,这个适用于子线程,并且子线程调用vm->GetEnv会报错
- JavaEnv每个进程就有一个
各种表的相关概念
- 导入表(import)导出表(export)符号表
- 存在import和export表的都可用frida获得地址,如果没有就需要手动计算(base_addr+off)函数地址,因为so层的hook都需要得到地址
so函数注册
- 静态注册
- 命名规则:c的函数必须遵循Java_包名_类名_方法名
- 编译时并不与函数绑定,当加载so后第一次调用函数,才会根据命名规则去找这个函数
- 静态注册JNI函数必然在导出表中.
- 动态注册
-
获得类名,注意不再用.而用/写路径
jclass MainActivityClazz = env->FindClass("com/example/myapplication/MainActivity"); -
创建对应关系(JNINativeMethod)
typedef struct { const char* name; const char* signature; void* fnPtr; } JNINativeMethod;参数为java的函数名字,签名,c函数地址
- 签名:显示函数的参数,返回值;"(参数)返回值"
-
注册函数,注意如果一个函数注册了多次,以最后一次为准
env->RegisterNatives(MainActivityClazz,methods,sizeof (methods)/sizeof (JNINativeMethod));step2只是找到了函数名与函数名之间的对应关系,但是不同的类可以有同名函数,所以注册的时候需要将method对应的类也指定,第三个参数为注册的函数个数
-
so路径动态获取
-
安装好apk后,so会存在与data/app/packname-xxxxxxx,后面的是随机的.32和64的so存放路径不一样,为了更加通用,可以用代码动态获取so路径
public String getPath(Context cxt){ PackageManager pm = cxt.getPackageManager(); //创建包管理器 List<PackageInfo> pkgList = pm.getInstalledPackages(0); //将所有安装的apk转换为PackageInfo形式 if (pkgList == null || pkgList.size() == 0) return null; for (PackageInfo pi : pkgList) { //遍历pkgList,找到当前的nativeLibraryDir if (pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/") && pi.packageName.startsWith("com.xiaojianbang.demo")) { //Log.e("xiaojianbang", pi.applicationInfo.nativeLibraryDir); return pi.applicationInfo.nativeLibraryDir; } } return null; }
so相互调用(从次开始,直接copy ppt)
使用dlopen、dlsym、dlclose获取函数地址,然后调用。需要导入dlfcn.h
void *soinfo = dlopen(nativePath, RTLD_NOW);
void (*def)(char* str) = nullptr;
def = reinterpret_cast<void (*)(char *)>(dlsym(soinfo, "_Z7fromSoBPc"));
def("xiaojianbang");
通过jni创建Java对象
通过jni访问Java属性
通过jni访问Java数组
通过jni访问Java方法
通过jni访问Java父类方法
内存管理
子线程中获取Java类
init与initarray
so逆向
so逆向分析
-
防止重新打包
//获得当前签名,如果与原签名不同,则被重新打包
sha1 = getSha1(env,context);
strcmp(sha1,app_sha1) -
so中用env->functions->GetMethodID调用的java函数,也会在java层被hook
-
hook函数可以获取修改参数,获取修改返回值,替换函数
-
frida的Java层hook和so层hook,环境配置是一样的
-
so层hook只需要得到函数地址
- 通过frida提供的api来得到,该函数必须有符号(存在于导入表,导出表,符号表中均可)的才可以
- 通过计算得到地址:so基址+函数在so中的偏移[+1]
Frida枚举各种表,modules
-
通过枚举导入表,可以得到出现在导入表中的函数地址
var imports = Module.enumerateImports("libxiaojianbang.so"); for(var i = 0; i < imports.length; i++){ if(imports[i].name == "strncat"){ console.log(JSON.stringify(imports[i])); console.log(imports[i].address); break; }} -
通过枚举导出表,可以得到出现在导出表中的函数地址
var exports = Module.enumerateExports("libxiaojianbang.so"); for(var i = 0; i < exports.length; i++){ console.log(JSON.stringify(exports[i]));} -
通过枚举符号表,可以得到出现在符号表中的函数地址
Module.enumerateSymbols("libencryptlib.so")
Frida
Base
-
运行frida-server
evergo:/data/local/tmp # ./frida-server -
查看info
frida-ps -Uai- frida-ps:这是 Frida 工具中的一个命令,用于列出目标设备(如 Android)的运行进程信息。
- -U:表示针对通过 USB 连接的设备(包括物理设备或模拟器)列出进程。
- -a:显示所有进程信息,而不仅仅是当前用户拥有的进程。
- -i:详细显示每个进程的信息,包括进程 ID(PID)、进程名等。
-
可以用grep找到想要的程序
frida-ps -Uai | grep '<name_of_application>' -
得到包名后attach,之后就可以在终端输入js代码进行注入
frida -U -f <package_name>-f <package_name>:强制启动并附加到指定包名对应的应用程序。
frida -U -f <package_name> -l ./hook.js启动脚本
-
webstorm启用frida代码提示
npm i @types/frida-gum
-
可以用AS查看log
package:com.example.myapplication "关键字"

Hooking a method(java)
-
template
Java.perform(function() {var <class_reference> = Java.use("<package_name>.<class>"); <class_reference>.<method_to_hook>.implementation = function(<args>) { /* OUR OWN IMPLEMENTATION OF THE METHOD */ } })-
Java.perform 是 Frida 中的一个函数,用于为脚本创建一个特殊上下文,以便与 Android 应用程序中的 Java 代码进行交互。这就像打开一扇门来访问和操纵应用程序内部运行的 Java 代码。
-
var <class_reference> = Java.use("<package_name>.<class>");在这里,您声明一个变量 <class_reference> 来表示目标 Android 应用程序中的 Java 类。您可以使用 Java.use 函数指定要使用的类,该函数将类名作为参数。<package_name> 表示 Android 应用程序的包名称,
表示您要与之交互的类。 -
<class_reference>.<method_to_hook>.implementation = function(<args>) {}在所选类中,您可以使用 <class_reference>.<method_to_hook> 符号访问要挂接的方法,从而指定该方法。您可以在此处定义挂接方法被调用时要执行的逻辑。
表示传递给函数的参数。不用传参就什么都不填.
-
-
部分method是一启动程序就会运行,所以需要在启动前注入
frida -U -f com.ad2001.frida0x1 -l .\script.js -
-
无参数函数hook
Java.perform(function() { var MainActivity = Java.use("com.ad2001.frida0x1.MainActivity"); MainActivity.get_random.implementation = function() { console.log("hook!!!!") //return 5; //修改返回值 var ret_val = this.get_random(); //获得返回值 console.log("The return value is " + ret_val); } }) -
有参数hook
处理带有参数的挂钩方法时,使用overload(arg_type)关键字指定预期的参数类型非常重要。
Java.perform(function() { var a = Java.use("com.ad2001.frida0x1.MainActivity"); a.check.overload('int', 'int').implementation = function(a, b) { // The function takes two arguments - check(random, input) console.log("The random number is " + a); console.log("The user input is " + b); //上面只是hook了函数,得到了他的参数,但是并没有运行原函数. this.check(a,b); } })
-
-
-
调用类的静态函数
需要在程序启动后使用,不能在启动时直接调用js文件
Java.perform(function (){ var MainActivity = Java.use("com.ad2001.frida0x2.MainActivity"); MainActivity.get_flag(4919); }) -
调用未实例非静态类中的函数
template
Java.perform(function() { var <class_reference> = Java.use("<package_name>.<class>"); var <class_instance> = <class_reference>.$new(); // Class Object <class_instance>.<method>(); // Calling the method })运用
Java.perform(function (){ var check = Java.use("com.ad2001.frida0x4.Check"); var cn = check.$new() var flag = cn.get_flag(1337) console.log(flag) }) -
调用已实例非静态类中函数(先启动后,在终端输入执行)
template
Java.performNow(function() { Java.choose('<Package>.<class_Name>', { onMatch: function(instance) { // TODO }, onComplete: function() {} }); });- onMatch
- 对于 Java.choose 操作期间找到的指定类的每个实例,都会执行 onMatch 回调函数。
- 此回调函数接收当前实例作为其参数。
- 您可以在 onMatch 回调中定义要在每个实例上执行的自定义操作。
- function(instance) {},instance 参数表示目标类的每个匹配实例。您可以使用任何其他您想要的名称。
- onComplete
- 在 Java.choose 操作完成后,onComplete 回调执行操作或清理任务。此块是可选的,如果您在搜索完成后不需要执行任何特定操作,您可以选择将其留空。
运用
Java.performNow(function (){ Java.choose('com.ad2001.frida0x5.MainActivity',{ onMatch: function (instance){ console.log("hook!!!"); instance.flag(1337); }, onComplete:function (){} }) }) - onMatch
-
-
修改类中数据的value
template:
Java.perform(function (){ var <class_reference> = Java.use("<package_name>.<class>"); <class_reference>.<variable>.value = <value>; }) -
实例类并修改类中变量,再调用已实例非静态函数
Java.perform(function () { var checker = Java.use("com.ad2001.frida0x6.Checker"); var A = checker.$new() A.num1.value = 1234 A.num2.value = 4321 Java.choose('com.ad2001.frida0x6.MainActivity', { onMatch: function (instance) { console.log("hook!!!!") instance.get_flag(A); }, onComplete: function () { } }) }) -
Hook类的constructor
template:
//其中<args>的个数好像对此无影响 Java.perform(function() { var <class_reference> = Java.use("<package_name>.<class>"); <class_reference>.$init.implementation = function(<args>){ /* */ } });
Hook the Native
-
获得函数的地址
-
通过枚举导入表,可以得到出现在导入表中的函数地址
var imports = Module.enumerateImports("libxiaojianbang.so"); for(var i = 0; i < imports.length; i++){ if(imports[i].name == "strncat"){ console.log(JSON.stringify(imports[i])); console.log(imports[i].address); break; } } -
通过枚举导出表,可以得到出现在导出表中的函数地址
var exports = Module.enumerateExports("libxiaojianbang.so"); for(var i = 0; i < exports.length; i++){ console.log(JSON.stringify(exports[i])); } -
通过枚举符号表,可以得到出现在符号表中的函数地址
Module.enumerateSymbols("libencryptlib.so") -
对于在libc.so等的库中的导出函数strcmp
Module.findExportByName("xxxx.so","name")这里面的name要用蓝色的标志,"get_flag"检测不到

-
-
调用框架
Interceptor.attach(targetAddress, { onEnter: function (args) { console.log('Entering ' + functionName); // Modify or log arguments if needed }, onLeave: function (retval) { console.log('Leaving ' + functionName); // Modify or log return value if needed } });
Interceptor.attach:将回调附加到指定的函数地址。targetAddress 应该是我们要挂接的本机函数的地址。onEnter:当进入挂接函数时会调用此回调。它提供对函数参数 (args) 的访问。onLeave:当挂接函数即将退出时会调用此回调。它提供对返回值 (retval) 的访问。Example:获得输入参数,并且以输出参数过滤函数调用//过滤strcmp使用,第一个参数输入用args[0] var strcmp_adr = Module.findExportByName("libc.so", "strcmp"); Interceptor.attach(strcmp_adr, { onEnter: function (args) { var arg0 = Memory.readUtf8String(args[0]); var cipher = Memory.readUtf8String(args[1]) if (arg0.includes("Hello")) { console.log("Hookin the strcmp function"); console.log(cipher) } }, onLeave: function (retval) { // Modify or log return value if needed } });
-
获得函数返回值,并修改返回值
var check_flag = Module.findExportByName("liba0x9.so", "Java_com_ad2001_a0x9_MainActivity_check_1flag"); Interceptor.attach(check_flag, { onEnter: function (args) { }, onLeave: function (retval) { console.log(retval); retval.replace(1337); } }); -
Call Native Function
var native_adr = new NativePointer(<address_of_the_native_function>); const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']); native_function(<arguments>);var native_adr = new NativePointer(<address_of_the_native_function>);
创造一个NativePointer对象,允许我们从frida调用本机函数
const native_function = new NativeFunction(native_adr, '
', ['argument_data_type']); 创建函数
native_function(
); 调用函数
Frida检测
- 检测当前进程的内存映射文件/proc/self/maps中是否有
frida,LIBFRIDA字样result = fopen("/proc/self/maps", "r"); v3 = result; if ( result ) { while ( __fgets_chk(v4, 512LL, v3, 512LL) ) { result = (FILE *)strstr(v4, "frida"); if ( !result ) { result = (FILE *)strstr(v4, "LIBFRIDA"); if ( !result ) continue; } a1[9] = 1144197410; goto LABEL_8; } result = (FILE *)fclose(v3); }
日常学习
[Hgame2025 zundu]
- 该页面在app>rec>layout下,右上角可以调节试图

- 反编译后的string对应的位置,除此之外还有values-zh文件夹,两个里面会有共同默认内容,但是写代码生成的一般在values/string.xml中.

values-zh(简体中文)
values-en(英文)
values-ja(日语) - R.id用来为控件添加标识

- 绑定控件的两种方法
- 传统方式 (使用 findViewById 和 R.id)
// 使用传统的 findViewById 方式访问控件 Button button = findViewById(R.id.my_button); button.setText("Hello"); - 使用 View Binding (不需要 R.id)
-
启用View Binding
在 build.gradle 文件中启用 View Binding:
android { viewBinding { enabled = true } } -
使用
// 使用 View Binding 直接通过 binding 访问控件 binding.myButton.setText("Hello");
-
- 传统方式 (使用 findViewById 和 R.id)
动态调试与Magisk相关(mumu)
调试相关
-
apk动态调试获得debuggable权限: https://www.lisok.cn/reverse-programming/455.html
-
用magisk使ro.debuggable = 1
- adb shell # adb进入命令行模式
- su # 切换至超级用户
- magisk resetprop ro.debuggable 1
- stop;start; # 一定要通过该方式重启
-
如何打开Lsposed: https://lsposed.cn/229
-
adb检测 mumu模拟器的devices
-
方法一: 在问题诊断中看到自己的adb的端口
adb connect 127.0.0.1:port
-
方法二(暂未研究完): https://blog.csdn.net/weixin_44751043/article/details/129656573
- ~/.android/adb_usb.ini文件存储自定义的 USB 供应商 ID(Vendor ID),以便adb可以识别特定的Android设备
例如: (每一行是一个 Vendor ID)
0x18D1 # Google 设备 0x12D1 # Huawei 设备 0x04E8 # Samsung 设备
-
Magisk相关
- mumu安装Magisk,打开root后,对于Magisk-delta(其余未测试),直接可以使用.
- Zygisk意思是注入Zygote后的Magisk,提供更深入、更强悍的修改能力.他自带的排除列表sulist可以撤销Magisk做的所有修改,但不能隐藏root,不能隐藏Zygisk,而Riru Hide可以隐藏root,但对Magisk v24.0后不再支持(或者说已更名为Zygisk)
- 隐藏root可以用Shamiko模块
[N1CTF Junior]
- sdcard路径给不上可执行权限



浙公网安备 33010602011771号