Frida 学习笔记

Posted by API Caller on March 30, 2019

Frida 真的是火到爆炸了, 学吧那就.

其实无论是 Frida 还是 Xposed 还是其它什么框架, 都只是工具, 更重要的是思路, 愿自己时刻牢记能跑就行.

本文环境

  • Windows 10 x64
  • Ubuntu 18.04
  • Frida
  • VSCode + Python 插件
  • Nexus 5 (6.0.1)
  • Nexus 5x (8.0.1)
  • iPhone 6sp

Android

环境

Magisk Hide 会 ptrace zygote, 关掉!

略.

1
2
3
4
5
6
7
8
9
10
11
 # 最好用 mfe 代替
export PATH=~/Android/Sdk/platform-tools/:$PATH
adb push frida-server-12.4.8-android-arm64 /data/local/tmp/fserver
# adb shell su -c "mount -o rw,remount /system"
# adb shell su -c "mv /data/local/tmp/fserver /system/bin/fserver"
# adb shell su -c "chmod 777 /system/bin/fserver"
# adb shell su -c "mount -o ro,remount /system"

adb shell su -c "mv /data/local/tmp/fserver /sbin/fserver"
adb shell su -c "chmod 777 /sbin/fserver"
adb shell su -c "fserver --help"

务必用 virtualenv, 指定版本安装

  • pip 安装 (virtualenv)
    1
    2
    3
    4
    5
    
    cd ~/frida
    virtualenv --no-site-packages frida-12.0.7 
    source ~/frida/frida-12.0.7/bin/activate
    pip3 install frida==12.0.7
    pip3 install frida-tools==1.1.0
    

    vscode 里添加

    1
    2
    3
    4
    5
    6
    
      "python.venvPath": "d:\\CODE\\Env\\Frida",
      "python.venvFolders": [
          "envs",
          ".pyenv",
          ".direnv"
      ],
    

    即可在 vscode 中选择 venv 中的解释器.

  • 验证一下 (不报错就行了)
    1
    2
    3
    
    # 验证 frida
    python
    >> import frida 
    
    1
    2
    
    # 验证 frida-tools
    frida-ps
    
  • 连上手机, 转发端口, 运行 frida-server
    1
    2
    3
    4
    
    # 杀进程
    adb shell su -c "pkill fserver" && adb shell su -c "pkill frida-helper-32" && adb shell su -c "pkill frida-helper-64" 
    
    adb forward tcp:27042 tcp:27042 && adb forward tcp:27043 tcp:27043 && adb shell su -c "fserver"
    

虚拟机堆栈

1
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

相当于 java 的

1
android.util.Log.getStackTraceString(new java.lang.Throwable())

枚举所有类

1
2
3
4
5
6
7
8
9
    console.log("n[*] enumerating classes...");
    Java.enumerateLoadedClasses({
      onMatch: function(_className){
        console.log("[*] found instance of '"+_className+"'");
      },
      onComplete: function(){
        console.log("[*] class enuemration complete");
      }
    });

注入 Dex

  • 先用 sdk - build-tools 中的 dx 制作 dex, push 到手机里
  • 注入代码
    1
    2
    3
    4
    5
    6
    7
    
      var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
      var context = currentApplication.getApplicationContext();
      var pkgName = context.getPackageName();
      var dexPath = "/data/local/tmp/guava.dex";
      Java.openClassFile(dexPath).load();
      console.log("inject " + dexPath + " to " + pkgName + " successfully!")
      console.log(Java.use("com.google.common.collect.Maps")); // 
    

Non-ASCII

比如说

1
2
3
    int ֏(int x) {
        return x + 100;
    }

