app登录协议逆向分析

 

随*记app

版本 v13.2.21.0 

 

一.过frida检测

hook dlopen看加载了哪些so

function hook_dlopen(soName) {
    Interceptor.attach(Process.findModuleByName("libandroid.so").getExportByName("android_dlopen_ext"), {
        onEnter: function (args) {
            this.fileName = args[0].readCString()
            console.log(`android_dlopen_ext onEnter: ${this.fileName}`)
            
        }, onLeave: function(retval){
            console.log(`android_dlopen_ext onLeave: ${this.fileName}`)

        }
    })

    Interceptor.attach(Process.findModuleByName("libdl.so").getExportByName("dlopen"), {
        onEnter: function (args) {
            this.fileName = args[0].readCString()
            console.log(`dlopen onEnter: ${this.fileName}`)

        }, onLeave: function(retval){
            console.log(`dlopen onLeave: ${this.fileName}`)

        }
    })
}

hook_dlopen()

 

程序在dlopen enter libmsaoaidsec.so 之后,dlopen leave libmsaoaidsec.so之前终止,检测的操作应该就是在libmsaoaidsec.so中,并且是在init函数中进行的

因此需要hook  call_constructors 去做进一步的定位

要进行frida检测就需要创建线程,在hook call_constructors 后,进一步去hook pthread_create

let is_hook_pthread_create = false;
function hook_pthread_create(soName){
    if(is_hook_pthread_create) {
        return;
    }
    let module = Process.findModuleByName(soName);
    if (module) {
        Interceptor.attach(Module.findExportByName('libc.so','pthread_create'),{
            onEnter(args){
                // do something
                let func_addr = args[2]
                console.log(`The thread Called function offset is: ${func_addr.sub(module.base)}`)
            }
        })

        is_hook_pthread_create = true;
    }
}

let is_hook_call_constructors = false;
function hook_call_constructors(soName) {
    if(is_hook_call_constructors) {
        return;
    }
    let symbols = Process.findModuleByName("linker64").enumerateSymbols()
    let call_constructors = null;
    for (let i = 0; i < symbols.length; i++) {
        let symbol = symbols[i];
        // __dl__ZN6soinfo17call_constructorsEv
        if(symbol.name.indexOf("call_constructors") != -1){
            call_constructors = symbol.address;
        }
    }
    if (call_constructors) {
        Interceptor.attach(call_constructors, {
            onEnter: function (args) {
                // do something
                hook_pthread_create()
            }
        });

        is_hook_call_constructors = true;
    }
}

function hook_dlopen(soName) {
    Interceptor.attach(Process.findModuleByName("libandroid.so").getExportByName("android_dlopen_ext"), {
        onEnter: function (args) {
            this.fileName = args[0].readCString()
            console.log(`android_dlopen_ext onEnter: ${this.fileName}`)
            hook_call_constructors(soName)

        }, onLeave: function(retval){
            console.log(`android_dlopen_ext onLeave: ${this.fileName}`)

        }
    })

    Interceptor.attach(Process.findModuleByName("libdl.so").getExportByName("dlopen"), {
        onEnter: function (args) {
            this.fileName = args[0].readCString()
            console.log(`dlopen onEnter: ${this.fileName}`)
            hook_call_constructors(soName)

        }, onLeave: function(retval){
            console.log(`dlopen onLeave: ${this.fileName}`)

        }
    })
}

hook_dlopen("libmsaoaidsec.so")

 

 

 

init中创建的线程函数有三个,分别在偏移0x1c544,0x1b8d4,0x26e5c处,relpace这三个函数为空

 

function hook_dlopen(soName) {
    Interceptor.attach(Process.findModuleByName("libandroid.so").getExportByName("android_dlopen_ext"), {
        onEnter: function (args) {
            this.fileName = args[0].readCString()
            console.log(`android_dlopen_ext onEnter: ${this.fileName}`)
            //hook_call_constructors(soName)

        }, onLeave: function(retval){
            console.log(`android_dlopen_ext onLeave: ${this.fileName}`)
            pass_frida_check()

        }
    })

    Interceptor.attach(Process.findModuleByName("libdl.so").getExportByName("dlopen"), {
        onEnter: function (args) {
            this.fileName = args[0].readCString()
            console.log(`dlopen onEnter: ${this.fileName}`)
            //hook_call_constructors(soName)

        }, onLeave: function(retval){
            console.log(`dlopen onLeave: ${this.fileName}`)
            pass_frida_check()

        }
    })
}



