Learn Learn Android Reverse

安卓基础

NDK开发

jni调用

  1. 什么是jni?

    jni是Java Native lnterface的缩写。从java1.1开始,jni标准成为Java平台的一部分,允许Java代码和其他语言写的代码进行交互

  2. GetStringUTFChars();将java字符串转换为c字符串.java的字符串在虚拟机中,通过硬编码调用,cstring在内存中.

  3. env->functions->NewstringUTF(env, Result);将c字符串转化为java字符串返回

  4. 这写都是hook的关键位置,例如直接将返回的字符串输出

  5. jni静态注册规则,而且跟java对接的函数会有两个默认的参数

      Java_com_example_myapplication_MainActivity_stringFromJNI(JNIEnv* env, jobject /* this */)
    
  6. jobject与jclass区别:

    jobject在动态声明时使用如public native String stringFromJNI();

    jclass在静态声明时使用如public static native String stringFromJNI();

  7. extern "C" JNIEXPORT jstring JNICALL的含义

    • extern "C"表示这个函数要用C的标准实现

    • JNIEXPORT的实现

      #define JNIEXPORT  __attribute__ ((visibility ("default")))
      

      如果设置为hidden,则此函数不会出现在export table中,对外部不可见,就不能被jni调用

    • jstring是返回值类型,返回一个jstring类型

  8. 如下c层调用java层代码,其中调用该函数时a1传参为JNIEnv* env

  9. 查看层调用java层那个函数,用arm架构的so文件打开,将第一个参数类型改为JNIEnv*,ida会自动识别函数
    img

so加固

  1. SHT table规定了so文件中的一些内容从什么位置取,ida解析时与运行时调用可能不同,导致解析失败
    img
  2. 运行时dump,fix
  3. 工具不适用的情况

    自定义Linker,不按照原先标准so文件结构来写和调用自己的so文件

NDK介绍

  1. 什么是NDK?

    一种交叉编译工具链,可以在pc端编译Android的程序,并且需要cmakelist指导进行build.
    使用:https://developer.android.com/ndk/guides?hl=zh-cn

  2. so中的输出:__android_log_print(ANDROID_LOG_DEBUG,"tag","mes");

  3. 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多线程

  1. 创建多线程

      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);
    
  2. 等待进程执行结束

      pthread_join(thread_pid, nullptr);
    

JNI_OnLoad

  1. so中各种函数的执行时机

    • 顺序: init,initarray,JNI_OnLoad
    • 当so被加载(System.loadLibrary("my____");)时就会执行JNI_Onload,而不是调用so中函数时执行
    • 返回值为jint,需要返回版本号
  2. 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

  1. JavaVm是一个结构体(typdef _JavaVm JavaVM),里面有很多函数
    • GetEnv()函数在主线程获得JNIEnv
    • jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)在子线程中获取JNIEnv
  2. JavaVm分为c版本(JNIInvokeInterface)和c++版本
    • c++版本其实就是封装了c语言的版本,区别是第一个参数JavaVm*被封装用this指针默认调用了
    • 由上面一点就可以知道,看到调用c++的函数名,但是进入后发现多传了一个参数(一般是第一个参数)
  3. JavaVM的获取方式
    • JNI_OnLoad的第一个参数
    • JNI_OnLoad的第一个参数
    • env->GetJavaVM
  4. JavaVm每个线程只有一个

JNIEnv

  1. 与JavaVm一样也分为c与c++版本,c++封装c,省略调用JavaEnv*
  2. JNIEnv也是一个结构体,有很多可用def(后续说)
  3. JNIEnv的获取方式
    • 函数静态/动态注册,传的第一参数
    • vm->GetEnv适用于主线程
    • globalVM->AttachCurrentThread,这个适用于子线程,并且子线程调用vm->GetEnv会报错
  4. JavaEnv每个进程就有一个

各种表的相关概念

  1. 导入表(import)导出表(export)符号表
  2. 存在import和export表的都可用frida获得地址,如果没有就需要手动计算(base_addr+off)函数地址,因为so层的hook都需要得到地址

so函数注册

  1. 静态注册
    • 命名规则:c的函数必须遵循Java_包名_类名_方法名
    • 编译时并不与函数绑定,当加载so后第一次调用函数,才会根据命名规则去找这个函数
    • 静态注册JNI函数必然在导出表中.
  2. 动态注册
    • 获得类名,注意不再用.而用/写路径

      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路径动态获取

  1. 安装好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逆向分析

  1. 防止重新打包

    //获得当前签名,如果与原签名不同,则被重新打包
    sha1 = getSha1(env,context);
    strcmp(sha1,app_sha1)

  2. so中用env->functions->GetMethodID调用的java函数,也会在java层被hook

  3. hook函数可以获取修改参数,获取修改返回值,替换函数

  4. frida的Java层hook和so层hook,环境配置是一样的

  5. so层hook只需要得到函数地址

    • 通过frida提供的api来得到,该函数必须有符号(存在于导入表,导出表,符号表中均可)的才可以
    • 通过计算得到地址:so基址+函数在so中的偏移[+1]