甚至有一些不可视, 所以可以先编码打印出来, 再用编码后的字符串去 hook.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    Java.perform(
        function x() {

            var targetClass = "com.example.hooktest.MainActivity";

            var hookCls = Java.use(targetClass);
            var methods = hookCls.class.getDeclaredMethods();

            for (var i in methods) {
                console.log(methods[i].toString());
                console.log(encodeURIComponent(methods[i].toString().replace(/^.*?\.([^\s\.\(\)]+)\(.*?$/, "$1")));
            }

            hookCls[decodeURIComponent("%D6%8F")]
                .implementation = function (x) {
                    console.log("original call: fun(" + x + ")");
                    var result = this[decodeURIComponent("%D6%8F")](900);
                    return result;
                }
        }
    )

TracerPid

1
2
3
4
5
6
7
8
9
10
11
12
    console.log("anti_fgets");
    var fgetsPtr = Module.findExportByName("libc.so", "fgets");
    var fgets = new NativeFunction(fgetsPtr, 'pointer', ['pointer', 'int', 'pointer']);
    Interceptor.replace(fgetsPtr, new NativeCallback(function (buffer, size, fp) {
        var retval = fgets(buffer, size, fp);
        var bufstr = Memory.readUtf8String(buffer);
        if (bufstr.indexOf("TracerPid:") > -1) {
            Memory.writeUtf8String(buffer, "TracerPid:\t0");
            // console.log("tracerpid replaced: " + Memory.readUtf8String(buffer));
        }
        return retval;
    }, 'pointer', ['pointer', 'int', 'pointer']));

System.loadLibrary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
            const System = Java.use('java.lang.System');
            const Runtime = Java.use('java.lang.Runtime');
            const VMStack = Java.use('dalvik.system.VMStack');

            System.loadLibrary.implementation = function (library: string) {
                try {
                    console.log('System.loadLibrary("' + library + '")');

                    var ret;
                    if (SDK_INT > 23) {
                        ret = Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), library);
                    } else {
                        ret = Runtime.getRuntime().loadLibrary(library, VMStack.getCallingClassLoader());
                    }

                    return ret;
                } catch (ex) {
                    console.log(ex);
                }
            };


            System.load.implementation = function (library: string) {
                try {
                    console.log('System.load("' + library + '")');
                    var ret;
                    if (SDK_INT > 23) {
                        ret = Runtime.getRuntime().load0(VMStack.getCallingClassLoader(), library);
                    } else {
                        ret = Runtime.getRuntime().load(library, VMStack.getCallingClassLoader());
                    }

                    return ret;
                } catch (ex) {
                    console.log(ex);
                }
            };

也可以直接 native 层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var mod_art = Process.findModuleByName("libart.so");
if (mod_art) {
    for (var exp of mod_art.enumerateExports()) {
        if (exp.name.indexOf("LoadNativeLibrary") != -1) {
            console.log(exp.name, exp.address);

            Interceptor.attach(exp.address, {
                onEnter: function (args) {
                    this.pathName = utils.readStdString(args[2]);
                    console.log("[*] [LoadNativeLibrary] in  pathName =", this.pathName);
                },
                onLeave: function (retval) {
                    console.log("[*] [LoadNativeLibrary] out pathName =", this.pathName);
                }
            });

            break;
        }
    }
}

var mod_dvm = Process.findModuleByName("libdvm.so");
if (mod_dvm) {
    for (var exp of mod_dvm.enumerateExports()) {
        if (exp.name.indexOf("dvmLoadNativeCode") != -1) {
            console.log(exp.name, exp.address);
            //    bool dvmLoadNativeCode(const char * pathName, void * classLoader, char ** detail);

            Interceptor.attach(exp.address, {
                onEnter: function (args) {
                    this.pathName = args[0].readUtf8String();
                    console.log("[*] [dvmLoadNativeCode] in  pathName =", this.pathName);
                },
                onLeave: function (retval) {
                    console.log("[*] [dvmLoadNativeCode] out pathName =", this.pathName);
                }
            });
            break;
        }
    }
}

SDK_INT

1
    const SDK_INT = Java.use('android.os.Build$VERSION').SDK_INT.value;

dump so

1
Process.enumerateModules();

然后

1
var fd = new File("/sdcard/Android/data/com.example/files/libxx.so","wb"); fd.write(new NativePointer(0x94300000).readByteArray(900368));fd.close(); 

asciicast