let is_pass_frida_check = false;
function pass_frida_check() {
    if (is_pass_frida_check) {
        //console.log('Function already hooked, skipping...');
        return;
    }

    let secmodule = Process.findModuleByName("libmsaoaidsec.so");

    if (secmodule) {
        Interceptor.replace(secmodule.base.add(0x1c544), new NativeCallback(function () {
            console.log(`hook_sub_1c544 >>>>>>>>>>>>>>>>> replace`);
        }, 'void', []));
        Interceptor.replace(secmodule.base.add(0x1b8d4), new NativeCallback(function () {
            console.log(`hook_sub_1b8d4 >>>>>>>>>>>>>>>>> replace`);
        }, 'void', []));
        Interceptor.replace(secmodule.base.add(0x26e5c), new NativeCallback(function () {
            console.log(`hook_sub_26e5c >>>>>>>>>>>>>>>>> replace`);
        }, 'void', []));

        is_pass_frida_check = true; // Mark as hooked
    }
}

 

 

绕过检测了

 

二.抓包与关键函数定位

 

登录过程抓包

有两个我们感兴趣的键password和sign

 

java层自吐算法没有获得结果

hook libart中的jni函数

获得sign,在libme.so!_Z5getDgP7_JNIEnvP7_jclassP8_jobjecthhi+0x118中

[GetStringUTFChars] result:businessType: moduleType:base tag:Networker message:--> GET https://auth.feidee.net/v2/oauth2/authorize?encode_version=v2&password=0fd69633c48a5c2a095ee5b50daac234&grant_type=password&scope=accountbook%3Buser&username=18559707719 http/1.1
<-- 403  https://auth.feidee.net/v2/oauth2/authorize?encode_version=v2&password=0fd69633c48a5c2a095ee5b50daac234&grant_type=password&scope=accountbook%3Buser&username=18559707719 (4999ms, 91-byte body)
 extraData:null fullReport:false
 0x6f565486cc libmarsxlog.so!Java_com_tencent_mars_xlog_Xlog_logWrite2+0x120

获得password,在libmarsxlog.so!Java_com_tencent_mars_xlog_Xlog_logWrite2+0x120中

[GetStringUTFChars] result:businessType: moduleType:base tag:Networker message:--> GET https://auth.feidee.net/v2/oauth2/authorize?encode_version=v2&password=0fd69633c48a5c2a095ee5b50daac234&grant_type=password&scope=accountbook%3Buser&username=18559707719 http/1.1
<-- 403  https://auth.feidee.net/v2/oauth2/authorize?encode_version=v2&password=0fd69633c48a5c2a095ee5b50daac234&grant_type=password&scope=accountbook%3Buser&username=18559707719 (4999ms, 91-byte body)
 extraData:null fullReport:false
 0x6f565486cc libmarsxlog.so!Java_com_tencent_mars_xlog_Xlog_logWrite2+0x120

 

 定制系统trace

 

 sign字符串由com.mymoney.util.DGUtil.getDG方法返回

 

 password字符串由com.mymoney.util.AESEncryptUtil.aesEncrypt方法返回

 

hook验证

function hook_java() {
    Java.perform(function () {
        let AESEncryptUtil = Java.use("com.mymoney.util.AESEncryptUtil");
        AESEncryptUtil["aesEncrypt"].implementation = function (str, i2) {
            console.log(`AESEncryptUtil.aesEncrypt is called: str=${str}, i2=${i2}`);
            let result = this["aesEncrypt"](str, i2);
            console.log(`AESEncryptUtil.aesEncrypt result=${result}`);
            return result;
        };

        let DGUtil = Java.use("com.mymoney.util.DGUtil");
        DGUtil["getDG"].implementation = function (context, z, z2, i2) {
            console.log(`DGUtil.getDG is called: context=${context}, z=${z}, z2=${z2}, i2=${i2}`);
            let result = this["getDG"](context, z, z2, i2);
            console.log(`DGUtil.getDG result=${result}`);
            return result;
        };
    })
}

 

 

 

验证完毕

接下来确定这两个Native方法在哪个so中

对于动态注册的native方法,hook RegisterNatives,对于静态注册的native方法hook dlsym

先尝试hook RegisterNatives