Frida枚举各种表,modules

  1. 通过枚举导入表,可以得到出现在导入表中的函数地址

      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; }}
    
  2. 通过枚举导出表,可以得到出现在导出表中的函数地址

      var exports = Module.enumerateExports("libxiaojianbang.so");
      for(var i = 0; i < exports.length; i++){
         console.log(JSON.stringify(exports[i]));}
    
  3. 通过枚举符号表,可以得到出现在符号表中的函数地址

      Module.enumerateSymbols("libencryptlib.so")
    

Frida

Base

  1. 运行frida-server

      evergo:/data/local/tmp # ./frida-server
    
  2. 查看info

    frida-ps -Uai
    
    • frida-ps:这是 Frida 工具中的一个命令,用于列出目标设备(如 Android)的运行进程信息。
    • -U:表示针对通过 USB 连接的设备(包括物理设备或模拟器)列出进程。
    • -a:显示所有进程信息,而不仅仅是当前用户拥有的进程。
    • -i:详细显示每个进程的信息,包括进程 ID(PID)、进程名等。
  3. 可以用grep找到想要的程序

      frida-ps -Uai | grep '<name_of_application>'
    
  4. 得到包名后attach,之后就可以在终端输入js代码进行注入

      frida -U -f <package_name>
    

    -f <package_name>:强制启动并附加到指定包名对应的应用程序。

      frida -U -f <package_name> -l ./hook.js
    

    启动脚本

  5. webstorm启用frida代码提示

    npm i @types/frida-gum

  6. 可以用AS查看log

    package:com.example.myapplication "关键字"
    img

Hooking a method(java)

  1. 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> 符号访问要挂接的方法,从而指定该方法。您可以在此处定义挂接方法被调用时要执行的逻辑。 表示传递给函数的参数。不用传参就什么都不填.

  2. 部分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 (){}
         })
      })
      
  3. 修改类中数据的value

    template:

      Java.perform(function (){
         var <class_reference> = Java.use("<package_name>.<class>");
         <class_reference>.<variable>.value = <value>;
      })
    
  4. 实例类并修改类中变量,再调用已实例非静态函数

    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 () {
          }
       })
    })
    
  5. Hook类的constructor

    template:

    //其中<args>的个数好像对此无影响
    Java.perform(function() {
    var <class_reference> = Java.use("<package_name>.<class>");
    <class_reference>.$init.implementation = function(<args>){
    
       /*
    
       */
    
    }
    });
    

Hook the Native

  1. 获得函数的地址

    • 通过枚举导入表,可以得到出现在导入表中的函数地址

      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"检测不到
      img

  2. 调用框架

    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
       }
    });
    
  1. 获得函数返回值,并修改返回值

    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);
       }
    });
    
  2. 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检测

  1. 检测当前进程的内存映射文件/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]

  1. 该页面在app>rec>layout下,右上角可以调节试图
    img
  2. 反编译后的string对应的位置,除此之外还有values-zh文件夹,两个里面会有共同默认内容,但是写代码生成的一般在values/string.xml中.
    img
    values-zh(简体中文)
    values-en(英文)
    values-ja(日语)
  3. R.id用来为控件添加标识
    img
  4. 绑定控件的两种方法
    • 传统方式 (使用 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");
        

动态调试与Magisk相关(mumu)

调试相关
  1. apk动态调试获得debuggable权限: https://www.lisok.cn/reverse-programming/455.html

  2. 用magisk使ro.debuggable = 1

    1. adb shell # adb进入命令行模式
    2. su # 切换至超级用户
    3. magisk resetprop ro.debuggable 1
    4. stop;start; # 一定要通过该方式重启
  3. 如何打开Lsposed: https://lsposed.cn/229

  4. 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相关
  1. mumu安装Magisk,打开root后,对于Magisk-delta(其余未测试),直接可以使用.
  2. Zygisk意思是注入Zygote后的Magisk,提供更深入、更强悍的修改能力.他自带的排除列表sulist可以撤销Magisk做的所有修改,但不能隐藏root,不能隐藏Zygisk,而Riru Hide可以隐藏root,但对Magisk v24.0后不再支持(或者说已更名为Zygisk)
  3. 隐藏root可以用Shamiko模块

[N1CTF Junior]

  1. sdcard路径给不上可执行权限
    img
    img
posted @ 2025-02-07 20:14  Un1corn  阅读(77)  评论(0)    收藏  举报