找 interface 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Java.enumerateLoadedClasses(
    {
        "onMatch": function (className) {
            if (className.indexOf("com.example.hooktest.") < 0) {
                return;
            }
            var hookCls = Java.use(className);
            var interFaces = hookCls.class.getGenericInterfaces();
            if (interFaces.length > 0) {
                console.log(className)
                for (var i in interFaces) {
                    console.log("\t", interFaces[i])
                }
                var methods = hookCls.class.getDeclaredMethods();
                for (var i in methods) {
                    console.log(methods[i].toString(), "\t", encodeURIComponent(methods[i].toString().replace(/^.*?\.([^\s\.\(\)]+)\(.*?$/, "$1")));
                }
            }
        },
        "onComplete": function () { }
    }
)

内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MainActivity extends AppCompatActivity {
    static public String TAG = "HookTest";

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


        
        compute(50, 30, new ComputeCallBack() {
            @Override
            public void onComputeEnd(int x, int y) {
                Log.e(TAG, "onComputeEnd:" + String.valueOf(x + y));
            }
        });

    }

    public interface ComputeCallBack {
        public void onComputeEnd(int x, int y);
    }

    public static void compute(int x, int y, ComputeCallBack callback) {
        callback.onComputeEnd(x, y);
    }

}

com.example.hooktest.MainActivity$1<init>() 的参数为 MainActivity.this.

1
Java.use("com.example.hooktest.MainActivity").compute(50, 50, Java.use("com.example.hooktest.MainActivity$1").$new(this));

打印 il2cpp 中返回的 c# string

内存布局中第三个四字节是长度, 第四个四字节开始存放 utf-16 字符串.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// public string DecryptString(string stringId); // RVA: 0x3D270 Offset: 0x3D270
function attach_DecryptString(){
    var func = Module.getBaseAddress("libil2cpp.so").add(0x3D270);
    console.log('func addr: ' + func);
    Interceptor.attach(func, {
        onEnter: function (args) {

        },
        onLeave: function (retval) {
            print_dotnet_string("onLeave", retval);
        }
    }
    );
}    
1
2
3
4
5
6
7
function print_dotnet_string(tag, dotnet_string) {
    console.log(JSON.stringify({
        tag: tag,
        len: dotnet_string.add(8).readU32(),
        data: dotnet_string.add(12).readUtf16String(-1)
    }))
}

读取 std::string

Frida and std::string

1
2
3
4
5
6
7
8
export function readStdString(str: any) {
    const isTiny = (str.readU8() & 1) === 0;
    if (isTiny) {
        return str.add(1).readUtf8String();
    }

    return str.add(2 * Process.pointerSize).readPointer().readUtf8String();
}

获取进程名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export function get_self_process_name(pid: any) {

    var ret = "";

    var fopen_ptr = Module.findExportByName('libc.so', 'fopen');
    var fgets_ptr = Module.findExportByName('libc.so', 'fgets');
    var fclose_ptr = Module.findExportByName('libc.so', 'fclose');
    if (fopen_ptr && fgets_ptr && fclose_ptr) {
        var fopen = new NativeFunction(fopen_ptr, 'pointer', ['pointer', 'pointer']);
        var fgets = new NativeFunction(fgets_ptr, 'pointer', ['pointer', 'int', 'pointer']);
        var fclose = new NativeFunction(fclose_ptr, 'int', ['pointer']);

        var filename = "/proc/" + (pid ? pid : "self") + "/cmdline";
        var f = fopen(Memory.allocUtf8String(filename), Memory.allocUtf8String("r"));
        if (f) {
            var len = 128;
            var buf = Memory.alloc(len);
            fgets(buf, len, f);

            var temp = buf.readCString();
            if (temp) {
                ret = temp;
            }
            fclose(f);
        }

    }

    return ret;
}

frida 破解示例

soul

有朋友说 soul 这个 app 有双向认证, 抓不了包, 那么拿 Frida 把 client certificate 给 dump 下来.

实际上 p12 证书文件 一般 就在 apk 文件夹里, 所以一般我们只需要 hook 拿一下密钥, 但这里只是我假装它没在.

  • 查资料, 随便查查就会知道一般来说 java 层加载 client certificate 进行双向认证都是用 java.security.KeyStore.load(), 那么 hook 它一般就可以拿到需要的证书文件以及密码.

  • 编写 hook.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