function find_RegisterNatives(params) {
    let symbols = Module.enumerateSymbolsSync("libart.so");
    let addrRegisterNatives = null;
    for (let i = 0; i < symbols.length; i++) {
        let symbol = symbols[i];

        //_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi
        if (symbol.name.indexOf("art") >= 0 &&
            symbol.name.indexOf("JNI") >= 0 &&
            symbol.name.indexOf("RegisterNatives") >= 0 &&
            symbol.name.indexOf("CheckJNI") < 0) {
            addrRegisterNatives = symbol.address;
            console.log("RegisterNatives is at ", symbol.address, symbol.name);
            hook_RegisterNatives(addrRegisterNatives)
        }
    }
}

let is_hook_RegisterNatives = false;
function hook_RegisterNatives(addrRegisterNatives) {
    if (is_hook_RegisterNatives) {
        return;
    }
    if (addrRegisterNatives != null) {
        Interceptor.attach(addrRegisterNatives, {
            onEnter: function (args) {
                console.log("[RegisterNatives] method_count:", args[3]);
                let java_class = args[1];
                //let class_name = Java.vm.tryGetEnv().getClassName(java_class);
                let methods_ptr = ptr(args[2]);
                let method_count = parseInt(args[3]);
                for (let i = 0; i < method_count; i++) {
                    let name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
                    let sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
                    let fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));

                    let name = Memory.readCString(name_ptr);
                    let sig = Memory.readCString(sig_ptr);
                    let _module = Process.findModuleByAddress(fnPtr_ptr);
                    let baseAddr = _module.base;
                    console.log("[RegisterNatives] name:", name, "sig:", sig, "libName:", Process.findModuleByAddress(fnPtr_ptr).name,  " fnOffset:", fnPtr_ptr.sub(baseAddr), " callee:", DebugSymbol.fromAddress(this.returnAddress));
                    //console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "libName:", Process.findModuleByAddress(fnPtr_ptr).name,  " fnOffset:", fnPtr_ptr.sub(baseAddr), " callee:", DebugSymbol.fromAddress(this.returnAddress));
                }
            }
        });

        is_hook_RegisterNatives = true;
    }
}

let is_hook_dlsym = false;
function hook_dlsym() {
    if (is_hook_dlsym) {
        return;
    }

    let dlsymAddr = Module.findExportByName(null, "dlsym");
    Interceptor.attach(dlsymAddr, {
        onEnter: function (args) {
            this.funcName = args[1].readCString();
        },
        onLeave: function (retval) {
            let module = Process.findModuleByAddress(retval);
            if (module) {
                console.log(module.name, this.funcName, retval.sub(module.base));
            }
        }
    })

    is_hook_dlsym = true;
}

function hook_dlopen(soName) {
    Interceptor.attach(Process.findModuleByName("libandroid.so").getExportByName("android_dlopen_ext"), {
        onEnter: function (args) {
            this.fileName = args[0].readCString()
            console.log(`android_dlopen_ext onEnter: ${this.fileName}`)
            //hook_call_constructors(soName)

        }, onLeave: function(retval){
            console.log(`android_dlopen_ext onLeave: ${this.fileName}`)
            pass_frida_check()
            find_RegisterNatives()

        }
    })

    Interceptor.attach(Process.findModuleByName("libdl.so").getExportByName("dlopen"), {
        onEnter: function (args) {
            this.fileName = args[0].readCString()
            console.log(`dlopen onEnter: ${this.fileName}`)
            //hook_call_constructors(soName)

        }, onLeave: function(retval){
            console.log(`dlopen onLeave: ${this.fileName}`)
            pass_frida_check()
            find_RegisterNatives()

        }
    })
}

let is_pass_frida_check = false;
function pass_frida_check() {
    if (is_pass_frida_check) {
        //console.log('Function already hooked, skipping...');
        return;
    }

    let secmodule = Process.findModuleByName("libmsaoaidsec.so");

    if (secmodule) {
        Interceptor.replace(secmodule.base.add(0x1c544), new NativeCallback(function () {
            console.log(`hook_sub_1c544 >>>>>>>>>>>>>>>>> replace`);
        }, 'void', []));
        Interceptor.replace(secmodule.base.add(0x1b8d4), new NativeCallback(function () {
            console.log(`hook_sub_1b8d4 >>>>>>>>>>>>>>>>> replace`);
        }, 'void', []));
        Interceptor.replace(secmodule.base.add(0x26e5c), new NativeCallback(function () {
            console.log(`hook_sub_26e5c >>>>>>>>>>>>>>>>> replace`);
        }, 'void', []));
        is_pass_frida_check = true; // Mark as hooked
    }
}

 

均在libme.so中

 

三.算法逆向(AES逆向与差分故障攻击,MD5逆向)

1.unidbg 模拟AES算法执行

