安卓安全從零到一: FRIDA hook Native基礎

2021-02-25 最小表達力
FRIDA hook Native基礎0. 致謝

以下內容全部來源於 @r0ysue


https://eternalsakura13.com/2020/07/04/frida/複製這段內容後打開百度網盤手機App,操作更方便哦 連結:https://pan.baidu.com/s/1__B7dnZ2xKBdXTNE_sklZg 提取碼:ljt1https://github.com/nickmyb/android_security_zero_to_one/blob/master/src/3_frida_hook_native/frida_hook_native_basic.md1. NDK開發入門

完整項目在 1_native_tutorial.7z

https://www.jianshu.com/p/87ce6f565d37https://www.jianshu.com/p/fde40a8d80d3https://developer.android.google.cn/ndk/index.html

JNI是做了二進位兼容的,所以C代碼編譯後可以在任意平臺中運行。

1.1 新建一個native項目Android Studio 新建native項目Start a new Android Studio projectFinish(選擇默認的Toolchain Default)安裝NDK(Install NDK '21.0.6113669' and sync project)Make Project(成功打包一個apk,生成在 /root/Codes/native_tutorial/app/build/outputs/apk/debug)

構建的app-debug.apk解壓後的lib文件夾下有arm64-v8a / armeabi-v7a / x86 / x86_64 四種架構的so

NDK編譯的工具鏈在/root/Android/Sdk/ndk/21.0.6113669/toolchains

Android Studio自帶platform-tools在 /root/Android/Sdk/platform-tools

1.2 Native的基本開發

就是一個基礎的native HelloWorld,相關的一些點都在代碼的備註部分。

/root/Codes/native_tutorial/app/src/main/java/com/example/native_tutorial/MainActivity.java

package com.example.native_tutorial;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());

        Log.i("native-lib", invokeMethodWithoutExternC());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();

    public native String invokeMethodWithoutExternC();
}

/root/Codes/native_tutorial/app/src/main/cpp/native-lib.cpp

#include <jni.h>
#include <string>


// JNI調用android的log
// https://www.jianshu.com/p/3c1aff6f100f
#include <android/log.h>
#define TAG "JNI_LOG"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)

/*
 * 1. 靜態註冊 art對於JNI函數的查詢方式決定了native方法的函數名構成
 *     Java_com_example_native_1tutorial_MainActivity_stringFromJNI
 * 2. extern "C"
 *     避免C++編繹器按照C++的方式去編繹C函數(效果就是保持函數籤名不變)
 *     Java_com_example_native_1tutorial_MainActivity_stringFromJNI 這種類型的native函數是必須帶extern "C",不然會導致 incorrect linkage
 * 3. 前兩個是默認參數 env + jobject(實例方法) / jclass(靜態方法)
 * 4. JNIEnv 和 JavaVM
 *     JNIEnv只在當前線程有效, 一個虛擬機一個JavaVM
 * 5. JNIEXPORT 和 JNICALL
 *     需要具體看C代碼中的定義
 */
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_native_1tutorial_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    // C/C++方法收到的是JNI類型,返回值也是JNI類型(例如jstring, jintArray),但是程序內部使用的是C/C++類型,所以中間需要JNI類型與C/C++類型的相互轉換。
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}


/*
 * C++ 使用 name mangling 實現重載函數籤名之間的區分,可以使用 `c++filt` 還原原始的函數籤名,內存中實際存在的是 name mangling 後的函數籤名
 * 不使用extern "C"就會被 name mangling
 * # c++filt _Z20methodWithoutExternCv
 * methodWithoutExternC()
 */
std::string methodWithoutExternC() {
    std::string str = "methodWithoutExternC";
    return str;
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_native_1tutorial_MainActivity_invokeMethodWithoutExternC(
        JNIEnv *env,
        jobject thiz) {
    std::string str = methodWithoutExternC();
    return env->NewStringUTF(str.c_str());
}

2. JNIEnv和反射

完整項目在 2_Reflect.7z

FART源碼解析及編譯鏡像支持到Pixel2(xl)https://www.anquanke.com/post/id/201896https://www.jianshu.com/p/9be58ee20dee2.1 JNIEnv基礎

/root/Codes/Reflect/app/src/main/java/com/example/reflect/MainActivity.java

package com.example.reflect;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());

        Log.i("native-lib", "getStringLengthFromJNI(\"hello\") = " + String.valueOf(getStringLengthFromJNI("hello")));
        Log.i("native-lib", "changeString2HelloFromJNI(\"Hello World!\") = " + changeString2HelloFromJNI("Hello World!"));
        Log.i("native-lib", "staticStringFromJNI() = " + MainActivity.staticStringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();

    public native int getStringLengthFromJNI(String j_str);

    public native String changeString2HelloFromJNI(String j_str);

    public static native String staticStringFromJNI();

}

/root/Codes/Reflect/app/src/main/cpp/native-lib.cpp

#include <jni.h>
#include <string>

#include <android/log.h>
#define TAG "JNI_LOG"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)