console.log("Script loaded successfully ");

 Java.perform(function () {



    const ByteArrayOutputStream = Java.use("java.io.ByteArrayOutputStream");
    const FileOutputStream = Java.use("java.io.FileOutputStream");
    const KeyStore = Java.use('java.security.KeyStore');
    const store = KeyStore.store.overload('java.io.OutputStream', '[C');

    java_security_KeyStore__load(function (instance, charArray) {
        if (instance.getType() === "PKCS12" && !dumped) {

            dumped = true;
            console.log("dumping...")
            try {
                // var s = ByteArrayOutputStream.$new();
                var s = FileOutputStream.$new(filesDir + "/test.cert");

                store.call(instance, s, charArray);

                // var b = s.toByteArray();

                // console.log(byteArrayToHex(b))

            } catch (e) {
                console.warn(e);
            }

        }
    })
}

function java_security_KeyStore__load(fn) {

    try {
        var KeyStore = Java.use('java.security.KeyStore')
        KeyStore['load']
            .overload('java.io.InputStream', '[C')
            .implementation = function (stream, charArray) {

                if (stream == null) {
                    this.load(stream, charArray);
                    return;
                }

                console.log("[KeyStore.load]");

                this.load(stream, charArray);

                if (fn) {
                    fn(this, charArray);
                }


                console.log("\t", stream.toString());
                console.log("\t", this.getType());
                console.log("\t", charArrayToAsciiHex(charArray))

            }
    } catch (e) {
        console.warn(e)
    }

}

function byteArrayToHex(bytes) {
    var hex = [];
    var data = [];
    for (var i = 0; i < bytes['length']; i++) {
        hex.push(('0' + (bytes[i] & 0xFF).toString(16)).slice(-2));
    }
    return hex.join(' ');
}

function charArrayToAsciiHex(charArray) {
    var hex = [];
    var data = [];
    for (var i = 0; i < charArray['length']; i++) {
        hex.push(('0' + (charArray[i].charCodeAt(0) & 0xFF).toString(16)).slice(-2));
        data.push(charArray[i]);
    }
    return hex.join(' ') + '\t' + data.join('');
}

触**闻

包名 com.to*****.*****tv, 全线 360 加固. 说是装了 justtrustme 还是抓不了包.

先用 frida 上工具类, hook KeyStore.load, 发现没经过, 基本排除 Java 层的双向认证.

接着直接遍历所有类, 发现可疑类 com.android.volley.toolbox.MyHTTPSTrustManager 以及它的方法 checkServerTrusted

开始 hook:

1
2
3
4
5
    Java.use('com.android.volley.toolbox.MyHTTPSTrustManager')['checkServerTrusted']
        .overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String')
        .implementation = function (arg1: any, arg2: string) {
            console.log("checkServerTrusted");
        }

可以抓到了.

下一题.

**

包名 com.nin***x.ncsea***new, 乐固壳, 说是双向认证.

既然确定是双向认证就好办, hook KeyStore.load, 确实打印出密码了.

仔细一看, 第一个参数的 InputStream 没打印出来. 也不知道为啥, 也懒得研究, 直接 toString, 发现是 AssetInputStream, 大概是读取资源文件.

那么两个思路:

  1. 直接静态遍历所有资源文件, 看看哪个文件是密钥的格式.
  2. hook AssetInputStream 的由来, 找到对应的文件名.

我选择了 2, 对着源码开始 hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 Java.use('android.content.res.AssetManager')['open']
     .overload('java.lang.String', 'int')
     .implementation = function (fileName, accessMode) {
         console.log("AssetManager.open", fileName);
         var ret = this.open(fileName, accessMode);
         console.log(ret.toString());
         return ret;
     }
 Java.use('android.content.res.AssetManager')['openNonAsset']
     .overload('int', 'java.lang.String', 'int')
     .implementation = function (cookie, fileName, accessMode) {
         console.log("AssetManager.openNonAsset", fileName);
         var ret = this.openNonAsset(cookie, fileName, accessMode);
         console.log(ret.toString());
         return ret;
     }

于是找到对应的文件, 导入抓包工具, 输入密码, 完毕.

下一题.

**投

包名 **p.********tou, 版本 1.5.1

flutter 的验证逻辑全都在 native 层, 底层 ssl 实现是 boringssl, 找 boringssl 验证的地方, 寻找偏移 hook 即可.

此处被 proxydroid 坑了, 换成 drony 即可.

1
2
3
4
5
6
    Interceptor.attach(flutter.base.add(0x377770 + 1), {
        onLeave: function (retval) {
            console.log("[*] [verify]", retval);
            retval.replace(ptr(1));
        }
    });

iOS

环境