首先分析aesEncrypt函数

 

反编译aesEncrypt,执行的两个最关键的函数是

其中8AE0似乎初始化了一个很大的数组v21

9E28实际上有三个参数,包括输入的字节数组和输出结果缓冲区,unidbg hook这个函数验证

 

package com.kanxue.test2;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.Symbol;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.arm.backend.ReadHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import unicorn.Arm64Const;
import unicorn.ArmConst;

import java.awt.*;
import java.io.*;
import java.util.Random;

public class MyMoney extends AbstractJni{
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    private final boolean logging;
    private final DvmClass AESEncryptUtil;

    MyMoney(boolean logging) {
        this.logging = logging;

        emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.mymoney").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/kanxue/test2/MyMoney.apk")); // 创建Android虚拟机

        vm.setJni(this);

        //vm.setVerbose(logging); // 设置是否打印Jni调用细节
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/kanxue/test2/libme.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数

        dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
        module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块

        // 加载声明native方法的类
        AESEncryptUtil = vm.resolveClass("com.mymoney.util.AESEncryptUtil");
    }

    void destroy() throws IOException {
        emulator.close();
        if (logging) {
            System.out.println("destroy");
        }
    }


    public static void main(String[] args) throws Exception {
        MyMoney test = new MyMoney(true);
        test.callFunc();
        test.destroy();
    }


    void callFunc() {

        
            emulator.attach().addBreakPoint(module.base + 0x4261C, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                Number number1 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X1);
                Number number2 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X2);
                byte[] bytes1 = emulator.getMemory().pointer(number1.longValue()).getByteArray(0, 32);
                byte[] bytes2 = emulator.getMemory().pointer(number2.longValue()).getByteArray(0, 32);

                Inspector.inspect(bytes1, "1:");
                Inspector.inspect(bytes2, "2:");

                return true;
            }
        });
        
        
        StringObject retVal = AESEncryptUtil.callStaticJniMethodObject(emulator, "aesEncrypt(Ljava/lang/String;I)Ljava/lang/String;",
                "123456789ABCDEF", 0); // 执行Jni方法
        System.out.println(retVal.getValue());


    }
}

 

 

注意到在输入明文少于16字节时输出为16字节,而在明文等于或多于16字节时输出为32字节,说明明文应当是做了填充的,且至少填充一个分组

同时,重复一个明文分组,例如明文为“0123456789ABCDEF0123456789ABCDEF”时,得到密文“C2226C9369A87C2FC370D04C29323794C2226C9369A87C2FC370D04C293237941058C97167132CE1FCBC1643544D010B”

密文中出现重复分组,说明加密模式是ECB的。接着进一步分析sub_9E28

有如下关键部分,做简单的分析可知sub_9D8C的第二个参数与明文有关,第三个是密文缓冲区,hook sub_9D8C

注意到明文被填充了一个字节01,填充方式就明白了

进入函数sub_9D8C

 

这里的逻辑较为简单,仅有两个函数sub_8FD4和sub_9D14,其中v9和明文有关,hook这两个函数

emulator.attach().addBreakPoint(module.base + 0x8FD4, new BreakPointCallback() {
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        Number number0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0);
        Number number1 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X1);
        byte[] bytes = emulator.getMemory().pointer(number0.longValue()).getByteArray(0, 32);
        byte[] bytes1 = emulator.getMemory().pointer(number1.longValue()).getByteArray(0, 32);

        Inspector.inspect(bytes, "0x8FD4 0:");
        Inspector.inspect(bytes1, "0x8FD4 1:");

        return true;
    }
});

emulator.attach().addBreakPoint(module.base + 0x9D14, new BreakPointCallback() {
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        Number number0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0);
        Number number1 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X1);
        Number number2 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X2);
        byte[] bytes = emulator.getMemory().pointer(number0.longValue()).getByteArray(0, 32);
        byte[] bytes1 = emulator.getMemory().pointer(number1.longValue()).getByteArray(0, 32);
        byte[] bytes2 = emulator.getMemory().pointer(number2.longValue()).getByteArray(0, 32);

        Inspector.inspect(bytes, "0x9D14 0:");
        Inspector.inspect(bytes1, "0x9D14 1:");
        Inspector.inspect(bytes2, "0x9D14 2:");

        return true;
    }
});

 

sub_8FD4处的v9

sub_9D14处的v9

说明加密是在sub_8FD4中进行的,进一步分析sub_8FD4

函数进行了主要三部分的操作