extern "C" JNIEXPORT jstring JNICALL
Java_com_example_reflect_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT jint JNICALL
Java_com_example_reflect_MainActivity_getStringLengthFromJNI(
        JNIEnv *env,
        jobject thiz,
        jstring j_str) {
    int length = env->GetStringUTFLength(j_str);
    return length;
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_reflect_MainActivity_changeString2HelloFromJNI(
        JNIEnv *env,
        jobject thiz,
        jstring j_str) {
    // C/C++方法收到的是JNI類型,返回值也是JNI類型(例如jstring, jintArray),但是程序內部使用的是C/C++類型,所以中間需要JNI類型與C/C++類型的相互轉換。
    const char * c_str = env->GetStringUTFChars(j_str, nullptr);
    LOGI("original j_str = %s", c_str);
    env->ReleaseStringUTFChars(j_str, c_str);

    jstring result = env->NewStringUTF("Hello");
    return result;
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_reflect_MainActivity_staticStringFromJNI(
        JNIEnv *env,
        jclass clazz) {
    // 實例方法第二個參數是jobject,static方法第二個參數是jclass
    std::string hello = "staticStringFromJNI";
    return env->NewStringUTF(hello.c_str());
}

2.1 反射

Xposed 和 FRIDA 都是基於反射

https://github.com/chame1eon/jnitracehttps://github.com/lasting-yang/frida_hook_libarthttps://mabin004.github.io/2018/07/31/Mac%E4%B8%8A%E7%BC%96%E8%AF%91Frida/https://bbs.pediy.com/thread-223713.htm

對於Java層和Native層通過反射獲取類以及對屬性和方法的基礎調用案例。

/root/Codes/Reflect/app/src/main/java/com/example/reflect/MainActivity.java

package com.example.reflect;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());

        Log.i("native-lib", "getStringLengthFromJNI(\"hello\") = " + String.valueOf(getStringLengthFromJNI("hello")));
        Log.i("native-lib", "changeString2HelloFromJNI(\"Hello World!\") = " + changeString2HelloFromJNI("Hello World!"));
        Log.i("native-lib", "staticStringFromJNI() = " + MainActivity.staticStringFromJNI());

        // test reflect
        testClazz();
        testField();
        testMethod();

        // invoke class Test from JNI
        String retFromJNI = invokeTestFromJNI();
        Log.i("retFromJNI", retFromJNI);
    }

    public void testClazz() {
        // 反射獲取類的方法一
        Class testClazz1 = null;
        try {
            testClazz1 = MainActivity.class.getClassLoader().loadClass("com.example.reflect.Test");
            Log.i("testClazz", "MainActivity.class.getClassLoader().loadClass(\"com.example.reflect.Test\"): " + testClazz1.getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        // 反射獲取類方法二
        Class testClazz2 = null;
        try {
            testClazz2 = Class.forName("com.example.reflect.Test");
            Log.i("testClazz", "Class.forName(\"com.example.reflect.Test\"): " + testClazz2.getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        // 反射獲取類方法三
        Class testClazz3 = Test.class;
        Log.i("testClazz", "Test.class: " + testClazz3.getName());
    }

    public void testField() {
        Class testClazz = Test.class;

        try {
            // 獲取任一public屬性
            Field publicStaticField_field = testClazz.getField("publicStaticField");
            Log.i("testField", "testClazz.getField(\"publicStaticField\"): " + publicStaticField_field);

            // 獲取任一屬性
            Field privateStaticField_field = testClazz.getDeclaredField("privateStaticField");
            Log.i("testField", "testClazz.getDeclaredField(\"privateStaticField\"): " + privateStaticField_field);

            // 獲取所有public屬性
            Field[] fields = testClazz.getFields();
            for(Field i: fields){
                Log.i("testField", "testClazz.getFields(): " + i);
            }

            // 獲取所有屬性
            Field[] declaredFields = testClazz.getDeclaredFields();
            for(Field i: declaredFields){
                Log.i("testField", "testClazz.getDeclaredFields(): " + i);
            }

            try {
                // 獲取屬性的值: 靜態傳入null,動態傳入對象
                String staticValue = (String) publicStaticField_field.get(null);
                Log.i("testField", "(String) publicStaticField_field.get(null): " + staticValue);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            try {
                // 獲取private屬性需要設置讀取權限
                // xposed原理分析 https://blog.csdn.net/zhangmiaoping23/article/details/52572447
                // setAccessible需要在get/set前設置
                privateStaticField_field.setAccessible(true);
                String staticPrivateValue = (String) privateStaticField_field.get(null);
                Log.i("testField", "(String) privateStaticField_field.get(null): " + staticPrivateValue);

                // 修改屬性值,靜態傳入null,動態傳入對象
                privateStaticField_field.set(null, "MODIFIED privateStaticField");
                staticPrivateValue = (String) privateStaticField_field.get(null);
                Log.i("testField", "(String) privateStaticField_field.get(null): " + staticPrivateValue);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    public void testMethod() {
        Class testClazz = Test.class;

        // 獲取public方法
        Method publicStaticFunc_method = null;
        try {
            publicStaticFunc_method = testClazz.getMethod("publicStaticFunc");
            Log.i("testMethod", "testClazz.getMethod(\"publicStaticFunc\"): " + publicStaticFunc_method);
            // 主動調用方法,靜態傳入null,動態傳入對象
            publicStaticFunc_method.invoke(null);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        // 獲取private方法
        Method privateStaticFunc_method = null;
        try {
            privateStaticFunc_method = testClazz.getDeclaredMethod("privateStaticFunc");
            Log.i("testMethod", "testClazz.getDeclaredMethod(\"privateStaticFunc\"): " + privateStaticFunc_method);
            privateStaticFunc_method.setAccessible(true);
            privateStaticFunc_method.invoke(null);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        // 只能列印public方法,並且基類的public方法也可以列印
        Method[] methods = testClazz.getMethods();
        for(Method i: methods){
            Log.i("testMethod", "testClazz.getMethods(): " + i);
        }

        // 可以列印自己的private方法,沒有基類
        Method[] declaredMethods = testClazz.getDeclaredMethods();
        for(Method i: declaredMethods){
            Log.i("testMethod", "testClazz.getDeclaredMethods(): " + i);
        }
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();

    public native int getStringLengthFromJNI(String j_str);

    public native String changeString2HelloFromJNI(String j_str);

    public static native String staticStringFromJNI();

    public static native String invokeTestFromJNI();
}

/root/Codes/Reflect/app/src/main/java/com/example/reflect/Test.java

package com.example.reflect;

import android.util.Log;

public class Test {
    private String identifier = null;
    public static String publicStaticField = "publicStaticField";
    public String publicField = "publicField";
    private static String privateStaticField = "privateStaticField";
    private String privateField = "privateField";

    public Test(){
        identifier = "Test()";
    }
    public Test(String arg1){
        identifier = "Test(String arg1)";
    }
    public Test(String arg1, int arg2){
        identifier = "Test(String arg1, int arg2)";
    }

    public static void publicStaticFunc(){
        Log.i("Test", "publicStaticFunc");
    }
    public void publicFunc(){
        Log.i("Test", "publicFunc");
    }
    private static void privateStaticFunc(){
        Log.i("Test", "privateStaticFunc");
    }
    private void privateFunc(){
        Log.i("Test", "privateFunc");
    }
}

/root/Codes/Reflect/app/src/main/cpp/native-lib.cpp

#include <jni.h>
#include <string>

#include <android/log.h>
#define TAG "JNI_LOG"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)


extern "C" JNIEXPORT jstring JNICALL
Java_com_example_reflect_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT jint JNICALL
Java_com_example_reflect_MainActivity_getStringLengthFromJNI(
        JNIEnv *env,
        jobject thiz,
        jstring j_str) {
    int length = env->GetStringUTFLength(j_str);
    return length;
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_reflect_MainActivity_changeString2HelloFromJNI(
        JNIEnv *env,
        jobject thiz,
        jstring j_str) {
    // C/C++方法收到的是JNI類型,返回值也是JNI類型(例如jstring, jintArray),但是程序內部使用的是C/C++類型,所以中間需要JNI類型與C/C++類型的相互轉換。
    const char * c_str = env->GetStringUTFChars(j_str, nullptr);
    LOGI("original j_str = %s", c_str);
    env->ReleaseStringUTFChars(j_str, c_str);

    jstring result = env->NewStringUTF("Hello");
    return result;
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_reflect_MainActivity_staticStringFromJNI(
        JNIEnv *env,
        jclass clazz) {
    // 實例方法第二個參數是jobject,static方法第二個參數是jclass
    std::string hello = "staticStringFromJNI";
    return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_reflect_MainActivity_invokeTestFromJNI(
        JNIEnv *env,
        jclass clazz) {
    // so中獲取class,需要用/分隔
    jclass testClass = env->FindClass("com/example/reflect/Test");
    // so中獲取field
    jfieldID publicStaticField = env->GetStaticFieldID(testClass, "publicStaticField", "Ljava/lang/String;");
    jstring publicStaticField_value = (jstring) env->GetStaticObjectField(testClass, publicStaticField);

    // 將字符串轉換為C才能列印
    const char* value_publicStaticField = env->GetStringUTFChars(publicStaticField_value, nullptr);
    LOGI("value_publicStaticField: %s", value_publicStaticField);

    std::string str = "invokeTestFromJNI";
    return env->NewStringUTF(str.c_str());
}

3. IDA調試so基本思路:開發一個Frida反調試

完整項目在 3_AntiFrida.7z + 3_libnative-lib.so.anti_frida_patched + 3_libnative-lib.so.original

https://github.com/b-mueller/frida-detection-demohttps://github.com/qtfreet00/AntiFridahttps://bbs.pediy.com/thread-217482.htmhttps://developer.aliyun.com/article/71120https://bbs.pediy.com/thread-253868.htmhttps://github.com/WalterInSH/risk-management-note/blob/master/README.md

對於 Java層API、Native層API 都是可以被 hook 的,如果自定義函數調用 Syscall 就會增加難度,對於syscall也是可以hook的但是需要自己編譯內核列印內核的系統調用(來自高維的對抗 - 逆向TinyTool自製),Unicorn列印反調試(Unicorn 在 Android 的應用),Nethunter可以直接列印syscall

風控的關鍵在於檢測的項目夠多,深層的混淆以及vmp(龍捲風),只是靠自己修改字符串特徵來編譯 Xposed / Frida 試圖過風控是不現實的。

3.1 使用 native 手寫一個簡易的 anti-frida

/root/Codes/Reflect/app/src/main/java/com/example/reflect/MainActivity.java

package com.example.antifrida;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText("ANTI-FRIDA");

        antiFrida();
    }

    // adb logcat 報錯 read: unexpected EOF!,需要 設置 - 開發者模式 - 調試(日誌記錄器緩衝區大小) - 16M
    public native void antiFrida();
}

/root/Codes/Reflect/app/src/main/cpp/native-lib.cpp

#include <jni.h>
#include <string>
#include <stdio.h>
#include <jni.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <android/log.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>


#include <android/log.h>
#define TAG "JNI_LOG"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)

// https://github.com/b-mueller/frida-detection-demo
// https://github.com/qtfreet00/AntiFrida
void *detect_frida_loop(void *) {
    struct sockaddr_in sa;
    memset(&sa, 0, sizeof(sa));
    sa.sin_family = AF_INET;
    inet_aton("0.0.0.0", &(sa.sin_addr));
    int sock;
    int i;
    int ret;
    char res[7];
    while (true) {
        LOGI("enter detect_frida_loop");

        // TCP掃描需要開網絡權限
        // /root/Codes/AntiFrida/app/src/main/AndroidManifest.xml 添加 <uses-permission android:name="android.permission.INTERNET" />
        // 只檢測開啟在[8000, 9000)埠之間的Frida
        for(i = 8000; i < 9000; i++){
            sock = socket(AF_INET,SOCK_STREAM, 0);
            sa.sin_port = htons(i);
            LOGI("detect_frida_loop is scanning socket port %d", i);

            if (connect(sock , (struct sockaddr*)&sa , sizeof sa) != -1) {
                memset(res, 0 , 7);
                send(sock, "\x00", 1, NULL);
                send(sock, "AUTH\r\n", 6, NULL);
                // give it some time to answer
                usleep(500);
                if ((ret = recv(sock, res, 6, MSG_DONTWAIT)) != -1) {
                    if (strcmp(res, "REJECT") == 0) {
                        LOGI("VITAL: Frida was appeared in port %d", i);
                    } else {
                        LOGI("SAFE: Frida was not found");
                    }
                }
            }
            close(sock);
        }
    }
}

// 函數的返回值注意和聲明要一致!!!否則代碼自己會莫名其妙崩
extern "C" JNIEXPORT void JNICALL
Java_com_example_antifrida_MainActivity_antiFrida(
        JNIEnv *env,
        jobject thiz) {
    pthread_t t;
    pthread_create(&t,NULL, detect_frida_loop, (void*)NULL);
    LOGI("Java_com_example_antifrida_MainActivity_antiFrida start");
}

/root/Codes/AntiFrida/app/src/main/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.antifrida">

    <!-- TCP掃描需要開網絡權限 -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

3.2 IDA 動靜態調試

準備工作: 把IDA/dbgsrv下面的android_server64(android_server 32位手機) push 到 /data/local/tmp,添加執行權限後開啟

root@Nick:~# adb push Downloads/android_server64 /data/local/tmp

sailfish:/data/local/tmp # chmod +x android_server64
sailfish:/data/local/tmp # ./android_server64

開啟對應手機架構版本的IDA - Debugger - Attach - Remote ARMLinux/Android debugger - hostname寫入手機ip - OK - 選擇進程 - OK

3.2.1 動態調試

IDA 右側 Modules 選 libnative-lib.so(可以Ctrl + F過濾) - 雙擊 - 進入Module: libnative-lib.so 可以看到 detect_frida_loop 和 Java_com_example_antifrida_MainActivity_antiFrida - 下斷點調試

3.2.2 靜態調試

直接IDA打開so即可

可以右鍵 代碼 Synchronize with - Hex View / PC

3.2.2.1 so硬編碼修改找到REJECT(右鍵Synchronize with hex view, 到hex view 視圖修改硬編碼,右鍵edit,右鍵apply change) - 保存找到REJECT設置 Synchronize with hex view - IDA左上角工具欄Edit - Patch program - Change byte/words - 修改字符 - OK - Edit - Patch program - Apply patches to input files(額外勾選Create backup) OK

patch後的libnative-lib.so 的 md5 是 6a5615f7f6f717212c02164e82e58b9fpatch前的libnative-lib.so 的 md5 是 e8f3be9db5b6e55afeb19d37761d88b8

3.2.2.2 objection找modules路徑
root@Nick:~# objection -N -h 192.168.50.42 -p 8888 -g com.example.antifrida explore

com.example.antifrida on (google: 8.1.0) [net] # memory list modules

顯示:
libnative-lib.so                                 0x726de9f000  12288 (12.0 KiB)      /data/app/com.example.antifrida-SwKcSsHgcvghJstE1gnqZg==/lib/arm64/libnativ...

3.2.2.3 patch so
root@Nick:~/Downloads/# adb push 3_libnative-lib.so.anti_frida_patched /sdcard/Download/

sailfish:/ $ su

sailfish:/ # cd /data/app/com.example.antifrida-SwKcSsHgcvghJstE1gnqZg==/lib/arm64/

sailfish:/data/app/com.example.antifrida-SwKcSsHgcvghJstE1gnqZg==/lib/arm64/ # cp /sdcard/Download/3_libnative-lib.so.anti_frida_patched libnative-lib.so

127|sailfish:/data/app/com.example.antifrida-SwKcSsHgcvghJstE1gnqZg==/lib/arm64/ # chmod 755 3_libnative-lib.so.anti_frida_patched

對於加載的so需要做完整性校驗否則硬編碼就可以被修改

4. java native / 符號 / JNI(art) / libc hook

完整項目在 4_Reflect.7z + 3_AntiFrida.7z + 4_anti_frida.js + 4_reflect.js

https://www.52pojie.cn/thread-742703-1-1.htmlhttps://www.jianshu.com/p/5092d6d5caa3

ELF裡面的GOT段/PLT段找到當前的符號,連結到偏移的地址,然後拿到符號進行跳轉,ELF結構分節和區,裡面就有GOT/PLT區表。

Frida的native hook主要分為符號hook(PLT/GOT hook) 和 inline hook

4.1 IDA導入jni.h

複製出jni.h(~/Android/Sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/jni.h,不同的apk只需要導入一次jni.h即可)

導入 jni.h (左上角工具欄 File - Load file - Parse C header file - 打開)讓IDA完整反編譯出_JNIEnv *

導入會報錯, 刪除jni報錯對應的.h, 重新導入

如果編譯的函數參數不是_JNIEnv *,就右鍵修改一下  如果沒有用到函數的參數,那IDA反編譯不會顯示這個參數的

4.2 so研究方式

主要就是通過trace來觀察so層的行為

/root/Codes/Reflect/app/src/main/java/com/example/reflect/MainActivity.java

在onCreate添加了一下調用native函數的死循環用於後面的so層hook實驗

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());

        Log.i("native-lib", "getStringLengthFromJNI(\"hello\") = " + String.valueOf(getStringLengthFromJNI("hello")));
        Log.i("native-lib", "changeString2HelloFromJNI(\"Hello World!\") = " + changeString2HelloFromJNI("Hello World!"));
        Log.i("native-lib", "staticStringFromJNI() = " + MainActivity.staticStringFromJNI());

        // test reflect
        testClazz();
        testField();
        testMethod();

        // invoke class Test from JNI
        String retFromJNI = invokeTestFromJNI();
        Log.i("retFromJNI", retFromJNI);

        while (true) {
            try {
                Thread.sleep(2000);
                // Do some stuff
            } catch (Exception e) {
                e.getLocalizedMessage();
            }
            Log.i("while in onCreate", changeString2HelloFromJNI("string in Java"));
        }
    }

reflect.js

/*
 * frida -H 192.168.50.42:8888 com.example.reflect -l agent/reflect.js
 */


// 檢查 以module_name_pattern開頭 的 module(即so) 是否存在在當前apk
function if_module_existed(module_name_pattern) {
    var modules = Process.enumerateModules()
    var module = null
    var existed = false

    for (var i = 0; i < modules.length; i++) {
        module = modules[i]

        if (module.name.indexOf(module_name_pattern) != -1) {
            if (!existed) {
                console.log('search module_name_pattern[' + module_name_pattern + '] found:')
                console.log()
                existed = !existed
            }
            console.log('module.name = ' + module.name)
            console.log('module.base = ' + module.base)
            console.log()
        }
    }
    return existed
}


function get_symbol_offset() {
    var libnative_lib_so_name = "libnative-lib.so"
    var existed = if_module_existed(libnative_lib_so_name)
    if (existed) {
        console.log(libnative_lib_so_name, 'is existed!')
    } else {
        console.log(libnative_lib_so_name, 'is not existed!')
    }

    var libnative_lib_so_address  = Module.findBaseAddress(libnative_lib_so_name)
    console.log("libnative_lib_so_address =", libnative_lib_so_address)

    // 先找到module然後找到想要hook的符號
    if (libnative_lib_so_address) {
        var symbol_name = null
        var symbol_address = null

        var symbol_name = 'Java_com_example_reflect_MainActivity_changeString2HelloFromJNI'
        symbol_address = Module.findExportByName(libnative_lib_so_name, symbol_name)
        console.log(symbol_name + ' address = ' + symbol_address)
        console.log(symbol_name + ' offset =', '0x' + (symbol_address - libnative_lib_so_address).toString(16))
    }
}


// 需要定義async函數且frida使用v8 --runtime=v8
// async function sleep() {
//     await new Promise(r => setTimeout(r, 5000))
// }


// 符號hook
function symbol_hook() {
    // spawn 需要延遲執行否則libnative-lib.so還沒有加載, 使用 setTimeout 或者 代碼裡加sleep
    // TODO 問: 對於只執行一次的native函數,spawn的時候so還沒有加載怎麼hook?
    var libnative_lib_so_name = "libnative-lib.so"
    var existed = if_module_existed(libnative_lib_so_name)
    if (existed) {
        console.log(libnative_lib_so_name, 'is existed!')
    } else {
        console.log(libnative_lib_so_name, 'is not existed!')
    }

    var libnative_lib_so_address  = Module.findBaseAddress(libnative_lib_so_name)
    console.log("libnative_lib_so_address =", libnative_lib_so_address)

    // 先找到module然後找到想要hook的符號
    if (libnative_lib_so_address) {
        var symbol_name = null
        var symbol_address = null

        var symbol_name = 'Java_com_example_reflect_MainActivity_changeString2HelloFromJNI'
        symbol_address = Module.findExportByName(libnative_lib_so_name, symbol_name)
        console.log(symbol_name + ' address = ' + symbol_address)
        console.log(symbol_name + ' offset =', '0x' + (symbol_address - libnative_lib_so_address).toString(16))
    }

    Interceptor.attach(symbol_address, {
        onEnter: function(args) {
            // args 是 jstring

            // Java_com_example_reflect_MainActivity_changeString2HelloFromJNI 三個參數
            console.log(symbol_address, "raw args[0], args[1], args[2]:", args[0], args[1], args[2])

            // 列印方式1
            // 星球jbytearray
            // 可能需要使用32位機器,sailfish這邊是Error: access violation accessing 0x7133cabb8
            // var args2_address = args[2].readPointer()
            // console.log(hexdump(args2_address))

            // 列印方式2
            // 通過JNI接口列印jstring, 注意需要對應到frida的api, 不要直接使用java的
            // https://github.com/frida/frida-java-bridge/blob/062999b618451e5ac414d08d4f52bec05bd6adf6/lib/env.js
            console.log('args[2]:', Java.vm.getEnv().getStringUtfChars(args[2], null).readCString())
        },
        onLeave: function(ret) {
            // ret 是 jstring
            console.log("raw ret:", ret)
            console.log('ret:', Java.vm.getEnv().getStringUtfChars(ret, null).readCString())
            var new_ret = Java.vm.getEnv().newStringUtf("new_ret from " + symbol_name)

            // 替換返回值
            ret.replace(ptr(new_ret))
        }
    })
}


// https://github.com/chame1eon/jnitrace
function hook_libart_so() {
    var libart_so_name = "libart.so"
    var existed = if_module_existed(libart_so_name)
    if (existed) {
        console.log(libart_so_name, 'is existed!')
    } else {
        console.log(libart_so_name, 'is not existed!')
    }

    var libart_so_address  = Module.findBaseAddress(libart_so_name)
    console.log("libart_so_address =", libart_so_address)

    // 由於 name mangling 導致符號被混淆(name mangling也不是完全混淆,真實函數名還是包含在結果中的), 所以需要枚舉所有符號
    // 不要用 Process.findModuleByName(module_name).enumerateSymbols() 符號可能枚舉結果為空
    var symbols = Module.enumerateSymbolsSync(libart_so_name)
    var jni_function_name = 'GetStringUTFChars'
    var jni_function_address = null

    for (var i = 0; i < symbols.length; i++){
        var symbol = symbols[i]
        if((symbol.name.indexOf("CheckJNI") == -1) && (symbol.name.indexOf("JNI") >= 0)) {
            if (symbol.name.indexOf(jni_function_name) >= 0) {
                console.log('symbol.name =', symbol.name)
                console.log('symbol.address =', symbol.address)
                jni_function_address = symbol.address
            }
        }
    }
    console.log(jni_function_name + ' address =', jni_function_address)

    // JNI 的 GetStringUTFChars 竟然可以列印android的Log.i, 陳總建議是可以看native的log實現, 然而現在並看不懂
    Interceptor.attach(jni_function_address, {
        onEnter: function(args) {
            console.log(jni_function_name + " args[0] =", args[0])
            console.log(jni_function_name + " args[1] =", args[1])

            // args[0] 是 JNIEnv
            // JNIEnv 可以用 hexdump(args[0].readPointer()), jxxx參數不能用, 要調用JNI的方法列印
            console.log('hexdump args[0] =', hexdump(args[0].readPointer()))
            // args[1] 是 jstring, 不能用hexdump
            // console.log('hexdump args[1] =', hexdump(args[1].readPointer()))
            // 兩個接口 getEnv / tryGetEnv
            console.log('args[1] =', Java.vm.getEnv().getStringUtfChars(args[1]).readCString())
            // console.log('args[1] =', Java.vm.tryGetEnv().getStringUtfChars(args[1]).readCString())

            // 列印調用棧: https://frida.re/docs/javascript-api/
            var trace_back_log = Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n')
            console.log(jni_function_name + ' traceback:\n' + trace_back_log + '\n')
        },
        onLeave: function(ret) {
            // char * 可以直接列印 ptr(ret).readCString()
            console.log(jni_function_name + " ret =", ptr(ret).readCString())
        }
    })
}


function main() {
    // get_symbol_offset()

    // symbol_hook()

    hook_libart_so()
}


setImmediate(main)

anti_frida.js

/*
 * frida -H 192.168.50.42:8888 -f com.example.antifrida -l agent/anti_frida.js
 */

// 1. 對native函數的hook,和普通java函數一致
function remove_antiFrida() {
    // 避免logcat日誌過多無法看出來hook native函數是否生效,先清理一下logcat日誌
    // 清除logcat日誌: adb logcat -c

    var MainActivity = Java.use('com.example.antifrida.MainActivity')
    MainActivity.antiFrida.implementation = function() {
        console.log('enter remove_antiFrida')
        return
    }
}


// 檢查 以module_name_pattern開頭 的 module(即so) 是否存在在當前apk
function if_module_existed(module_name_pattern) {
    var modules = Process.enumerateModules()
    var module = null
    var existed = false

    for (var i = 0; i < modules.length; i++) {
        module = modules[i]

        if (module.name.indexOf(module_name_pattern) != -1) {
            if (!existed) {
                console.log('search module_name_pattern[' + module_name_pattern + '] found:')
                console.log()
                existed = !existed
            }
            console.log('module.name = ' + module.name)
            console.log('module.base = ' + module.base)
            console.log()
        }
    }
    return existed
}


function enumerate_modules_and_exports(module_name) {
    // 枚舉符號和導出符號
    // symbols > exports
    // 但enumerateSymbols可能搜索結果為空,並且as打包生成的so會出現有導出符號沒有符號的情況,不知道是做了什麼配置,日常請使用sync版本
    // var symbols = Process.findModuleByName(libnative_lib_so_name).enumerateSymbols()
    // var symbols = Module.enumerateSymbolsSync(libnative_lib_so_name)

    // var symbols = Process.findModuleByName(module_name).enumerateSymbols()
    var symbols = Module.enumerateSymbolsSync(module_name)
    var exports = Process.findModuleByName(module_name).enumerateExports()
}


// 2. 找到偏移
function get_symbol_offset() {
    // root@Nick:~# objection -N -h 192.168.50.42 -p 8888 -g com.example.antifrida explore
    // com.example.antifrida on (google: 8.1.0) [net] # memory list modules

    // 找到:
    // libnative-lib.so                                 0x72b6e6c000  12288 (12.0 KiB)      /data/app/com.example.antifrida-cUCV8773Ge8uJPawmLlRPw==/lib/arm64/libnativ...

    var libnative_lib_so_name = "libnative-lib.so"
    var existed = if_module_existed('libnative')
    if (existed) {
        console.log(libnative_lib_so_name, 'is existed!')
    } else {
        console.log(libnative_lib_so_name, 'is not existed!')
    }

    // 找到基址, app重啟後基址和導出函數地址是會變化的, 但偏移量是固定的
    var libnative_lib_so_address  = Module.findBaseAddress(libnative_lib_so_name)
    console.log("libnative_lib_so_address =", libnative_lib_so_address)

    // 先找到module然後找到想要hook的符號
    if (libnative_lib_so_address) {
        // 由於namemagling導致符號被混淆, detect_frida_loop 的導出符號是 _Z17detect_frida_loopPv
        // extern "C" JNIEXPORT void JNICALL Java_com_example_antifrida_MainActivity_antiFrida
        var symbol_name = null
        var symbol_address = null

        var symbol_name = 'Java_com_example_antifrida_MainActivity_antiFrida'
        symbol_address = Module.findExportByName(libnative_lib_so_name, symbol_name)
        console.log(symbol_name + ' address = ' + symbol_address)
        // 偏移量是固定的, 和Frida的IDA-View按空格顯示的偏移一致
        console.log(symbol_name + ' offset =', '0x' + (symbol_address - libnative_lib_so_address).toString(16))

        symbol_name = '_Z17detect_frida_loopPv'
        symbol_address = Module.findExportByName(libnative_lib_so_name, symbol_name)
        console.log(symbol_name + ' address = ' + symbol_address)
        console.log(symbol_name + ' offset =', '0x' + (symbol_address - libnative_lib_so_address).toString(16))
    }
}


function hook_libc_so() {
    var libc_so_name = "libc.so"
    var existed = if_module_existed(libc_so_name)
    if (existed) {
        console.log(libc_so_name, 'is existed!')
    } else {
        console.log(libc_so_name, 'is not existed!')
    }

    var libc_so_address  = Module.findBaseAddress(libc_so_name)
    console.log("libc_so_address =", libc_so_address)

    var libc_function_name = 'pthread_create'
    var libc_function_address = null

    var symbols = null

    symbols = Module.enumerateSymbolsSync(libc_so_name)

    // C函數名字該是什麼就是什麼
    // objection: memory list exports libc.so --json libc_so.json
    for (var i = 0; i < symbols.length; i++) {
        var symbol = symbols[i]
        var name = symbol.name
        var address = symbol.address
        // 使用android native中的方法名
        if ((name.indexOf(libc_function_name) >= 0) && (name.indexOf(".cpp") == -1)) {
            console.log('name:', name)
            console.log('address:', address)

            libc_function_address = address
        }
    }

    Interceptor.attach(libc_function_address, {
        onEnter: function(args) {
            console.log(libc_function_name, 'arg[0] =', args[0])
            console.log(libc_function_name, 'arg[1] =', args[1])
            console.log(libc_function_name, 'arg[2] =', args[2])
            console.log(libc_function_name, 'arg[3] =', args[3])
        },
        onLeave: function(ret) {
            console.log('ret =', ret)
        }
    })
}


function main() {
    // Java.perform(remove_antiFrida)

    // get_symbol_offset()

    hook_libc_so()
}


setImmediate(main)

5. JNI_Onload / 主動調用 / inline_hook

完整項目在 5_Reflect.7z + 5_AntiFrida.7z + + 5_anti_frida.js + 5_reflect.js

https://github.com/android/ndk-samples.gitAndroid逆向新手答疑解惑篇——JNI與動態註冊https://bbs.pediy.com/thread-224672.htmhttps://www.jianshu.com/p/b71aeb4ed13d5.1 動態註冊靜態註冊: Java_com_example_demo_MainActivity_stringFromJNI5.1.1 JNI_OnLoad動態註冊

/root/Codes/Reflect/app/src/main/java/com/example/antifrida/MainActivity.java 修改onCreate 並添加 native方法 dynamicallyRegisteredStringFromJNI


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());

        Log.i("native-lib", "getStringLengthFromJNI(\"hello\") = " + String.valueOf(getStringLengthFromJNI("hello")));
        Log.i("native-lib", "changeString2HelloFromJNI(\"Hello World!\") = " + changeString2HelloFromJNI("Hello World!"));
        Log.i("native-lib", "staticStringFromJNI() = " + MainActivity.staticStringFromJNI());

        // test reflect
        testClazz();
        testField();
        testMethod();

        // invoke class Test from JNI
        String retFromJNI = invokeTestFromJNI();
        Log.i("retFromJNI", retFromJNI);

        while (true) {
            try {
                Thread.sleep(2000);
                // Do some stuff
            } catch (Exception e) {
                e.getLocalizedMessage();
            }
            Log.i("while in onCreate", changeString2HelloFromJNI("string in Java"));
            Log.i("RegisterNatives", dynamicallyRegisteredStringFromJNI());
        }
    }

    // IDA中找不到動態註冊的dynamicallyRegisteredStringFromJNI
    public native String dynamicallyRegisteredStringFromJNI();

/root/Codes/Reflect/app/src/main/cpp/native-lib.cpp 添加下面兩個方法用於主動註冊

// 去除了extern "C", 這樣會被name mangling
JNIEXPORT jstring JNICALL
dynamicallyRegisteredString(
        JNIEnv *env,
        jobject thiz) {
    std::string str = "dynamicallyRegisteredStringFromJNI";
    return env->NewStringUTF(str.c_str());
}

// 在jni.h中定義但沒有實現, 類似Activity的onCreate
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv *env;
    // 通過JavaVM獲取JNIEnv
    vm->GetEnv((void**)&env, JNI_VERSION_1_6);
    JNINativeMethod methods[] = {
            // Java中native函數名, (參數)返回值, native函數
            // 當參數為引用類型的時候,參數類型的標識為"L包名", 其中包名的.(點)要換成"/"
            {"dynamicallyRegisteredStringFromJNI", "()Ljava/lang/String;", (void*)dynamicallyRegisteredString},
    };
    // 第一個參數: native函數所在的類,可通過FindClass獲取(將.換成/)
    // 第二個參數: 一個數組,其中包含註冊信息
    // 第三個參數: 數量
    env->RegisterNatives(env->FindClass("com/example/reflect/MainActivity"), methods, 1);
    return JNI_VERSION_1_6;
}

5.1.2 trace libart

應該花時間多看大佬們寫的代碼,先抄後理解

RegisterNatives / libart(JNI)https://github.com/lasting-yang/frida_hook_libart

注意hook時機,例如hook RegisterNatives就需要用spawn的方式啟動

5.1.3 frida反調試https://bbs.pediy.com/thread-255653.htm5.1.4 主動調用native函數

Interceptor.replace

reflect.js

// https://frida.re/docs/javascript-api/
// Interceptor.replace
function native_initiatively_invoke() {
    var module_name = 'libnative-lib.so'
    var function_name = 'Java_com_example_reflect_MainActivity_changeString2HelloFromJNI'
    var Java_com_example_reflect_MainActivity_changeString2HelloFromJNI_address = Module.getExportByName(module_name, function_name)
    var Java_com_example_reflect_MainActivity_changeString2HelloFromJNI = new NativeFunction(Java_com_example_reflect_MainActivity_changeString2HelloFromJNI_address, 'pointer', ['pointer', 'pointer', 'pointer'])

    // Interceptor.attach只能hook和修改返回值, Interceptor.replace可以主動調用
    Interceptor.replace(Java_com_example_reflect_MainActivity_changeString2HelloFromJNI_address, new NativeCallback(function(env, thiz, j_str) {
        var str = Java.vm.getEnv().getStringUtfChars(j_str).readCString()
        console.log('j_str =', str)

        // 主動調用
        // var ret = Java_com_example_reflect_MainActivity_changeString2HelloFromJNI(env, thiz, j_str)
        // 替換參數的主動調用
        // 創建一個 jstring: Java.vm.getEnv().newStringUtf(""), 創建一個 char*: Memory.allocUtf8String("")
        var new_j_str = Java.vm.getEnv().newStringUtf("HelloHello")
        var ret = Java_com_example_reflect_MainActivity_changeString2HelloFromJNI(env, thiz, new_j_str)

        // 但這裡也沒有做到完全的主動調用, replace是類似於Java.choose, native new參數解決就是真正的主動調用了
        return ret
    }, 'pointer', ['pointer', 'pointer', 'pointer']))
}

anti_frida.js

function anti_anti_frida() {
    var libnative_lib_so_name = "libnative-lib.so"
    var existed = if_module_existed(libnative_lib_so_name)
    if (existed) {
        console.log(libnative_lib_so_name, 'is existed!')
    } else {
        console.log(libnative_lib_so_name, 'is not existed!')
    }

    var libnative_lib_so_address  = Module.findBaseAddress(libnative_lib_so_name)
    console.log("libnative_lib_so_address =", libnative_lib_so_address)

    var detect_frida_loop_name = 'detect_frida_loop'
    var detect_frida_loop_address = null

    var symbols = null

    // 坑坑坑!!!
    // 找自己用as寫的native函數要enumerateExportsSync, 不能enumerateSymbolsSync
    symbols = Module.enumerateExportsSync(libnative_lib_so_name)

    for (var i = 0; i < symbols.length; i++) {
        var symbol = symbols[i]
        var name = symbol.name
        var address = symbol.address
        if (name.indexOf(detect_frida_loop_name) >= 0) {
            console.log('name:', name)
            console.log('address:', address)

            detect_frida_loop_address = address
        }
    }

    // detect_frida_loop地址如下, 可以利用後三位地址偏移特徵進行過濾
    // 0x72b6f34a7c
    // 0x72b6e97a7c


    var libc_so_name = "libc.so"
    var existed = if_module_existed(libc_so_name)
    if (existed) {
        console.log(libc_so_name, 'is existed!')
    } else {
        console.log(libc_so_name, 'is not existed!')
    }

    var libc_so_address  = Module.findBaseAddress(libc_so_name)
    console.log("libc_so_address =", libc_so_address)

    var libc_function_name = 'pthread_create'
    var libc_function_address = null

    var symbols = null

    symbols = Module.enumerateSymbolsSync(libc_so_name)

    for (var i = 0; i < symbols.length; i++) {
        var symbol = symbols[i]
        var name = symbol.name
        var address = symbol.address
        // 使用android native中的方法名
        if ((name.indexOf(libc_function_name) >= 0) && (name.indexOf(".cpp") == -1)) {
            console.log('name:', name)
            console.log('address:', address)

            libc_function_address = address
        }
    }

    // c的大函數的signature都可以直接搜到
    var pthread_create = new NativeFunction(libc_function_address, 'int', ['pointer', 'pointer', 'pointer', 'pointer'])

    Interceptor.replace(libc_function_address, new NativeCallback(function(arg0, arg1, arg2, arg3) {
        var ret = null
        if (String(arg2).endsWith('a7c')) {
            // 這裡不能傳NULL,會導致報錯的(NULL就是空指針但為什麼會導致報錯呢),是因為NULL的address是0的原因嗎? 可以傳一個枚舉出來的合理的函數地址, 如何傳一個NULL pointer呢?
            // ret = pthread_create(arg0, arg1, NULL, arg3)
            ret = 0
            console.log(detect_frida_loop_name, 'is passed')
        } else {
            ret = pthread_create(arg0, arg1, arg2, arg3)
            console.log('normal pthread_create')
        }
        return ret
    }, 'int', ['pointer', 'pointer', 'pointer', 'pointer']))
}

5.1.5 inline hookIDA(IDA View)判斷是ARM指令還是thumb指令Options - General - Number of opcodes byte 修改為8(或4)如果地址顯示是2個數字(可能需要按一下空格)就是thumb,地址需要+1,4個數字就不需要+1
function inline_hook() {
var libnative_lib_so_name = "libnative-lib.so"
var existed = if_module_existed(libnative_lib_so_name)
if (existed) {
console.log(libnative_lib_so_name, 'is existed!')
} else {
console.log(libnative_lib_so_name, 'is not existed!')
}

var libnative_lib_so_address = Module.findBaseAddress(libnative_lib_so_name)
console.log("libnative_lib_so_address =", libnative_lib_so_address)

// 先找到module然後找到想要hook的符號
if (libnative_lib_so_address) {
var offset = 0xFC28
var FC28_address = libnative_lib_so_address.add(offset)


Interceptor.attach(FC28_address, {
onEnter: function(args) {
// hook地址沒有參數,一般只能列印寄存器
// 因為是64位所以是x
// IDA找到dynamicallyRegisteredString,找偏移末尾是C28的前面一點下斷點,可以發現斷點到C28時候寄存器的值和inline_hook是一致的
console.log('this.context.PC =', this.context.PC)
console.log('this.context.x1 =', this.context.x1)
console.log('this.context.x5 =', this.context.x5)
console.log('this.context.x10 =', this.context.x10)
},
onLeave: function(ret) {
console.log('ret =', ret)
}
})
}
}

6. Frida hook native App實戰

完整項目在 6_xman.js

如果so中的函數符號被strip掉的話IDA是搜不到的

6.1 反調試https://gtoad.github.io/2017/06/25/Android-Anti-Debug/https://github.com/gnaixx/anti-debug6.2 xman用yang神的hook_RegisterNatives看一下找到偏移對應的native函數偏移,IDA直接跳轉到對應函數(IDA可以嘗試修改函數第一個參數類型為JNIEnv *)看native函數來猜一下整個流程,然後來trace驗證一下hook strcmp過驗證 / 兩種寫入文件過驗證
function hook_MyApp() {
var MyApp = Java.use('com.gdufs.xman.MyApp')
MyApp.m.value = 1

var Toast = Java.use('android.widget.Toast')
Toast.makeText.overload('android.content.Context', 'java.lang.CharSequence', 'int').implementation = function(arg0, arg1, arg2) {
console.log('arg2:', arg1)
var ret = this.makeText(arg0, arg1, arg2)
return ret
}
}


function if_module_existed(module_name_pattern) {
var modules = Process.enumerateModules()
var module = null
var existed = false

for (var i = 0; i < modules.length; i++) {
module = modules[i]

if (module.name.indexOf(module_name_pattern) != -1) {
if (!existed) {
console.log('search module_name_pattern[' + module_name_pattern + '] found:')
console.log()
existed = !existed
}
console.log('module.name = ' + module.name)
console.log('module.base = ' + module.base)
console.log()
}
}
return existed
}


function hook_libc_so() {
var libc_so_name = "libc.so"
var existed = if_module_existed(libc_so_name)
if (existed) {
console.log(libc_so_name, 'is existed!')
} else {
console.log(libc_so_name, 'is not existed!')
}

var libc_so_address = Module.findBaseAddress(libc_so_name)
console.log("libc_so_address =", libc_so_address)

var libc_function_name = 'strcmp'
var libc_function_address = null

var symbols = null

symbols = Module.enumerateSymbolsSync(libc_so_name)

for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i]
var name = symbol.name
var address = symbol.address
if ((name.indexOf(libc_function_name) >= 0) && (name.indexOf(".cpp") == -1)) {
console.log('name:', name)
console.log('address:', address)

libc_function_address = address
}
}

var libc_function = new NativeFunction(libc_function_address, 'int', ['pointer', 'pointer'])


Interceptor.replace(libc_function_address, new NativeCallback(function(s1, s2) {
var char_star_s1 = ptr(s1).readCString()
var char_star_s2 = ptr(s2).readCString()

if (char_star_s2 == 'EoPAoY62@ElRD') {
console.log('char_star_s1 =', char_star_s1)
console.log('char_star_s2 =', char_star_s2)
return 0
}

var ret = libc_function(s1, s2)
return ret
}, 'int', ['pointer', 'pointer']))
}


function frida_write_reg(){
var file = new File("/sdcard/reg.dat","w+")
file.write("EoPAoY62@ElRD")
file.flush()
file.close()

console.log('frida_write_reg')
}


// 主動調用so函數
function hook_libc_so_write_reg(){
var fopen_address = Module.findExportByName("libc.so", "fopen")
var fputs_address = Module.findExportByName("libc.so", "fputs")
var fclose_address = Module.findExportByName("libc.so", "fclose")
console.log('fopen_address =', fopen_address)
console.log('fputs_address =', fputs_address)
console.log('fclose_address =', fclose_address)

var fopen = new NativeFunction(fopen_address, "pointer", ["pointer", "pointer"])
var fputs = new NativeFunction(fputs_address, "int", ["pointer", "pointer"])
var fclose = new NativeFunction(fclose_address, "int", ["pointer"])

// 創建C的字符串,不能直接傳入js的字符串
var filename = Memory.allocUtf8String("/sdcard/reg.dat")
var mode = Memory.allocUtf8String("w+")
var file = fopen(filename, mode)
var contents = Memory.allocUtf8String("EoPAoY62@ElRD")
var fputs_ret = fputs(contents, file)
fclose(file)

console.log('fputs_ret =', fputs_ret)
console.log('hook_libc_so_write_reg')
}


function main() {
// Java.perform(hook_MyApp)

// hook_libc_so()

// frida_write_reg()

hook_libc_so_write_reg()
}


setImmediate(main)

7. trace工具鏈7.1 trace三件套https://github.com/chame1eon/jnitracehttps://frida.re/docs/frida-trace/https://github.com/lasting-yang/frida_hook_libart8. init_array開發和逆向自動化

完整項目在 8_Reflect.7z + hook_linker.js

https://www.cnblogs.com/bingghost/p/6297325.htmlAndroid逆向新手答疑解惑篇——JNI與動態註冊https://bbs.pediy.com/thread-224672.htmlinker加載so(init_array) - hook linkerhttp://note.youdao.com/noteshare?id=38aa9a5ce7d03a9fd61a21d076eab219Android NDK中.init段和.init_array段函數的定義方式https://www.dllhook.com/post/213.html8.1 init & init_array

/root/Codes/Reflect/app/src/main/cpp/native-lib.cpp添加

// 先init後init_array
// 編譯生成後在.init段 [名字不可更改]
extern "C" void _init(void) {
    LOGI(".init");
}

// 編譯生成後在.init_array段 [名字可以更改]
__attribute__((__constructor__)) static void dot_init_array() {
    LOGI(".init_array");
}

.init可以直接在IDA Function window / Exports 搜到

shift + F7 / IDA View中Ctrl + s: 雙擊函數可以找到init_array中定義的函數內容

32位so通過call_function加載init_array中的方法的流程#2228-2236http://androidxref.com/6.0.1_r10/xref/bionic/linker/linker.cpp#2228https://www.cnblogs.com/bingghost/p/6297325.html

/root/Codes/Reflect/app/build.gradle: android - defaultConfig - externalNativeBuild添加

// 編譯32位庫
ndk {
    // Specifies the ABI configurations of your native
    // libraries Gradle should build and package with your APK.
    abiFilters "armeabi-v7a"
}

32位(資料多)動態調試init_array(我這邊沒有debug成功)linker 中找 call_function(其中的參數就是.init和.init_array)_dl__ZL13call_functionPKcPFvvES0 中的 CODE XREF: _dl__ZL13call_functionPKcPFvvES0+18↑j 後的 BLX R6(對應文章中的BLX R4)8.2 jnitrace 自吐 JNI_Onload & init_array
JNI_Onload:
jnitrace -m spawn -l libnative-lib.so com.example.reflect


獲得如下列印:
    417 ms [+] JNIEnv->RegisterNatives
    417 ms |- JNIEnv*          : 0xf39312a0
    417 ms |- jclass           : 0x85    { com/example/reflect/MainActivity }
    417 ms |- JNINativeMethod* : 0xffae15c8
    417 ms |:     0xd8d62495 - dynamicallyRegisteredStringFromJNI()Ljava/lang/String;
    417 ms |- jint             : 1
    417 ms |= jint             : 0

    417 ms Backtrace
    417 ms |-> 0xd8d625e1: _ZN7_JNIEnv15RegisterNativesEP7_jclassPK15JNINativeMethodi+0x2c (libnative-lib.so:0xd8d59000)


dynamicallyRegisteredStringFromJNI對應的so中函數的偏移即: 0xd8d62495 - 0xd8d59000 = 0x9495


init_array(感謝hanbing老師的腳本):
沒有支持arm64,可以在安裝app的時候 adb install --abi armeabi-v7a 強制讓app運行在32位模式

function LogPrint(log) {
    var theDate = new Date();
    var hour = theDate.getHours();
    var minute = theDate.getMinutes();
    var second = theDate.getSeconds();
    var mSecond = theDate.getMilliseconds()

    hour < 10 ? hour = "0" + hour : hour;
    minute < 10 ? minute = "0" + minute : minute;
    second < 10 ? second = "0" + second : second;
    mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond;

    var time = hour + ":" + minute + ":" + second + ":" + mSecond;
    var threadid = Process.getCurrentThreadId();
    console.log("[" + time + "]" + "->threadid:" + threadid + "--" + log);

}

function hooklinker() {
    var linkername = "linker";
    var call_function_addr = null;
    var arch = Process.arch;
    LogPrint("Process run in:" + arch);
    if (arch.endsWith("arm")) {
        linkername = "linker";
    } else {
        linkername = "linker64";
        LogPrint("arm64 is not supported yet!");
    }

    var symbols = Module.enumerateSymbolsSync(linkername);
    for (var i = 0; i < symbols.length; i++) {
        var symbol = symbols[i];
        //LogPrint(linkername + "->" + symbol.name + "---" + symbol.address);
        if (symbol.name.indexOf("__dl__ZL13call_functionPKcPFviPPcS2_ES0_") != -1) {
            call_function_addr = symbol.address;
            LogPrint("linker->" + symbol.name + "---" + symbol.address)

        }
    }

    if (call_function_addr != null) {
        var func_call_function = new NativeFunction(call_function_addr, 'void', ['pointer', 'pointer', 'pointer']);
        Interceptor.replace(new NativeFunction(call_function_addr,
            'void', ['pointer', 'pointer', 'pointer']), new NativeCallback(function (arg0, arg1, arg2) {
            var functiontype = null;
            var functionaddr = null;
            var sopath = null;
            if (arg0 != null) {
                functiontype = Memory.readCString(arg0);
            }
            if (arg1 != null) {
                functionaddr = arg1;

            }
            if (arg2 != null) {
                sopath = Memory.readCString(arg2);
            }
            var modulebaseaddr = Module.findBaseAddress(sopath);
            LogPrint("after load:" + sopath + "--start call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr);
            if (sopath.indexOf('libnative-lib.so') >= 0 && functiontype == "DT_INIT") {
                LogPrint("after load:" + sopath + "--ignore call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr);

            } else {
                func_call_function(arg0, arg1, arg2);
                LogPrint("after load:" + sopath + "--end call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr);

            }

        }, 'void', ['pointer', 'pointer', 'pointer']));
    }


}

setImmediate(hooklinker)

8.3 frida-trace 對 任意函數的 trace
# -f 啟動有時候so沒加載會hook不到
frida-trace -H 192.168.50.42:8888 -I libnative-lib.so com.example.reflect

# 基於單個函數地址的hook
frida-trace -H 192.168.50.42:8888 -a libnative-lib.so\!0x9495 com.example.reflect

# 幾種trace工具的文檔和源碼去了解一下

9. C hook & CModule

完整項目在 9_libnative-lib.so + 9_invoke_so.js + 9_cmodule.js + 9_explore.js + 9_explore.c

9.1 參數列印和構造jstring是沒法直接列印的,java vm中只能轉化為char*後列印,和native編程的流程一致jni的基本類型,要通過調用jni相關api轉化為c++對象才能列印和調用(參數構造可以java.vm.xx/直接獲取hook的參數進行傳遞),因為Interceptor(相當於在libart.so中執行)沒有在java.perform裡,所以不能使用java的方法9.2 Frida調用C++編寫的so

invoke_so.js

/*
 * frida -H 192.168.50.42:8888 -f com.android.settings -l agent/invoke_so.js --no-pause
 */
function invoke_so() {
    /**
     * https://github.com/lasting-yang/frida_hook_libart
     * 導入AntiFrida的libnative-lib.so在settings中執行
     * root@Nick:~# adb push Downloads/libnative-lib.so /data/local/tmp
     * root@Nick:~# adb shell su -c "cp /data/local/tmp/libnative-lib.so /data/app/libnative-lib.so"
     * root@Nick:~# adb shell su -c "chown 1000.1000 /data/app/libnative-lib.so"
     * root@Nick:~# adb shell su -c "chmod 777 /data/app/libnative-lib.so"
     * root@Nick:~# adb shell su -c "ls -al /data/app/libnative-lib.so"
     */

    var module_libnative_lib_so = Module.load("/data/app/libnative-lib.so")
    var export_function_name = "_Z17detect_frida_loopPv"
    var export_function_address = module_libnative_lib_so.findExportByName(export_function_name)
    console.log('export_function_address =', export_function_address)

    var detect_frida_loop = new NativeFunction(export_function_address, 'pointer', ['pointer'])
    var void_star = Memory.allocUtf8String("hello")
    detect_frida_loop(void_star)
    // adb logcat 可以看到開始Frida埠檢測
}


function main() {
    invoke_so()
}


setImmediate(main)

9.3 CModulehttps://frida.re/news/2019/09/18/frida-12-7-released/

不依賴架構並且不用編譯為so直接可以將C代碼注入

root@Nick:~# frida -H 192.168.50.42:8888 com.android.settings
[Remote::com.android.settings]-> var cm = new CModule('int add(int a, int b) { return a + b; }')
[Remote::com.android.settings]-> cm
{
    "add": "0x779d1d5000"
}
[Remote::com.android.settings]-> var add = new NativeFunction(cm.add, 'int', ['int', 'int'])
[Remote::com.android.settings]-> add(1, 2)
3

cmodule.js

/**
 * 1.
 * frida -H 192.168.50.42:8888 com.android.settings -l agent/cmodule.js --runtime=v8
 * 
 * 2.
 * root@Nick:~/Downloads# ./frida-server-12.8.0-linux-x86_64
 * root@Nick:~# mousepad cmodule.md
 */
function open_1() {
    // 無法保存文件內容了!
    const m = new CModule(`
    #include <gum/guminterceptor.h>
    
    #define EPERM 1
    
    int
    open (const char * path,
          int oflag,
          ...)
    {
      GumInvocationContext * ic;
    
      ic = gum_interceptor_get_current_invocation ();
      ic->system_error = EPERM;
    
      return -1;
    }
    `)
    
    const openImpl = Module.getExportByName(null, 'open')
    console.log('openImpl = ', openImpl)
    
    Interceptor.replace(openImpl, m.open)
    
}


function open_2() {
    const openImpl = Module.getExportByName(null, 'open')

    Interceptor.attach(openImpl, new CModule(`
      #include <gum/guminterceptor.h>
      #include <stdio.h>
    
      void
      onEnter (GumInvocationContext * ic)
      {
        const char * path;
    
        path = gum_invocation_context_get_nth_argument (ic, 0);
    
        printf ("open() path=\\"%s\\"\\n", path);
      }
    
      void
      onLeave (GumInvocationContext * ic)
      {
        int fd;
    
        fd = (int) gum_invocation_context_get_return_value (ic);
    
        printf ("=> fd=%d\\n", fd);
      }
    `))
}


function open_3() {
    const openImpl = Module.getExportByName(null, 'open')

    Interceptor.attach(openImpl, new CModule(`
            #include <gum/guminterceptor.h>

            extern void onMessage (const gchar * message);

            static void log (const gchar * format, ...);

            void
            onEnter (GumInvocationContext * ic)
            {
                const char * path;

                path = gum_invocation_context_get_nth_argument (ic, 0);

                log ("open() path=\\"%s\\"", path);
            }

            void
            onLeave (GumInvocationContext * ic)
            {
                int fd;

                fd = (int) gum_invocation_context_get_return_value (ic);

                log ("=> fd=%d", fd);
            }

            static void
            log (const gchar * format,
                ...)
            {
                gchar * message;
                va_list args;

                va_start (args, format);
                message = g_strdup_vprintf (format, args);
                va_end (args);

                onMessage (message);

                g_free (message);
            }
            `,
            {
                onMessage: new NativeCallback(messagePtr => {
                    const message = messagePtr.readUtf8String()
                    console.log('onMessage:', message)
                }, 'void', ['pointer'])
            }
        )
    )
}


function open_4() {
    const calls = Memory.alloc(4)

    const openImpl = Module.getExportByName(null, 'open')

    Interceptor.attach(
        openImpl,
        new CModule(`
                #include <gum/guminterceptor.h>

                extern volatile gint calls;

                void
                onEnter (GumInvocationContext * ic)
                {
                    g_atomic_int_add (&calls, 1);
                }
            `,
            { calls }
        )
    )

    setInterval(() => {
        console.log('Calls so far:', calls.readInt())
    }, 1000)
}


function main() {
    // open_1()

    // open_2()

    // open_3()

    open_4()
}

setImmediate(main)

9.4 Frida C + JS 混合編程https://frida.re/docs/javascript-api/

root@Nick:~/Codes/github/others/frida-agent-example# frida -p 0 -l agent/explore.js -C agent/explore.c

explore.js

console.log('Hello from Javascript')

var counter = Memory.alloc(4)
var bump = null
cs.counter = counter

rpc.exports.init = function () {
    bump = new NativeFunction(cm.bump, 'void', ['int'])
}

explore.c

extern int counter;

void init(void) {
    frida_log("Hello from C");
}

void bump(int n) {
    counter += n;
}

關於逆向學習的幾個觀點學會hook大法之後trace是幾乎不需要手動調的

QA:有時候-f so還沒有加載hook不到,可以延遲hook嗎?(但不能attach,有時候attach已經錯過調用時機了)

相關焦點

  • 詳解Hook框架frida,讓你在逆向工作中效率成倍提升!
    一、frida簡介frida是一款基於python + javascript 的hook框架,可運行在androidioslinuxwinosx等各平臺,主要使用動態二進位插樁技術。本期「安仔課堂」,ISEC實驗室的彭老師為大家詳解frida,認真讀完這篇文章會讓你在逆向工作中效率成倍提升哦!
  • frida學習筆記3 之hook so中的方法
    hook so 常用工具SubstrateCydia-需rootfrida--需rootVA系列-非root(VA、VXP、SandVXposed)frida方式hook材料準備heibaobao.apk
  • 安卓逆向——Frida hook java層
    各位愛好安卓逆向的大佬們早上好,今天呢小弟不才在這裡拙劣的給大家講解一下咱們frida hook原因是我們要找到hook的具體類以及方法名,參數等這些基本信息,這樣才能達到我們的hook目的。我們先輸入我們的hook方法命令我們代碼的一個腳本拖到我們的cmd窗口裡面,然後回車。
  • Frida全平臺使用
    在不同平臺下編寫hook代碼是一件很痛苦的事情,Windows下,必須要熟悉窗口、消息傳遞機制的,而在Linux下hook底層的消息還需要重新編寫內核模塊,在Android平臺要而Frida的出現,讓一切變得簡單了,只需要會Python,會JavaScript就能夠hook任何想hook的內容。Frida is so great!
  • Frida腳本教程
    使用下面的命令設置Python3是默認sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 150使用 sudo update-alternatives--config python命令切換到制定的版本
  • 當Frida來「敲」門
    0x2 fridafrida是平臺原生app的Greasemonkey,說的專業一點,就是一種動態插樁工具,可以插入一些代碼到原生app的內存空間去,(動態地監視和修改其行為),這些原生平臺可以是Win、Mac、Linux、Android
  • Frida進階-API
    同時簡單介紹下HOOK 系統函數的利器frida-trace。內存,內存還是內存。很多時候,我們需要hook系統函數read,查看文件描述符,緩存等信息。Frida-trace是一個動態跟蹤函數調用的工具,其強大之處在於能夠hook系統函數。
  • 花裡胡哨之突破安卓某APP低層加密
    今天給大家分享一下如何逆向分析Native層的加密,整個過程的思路值得大家借鑑,接下來小編給大家上才藝所需要的設備和環境設備:安卓手機(獲取root權限)反編譯反編譯之前先使用ApkScan-PKID 查一下是否有殼,養成好習慣,這個 app 沒有加殼,直接用 jadx 打開,全局搜索 「x-app-token」,只有一處,應該就在這裡了:
  • Frida之文件操作
    @[toc]基礎概念Linux下文件描述符一個Linux進程啟動後,會在內核空間創建一個PCB進程控制塊,PCB是一個進程的私有財產。舉個例子,hook Activity的onResume函數,當回調onResume函數的時候,先通過File操作向&39;這個文件中寫入&39;,最後調用系統的onResume回調。這裡只是舉例,實際情況可能需要保存一些重要的數據或者修改一些重要的數據。
  • ​【移動安全】一文入門Android逆向
    1、環境準備環境也分三六九等,一個好的環境能讓你節省大量時間和精力去投入到更有意義的事情中,所以Android移動安全第一步,就是配置一套無坑的環境,首先是硬體配置如下:Kali-linux-2019-4-vmware-amd641.1、VMware虛擬機
  • 打破重重阻礙,手淘如何突破native crash提升穩定性?
    (1)難點一:crash堆棧目前絕大部分只有系統so,缺乏問題關聯的業務模塊so,定位問題難度大,可以看一下如下的一個native crash堆棧,堆棧中的so都是系統的,無法直接定位業務問題。Native治理——Native Finder整體技術方案Native Finder是利用elf hook的原理,對malloc、free、realloc、alloc、mmap、munmap 等操作進行 hook,在自己的 hook
  • 安卓逆向學習之詳解Hook含義及作用
    hook英文單詞意為「鉤子」,很形象的,它的作用就是去調用或者篡改APP代碼。舉一個簡單的例子。消消樂風靡的時候大受歡迎,相信男女老幼都對這款遊戲不陌生。開心消消樂這個小遊戲,相信各位小夥伴都玩過。而在進行遊戲時,闖關是很有成就感。遊戲大神們自然是輕輕鬆鬆「unbelievable」、「amazing」!
  • Frida API進階
    function frida_Module_address() { Java.perform(function () { console.log(&34;,Module.getExportByName(&39;, &39;)); console.log(&34;,Module.findExportByName(&39;, &39;));
  • Frida使用之資料庫
    SQLite是一個軟體庫,實現了自給自足的、無伺服器的、零配置的、事務性的 SQL 資料庫引擎。SQLite 是在世界上最廣泛部署的 SQL 資料庫引擎。SQLite 原始碼不受版權限制。改變表名 - ALTER TABLE 舊錶名 RENAME TO 新表名;增加一列 - ALTER TABLE 表名 ADD COLUMN 列名 數據類型 限定符select * from sqlite_master where type=&34;; //顯示所有表的結構select * from sqlite_master where type=&34; and name=&34;; //顯示某個表的結構
  • 閒魚對Flutter-Native混合工程解耦的探索
    閒魚集成了集團眾多中間件,很多功能無法通過flutter直接調用,需使用各種channel到native去調用對應功能。混合棧編程中歷史包袱導致IOS/Android雙端返回給Flutter側的數據可能存在不一致性;集成模塊開發效率相比模塊開發較低,單模塊頁面測試性能數據無法展開;方案一:
  • Frida常用API
    instance.getName()); // bluetoothDeviceInfo(instance); }, onComplete: function() { console.log(&34;);} }); }); });通過下面的命令運行程序frida