安卓基础
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类型
-
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,找到当前的nativeLibraryDirif (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>:强制启动并附加到指定包名对应的应用程序。
Hooking a method
-
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>;})