第一部分,对明文和最开始初始化的大缓冲区进行操作,疑似进行初始密钥加的操作

第二部分进行了10轮操作,必然就是轮函数了

最后一部分给state赋值

 

2.直接获取主密钥

这里的AES似乎不是查表法,我们尝试直接获取主密钥,转到a1[520]的汇编代码处

取a1[520]对应如下汇编

hook该地址,查看a1数组

emulator.attach().addBreakPoint(module.base + 0x903C, new BreakPointCallback() {
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        Number number0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X20);

        byte[] bytes = emulator.getMemory().pointer(number0.longValue() + 520).getByteArray(0, 32);

        Inspector.inspect(bytes, "a1[520] 0:");

        return true;
    }
});

 

 

结果错误,正确的结果应该是“C2226C9369A87C2FC370D04C293237941058C97167132CE1FCBC1643544D010B”

尝试改变字节顺序,B1取作第1个字节,B5取作第2个字节,B9取作第3个字节,B13取作第4个字节,B2取作低5个字节。。。即取作“262A2824484A44474834383637252654”

结果对应上了

 

3.差分故障攻击获取主密钥

轮函数的位置我们也已经获取到了,再来尝试一下差分故障攻击

首先确定hook点以找到正确的攻击时机

就选择在轮函数的while处

hook 9338处,在进行第8轮的轮函数后,改变state

public void dfaAttack() {
    emulator.attach().addBreakPoint(module.base + 0x9338, new BreakPointCallback() {
        int count = 0;
        UnidbgPointer pointer;
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            count += 1;
            pointer = emulator.getMemory().pointer(addr);
            if (count % 10 == 8) {
                pointer.setByte(randInt(0, 15), (byte) randInt(0, 0xff));
            }
            return true;
        }
    });
}

当然还要先确定state的位置,由最后一部分的state保存的位置可以推导,轮函数中的state保存在如下汇编的X19处

addr保存state的地址,供dfaAttack使用

emulator.attach().addBreakPoint(module.base + 0x92B8, new BreakPointCallback() {
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        Number number0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X19);
        addr = number0.longValue();
                
        return true;
    }
});

 

dfaAttack前的结果795F567258F4110E73D1F74998E1EC7B

dfaAttack后的结果4E5F567258F4116373D1BE499888EC7B

符合在第8此列混淆后,第9轮列混淆前的结果,进行多次差分攻击,将结果进行phoenixAES crack。注意trace_file的第一行为正确密文

795FE072587E110E8DD1F74998E1EC16
795F377258F0110ED5D1F74998E1ECA7
7981567234F4110E73D1F78C98E19E7B
795F56F658F47A0E7351F749B3E1EC7B
245F567258F411C473D1234998B6EC7B
795F56A958F47E0E73CCF74912E1EC7B
795F6372580F110E3BD1F74998E1EC5C
795F567E58F4820E73EBF74988E1EC7B
795F560258F4C40E736DF74943E1EC7B
795F117258E3110E4DD1F74998E1ECF0
E25F567258F411EF73D1C749982BEC7B
799B56728CF4110E73D1F7CA98E1A87B

。。。

 

import phoenixAES

phoenixAES.crack_file('trace_file', [], True, False, 3)

得到第10轮密钥后,再反推主密钥,WBAES得到如下结果

 .\WBAES.exe 7977097D71EE280461F122CEB112BE57 10
K00: 262A2824484A44474834383637252654
K01: 18DD08BE50974CF918A374CF2F86529B
K02: 5EDD1CAB0E4A505216E9249D396F7606
K03: F2E573B9FCAF23EBEA460776D3297170
K04: 5F4622DFA3E9013449AF06429A867732
K05: 0BB30167A85A0053E1F506117B737123
K06: A41027460C4A2715EDBF210496CC5027
K07: AF43EBD6A309CCC34EB6EDC7D87ABDE0
K08: F5390AB75630C67418862BB3C0FC9653
K09: 5EA9E70D08992179101F0ACAD0E39C99
K10: 7977097D71EE280461F122CEB112BE57

K00: 262A2824484A44474834383637252654 与上面直接获取的主密钥一致

 

ex: 轮函数快速定位

AES的加密必然要进行一定的轮运算,执行路径必然呈现出一定的周期性,用如下代码追踪指令执行

public void traceAESCode() {
    emulator.getBackend().hook_add_new(new CodeHook() {
        @Override
        public void hook(Backend backend, long address, int size, Object user) {
            long now = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_PC).longValue();
            if ((now > module.base) & (now < (module.base + module.size))) {
                System.out.println(now - module.base);
            }
        }

        @Override
        public void onAttach(UnHook unHook) {

        }

        @Override
        public void detach() {

        }
    }, module.base, module.base + module.size, null);
}

结果由以下python代码处理

import matplotlib.pyplot as plt
import numpy

input = numpy.loadtxt("trace.txt", int)

plt.ylim(30000, 51000)
plt.plot(range(len(input)), input)
plt.show()

发现明显的10轮循环周期,想必这就是轮函数的位置了

定位如图的框选部分可定位到轮函数中

 

4.frida hook

接下来是函数getDg

第一个参数是Context,不知道里面有啥

public static native String getDG(Context p0,boolean p1,boolean p2,int p3);

 

 

 

分析不出来什么东西,查看ida反编译代码

尝试直接hook sub_C8F8,因为看着和返回结果直接相关

hook 代码

function print_ptr(addr) {
    let module = Process.findRangeByAddress(addr);
    if (module) return hexdump(addr) + "\n";
    return ptr(addr) + "\n";
}


function hook_native_addr(funcPtr, paramsNum) {
    let module = Process.findModuleByAddress(funcPtr);
    Interceptor.attach(funcPtr, {
        onEnter: function (args) {
            this.logs = [];
            this.params = [];
            this.logs.push("call" + module.name + "!" + ptr(funcPtr).sub(module.base) + "\n");

            for (let i = 0; i < paramsNum; i++) {
                this.params.push(args[i]);
            }
            for (let i = 0; i < paramsNum; i++) {
                this.logs.push("arg" + i + "onEnter: " + print_ptr(this.params[i]));
            }
        },
        onLeave: function (retval){
            for (let i = 0; i < paramsNum; i++) {
                this.logs.push("arg" + i + "onLeave: " + print_ptr(this.params[i]));
            }
            this.logs.push("retval onLeave: " + print_ptr(retval) + "\n");
            console.log(this.logs);
        }
    })
}

let base = Process.findModuleByName("libme.so").base;
hook_native_addr(base.add(0xC8F8), 3)

 

函数返回后前两个参数的结果

getDG实际返回  56f95141a5e5534737e99962cea0491ad274eb9fc0d1488f869808e0fe04bca3BB6E4A1F43024A6FA69538B991E12E0D

对比可发现,返回值是由arg1onLeave的结果拼接上arg0onLeave中的部分字符串,再对比抓包的结果

可以发现BB6E4A1F43024A6FA69538B991E12E0D是client-key,是固定的,可能和设备相关,

而前一部分56f95141a5e5534737e99962cea0491ad274eb9fc0d1488f869808e0fe04bca3来自arg0onLeave的字节转16进制,查看函数sub_C8F8

让ai增强一下可读性

__int64 __fastcall sub_C8F8(__int64 result, __int64 a2, int a3) {
    int i = 0; // 字节索引
    while (i < 16) {
        // 目标位置为 a2 + 2 * i(每个字节转成两个字符)
        _BYTE *dest = (_BYTE *)(a2 + 2 * i);

        // 取当前字节
        unsigned char byte_value = *(unsigned __int8 *)(result + i);

        // 高4位(高半字节)
        *dest = a0123456789abcd[byte_value >> 4];

        // 低4位(低半字节)
        *(dest + 1) = a0123456789abcd[byte_value & 0xF];

        i++;
    }
    return result; // 返回原始指针

 

确实是这样的,接着我们来追踪arg0onLeave的16个字节从哪来的

hook sub_E3C8

这16个字节是从sub_E3C8里来的,注意到第三个参数是0x60,正好对应第二哥参数里的0x60字节的字符串

对比抓包结果,其中

首20字节: BB6E4A1F43024A6FA69538B991E12E0D,正好对应抓包中的client-key,是固定的,应该和设备相关

中间20字节:e3ca31777927466fa03e35a7242d86c8,正好对应抓包中的nonce-str,随机生成

尾20字节:1010B3E4F87F46AFA8456873BCBBE009,不知道是什么,但也是固定的

sub_E3C8应当对以上字符串进行了加密,并最后生成16字节的数据,

首先想到md5算法

和结果是对的上的:DGUtil.getDG result=6cdba91a79626731332d5fb02da66626e3ca31777927466fa03e35a7242d86c8BB6E4A1F43024A6FA69538B991E12E0D

 

posted @ 2025-07-02 18:06  superlahm  阅读(28)  评论(0)    收藏  举报