Bootstrap

某团mtgsig2.3-unidbg

前言

只作学习研究,禁止用于非法用途,否则后果自负,如有侵权,请告知删除,谢谢!

最新的版本应该是2.4还是2.5了, 但是用unidbg调试会失败,所以用老版本2.3,但是api请求依然会403,所以只能构造出mtgsig(具体原因后面分析有写)

更新: 现在这版本的app已经无法正常使用了 让升级最新版本 文章看一看就好

开搞:

环境配置

软件作用
jadx-gui反编译
frida-16.0.10hook

参考项目: https://github.com/irabbit666666/unidbg-mt-server23

0x1 jadx反编译分析

搜索mtgsig 找到这个类
在这里插入图片描述
版本5.98
应该是在这个getRequestSignature生成
在这里插入图片描述
进入这个a.a()方法,没有反编译出java代码, 但是可以看到是有put操作的
在这里插入图片描述

直接hook这个CommonCandyInterceptor.getRequestSignature会报错ClassNotFound,搜了一下好像是动态加载的原因,需要先找到classLoader,根据源码分析了一下大致的参数
public String getRequestSignature(String str, URI uri, String str2, String str3, String str4, byte[] bArr)

// 由于是动态加载 需要先找到classLoader 测试为loader的最后一个
function hook_getRequestSignature_with_loader() {
    // hook 动态加载的dex 类, 以及查看类的类名
    Java.perform(function () {
        // hook 动态加载的 dex
        console.log('----寻找loader start----');
        let loaders = new Array();
        Java.enumerateClassLoaders({
            onMatch(loader) {
                console.log(`找到loader: ${loader}`);
                loaders.push(loader);
            },
            onComplete() { }
        })
        console.log('----寻找loader end----');
        console.log(`共找到${loaders.length}个loader`);
        if (loaders.length == 0) {
            return;
        }
        // 测试为最后一个loader 设置为需要使用的类加载器
        const currentLoder = loaders[loaders.length - 1]
        Java.classFactory.loader = currentLoder;
        console.log(`设置loader为: ${currentLoder}`);
		
		const class_name = "com.meituan.android.common.mtguard.wtscore.plugin.sign.interceptors.CommonCandyInterceptor";
   	 	const CommonCandyInterceptor = Java.classFactory.use(class_name);
	    /**
	     * 
	     * @param {*} str1 猜测为post/get 即method 不区分大小写
	     * @param {*} uri 猜测为url
	     * @param {*} str2 猜测为useragent/useragent不存在则为固定值
	     * @param {*} str3 HttpHeaders.CONTENT_ENCODING gzip等
	     * @param {*} str4 猜测为Content-Type
	     * @param {*} bArr inputStream转byte[] 
	     */
	    CommonCandyInterceptor.getRequestSignature.implementation = function (str1, uri, str2, str3, str4, bArr) {
	        console.log('getRequestSignature start');
	
	        console.log(`str1: ${str1}`);
	        console.log(`uri: ${uri}`);
	        console.log(`str2: ${str2}`);
	        console.log(`str3: ${str3}`);
	        console.log(`str4: ${str4}`);
	        console.log(`bArr: ${bArr}`);
	        // 生成的mtgsig
	        const ret = this.getRequestSignature(str1, uri, str2, str3, str4, bArr);
	        console.log(ret);
	
	        console.log('getRequestSignature end');
	        return ret;
	    };        

    });
}

运行没有报ClassNotFound, 但是也不打印任何信息, 不应该啊,直接hook生成试试

function hook_mock_getRequestSignature() {
    console.log('hook start');
    const class_name = "com.meituan.android.common.mtguard.wtscore.plugin.sign.interceptors.CommonCandyInterceptor";
    const String = Java.use("java.lang.String");
    const URI = Java.use("java.net.URI");
    Java.perform(() => {
        const CommonCandyInterceptor = Java.use(class_name);
        const instance = CommonCandyInterceptor.$new();
        const str1 = String.$new("post");
        const uri = URI.$new("https://test.com/test");
        const str2 = String.$new("unknown");
        const str3 = String.$new("gzip");
        const str4 = String.$new("unknown");
        const bArr = Java.array('byte', [13, 37, 42, 66]);
        const ret = instance.getRequestSignature(str1, uri, str2, str3, str4, bArr);
        console.log(ret);

        CommonCandyInterceptor.getRequestSignature.implementation = function (str1, uri, str2, str3, str4, bArr) {
            console.log('getRequestSignature start');
    
            console.log(`str1: ${str1}`);
            console.log(`uri: ${uri}`);
            console.log(`str2: ${str2}`);
            console.log(`str3: ${str3}`);
            console.log(`str4: ${str4}`);
            console.log(`bArr: ${bArr}`);
            // 生成的mtgsig
            const ret = this.getRequestSignature(str1, uri, str2, str3, str4, bArr);
            console.log(ret);
    
            console.log('getRequestSignature end');
            return ret;
        };
    });
}

可以正常生成没问题,但是看了其他人的文章都会加载一个so,看看这个a.a()试试
这里确实调用了NBridge.main3()
在这里插入图片描述
在这里插入图片描述
NBridge.main3()调用main, main是一个native函数,果然有蹊跷
在这里插入图片描述
这里的main1和main3都会调用native main方法
NBridge里并没有发现System.loadLibrary方法, so不是在这个文件加载的

搜索NBridge.main试试, 这里有一个Init,点进去看看
在这里插入图片描述
看样子确实是有初始化操作,然后执行NBridge.main(4, new Object[1])
在这里插入图片描述
这里MTGConfigs.b = "mtguard" 加载的是libmtguard.so文件, 第二行debug语句也能看出来
在这里插入图片描述
在这里插入图片描述
加载完成后会调用NBridge.main3(1, new Object[]{sAppKey}), 这个也是后续unidbg要做的事情, 进行初始化,这里的sAppKey可以在AndroidManifest里找到
在这里插入图片描述
在这里插入图片描述
接下来hook一下main方法

function hook_main() {
    Java.perform(() => {
        const NBridge = Java.use("com.meituan.android.common.mtguard.NBridge");
        const String = Java.use("java.lang.String");
        const JavaByte = Java.use("[B");
        NBridge.main.implementation = function (i, args) {

            console.log('------- start -------');
            console.log(`参数(int)i ${i}`);
            console.log(`args length ${args.length}`)
            for (let i = 0; i < args.length; i++) {
                try {
                    // 强转为byte 不行的话直接走catch直接输出
                    const buffer = Java.cast(args[i], JavaByte);
                    // 创建byte数组
                    const result = Java.array('byte', buffer);
                    // 转string输出
                    const foramtStr = String.$new(result);
                    console.log(`格式化args[${i}] ${foramtStr}`);
                } catch (e) {
                    console.log(`普通args[${i}] ${args[i]}`);
                }
            }
            const ret = this.main(i, args);
            console.log('ret---')
            console.log(ret)

            console.log('------- end -------');
            return ret;
        }
    });
}

在这里插入图片描述
确实和分析的一样, 加载so后会调用NBridge.main3(1, new Object[]{"appKey"}), 返回0则init成功
在这里插入图片描述

0x2 unidbg调试

知道逻辑后就开始unidbg, 先搭好架子, 参考这个项目 (可以直接copy就用), 跑起来没问题

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.EmulatorBuilder;
import com.github.unidbg.Module;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class MtgsigHook extends AbstractJni implements IOResolver {

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    private final DvmClass NBridge;

    private static final String BASE_PATH = System.getProperty("user.dir") + "/data";
    private static final String APK_PATH = BASE_PATH + "/meituan.APK";

    public static void main(String[] args) {
        MtgsigHook hook = new MtgsigHook(true);
    }
    public MtgsigHook(boolean debug) {
        // 创建模拟器实例,要模拟32位或者64位,在这里区分
        EmulatorBuilder<AndroidEmulator> builder = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sankuai.meituan");
        emulator = builder.build();
        // 模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));

        // 创建Android虚拟机
        // vm = emulator.createDalvikVM(); // 只创建vm,用来读so,不加载apk
        vm = emulator.createDalvikVM(new File(APK_PATH));
        // 设置是否打印Jni调用细节
        vm.setVerbose(debug);
        vm.setJni(this);
        emulator.getSyscallHandler().addIOResolver(this);
        emulator.getSyscallHandler().setEnableThreadDispatcher(true);
        // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数,这是直接读so文件
        // DalvikModule dm = vm.loadLibrary(TempFileUtils.getTempFile(LIBTT_ENCRYPT_LIB_PATH), false);
        // 这是搜索加载apk里的模块名,比如 libguard.so 那么模块名一般是guard
        DalvikModule dm = vm.loadLibrary("mtguard", false);
        // 手动执行JNI_OnLoad函数
        dm.callJNI_OnLoad(emulator);
        // 加载好的libttEncrypt.so对应为一个模块
        module = dm.getModule();

        dm.callJNI_OnLoad(emulator);
        NBridge = vm.resolveClass("com/meituan/android/common/mtguard/NBridge");
    }

    @Override
    public FileResult resolve(Emulator emulator, String s, int i) {
        System.out.println("pathname: " + pathname);
        return null;
    }
}

ps: 注意实现的这个接口IOResolver 后期需要用到

根据之前分析的逻辑, 写一个init调用试试

	private void init() {
        ArrayObject dvmObject = NBridge.callStaticJniMethodObject(emulator,
                "main(I[Ljava/lang/Object;)[Ljava/lang/Object;",
                1,
                ArrayObject.newStringArray(vm, "9b69f861-e054-4bc4-9daf-d36ae205ed3e"));
        String ret = dvmObject.getValue()[0].getValue().toString();
        int code = Integer.parseInt(ret);
        if (code != 0) {
            throw new RuntimeException("init失败: " + code);
        }
        System.out.println("init成功.");
    }

意料之中, 环境还没补全
在这里插入图片描述
补了两个classLoader后开始调用main2方法了
在这里插入图片描述
main2根据传入的int值返回对应的结果
在这里插入图片描述
注意调用main2的一定要补全, 不然直接返回null或者报错,

类/方法需要补全
android/os/Build补全机型信息
java/lang/System->getProperty返回java.io.tmpdir和httpProxy信息
android/os/SystemProperties->get基本上是获取一些usb信息
android/content/pm/ApplicationInfo->sourceDir这里要求返回apk路径, 需要下一步使用

上面实现的IOResolver这时候就要用了, 第三个调用的就是apk路径
在这里插入图片描述
这里会读取apk,返回null的话会返回错误的状态码,init失败
最新版本的mtgsig这里返回apk会卡在这里,无法继续, 不知道什么原因
unidbg补完apk文件路径访问,并重定向到指定的apk文件后,程序一直卡住,不再往下运行了

在这里插入图片描述
补一下apk路径试试

	@Override
    public FileResult resolve(Emulator emulator, String pathname, int i) {
        System.out.println("pathname: " + pathname);
        if (pathname.equals("/data/app/com.sankuai.meituan-2nOCxLCJUl7lL3J_S7uSPA==/base.apk")) {
            return FileResult.success(new SimpleFileIO(i, new File(APK_PATH), pathname));
        }
        return null;
    }

在这里插入图片描述
写一个test试试调用, 生成mtgsig的参数 ->
private static native Object[] main(int i, Object[] objArr);

参数类型说明
iint初始化为1,生成mtgsig为2,也有其他的功能
objArr[0]stringappKey 固定值
objArr[1]byte[]按[method params(按A-za-z排序) ]的byte数组
objArr[2]byte[]host -> byte[]
	public void callTest() {
        StringObject arg1 = new StringObject(vm, "9b69f861-e054-4bc4-9daf-d36ae205ed3e");
        ByteArray arg2 = new ByteArray(vm, "GET /TEST/aaa=aa?bbb=bb".getBytes(StandardCharsets.UTF_8));
        ByteArray arg3 = new ByteArray(vm, "test.com".getBytes(StandardCharsets.UTF_8));
        ArrayObject dvmObject = NBridge.callStaticJniMethodObject(emulator,
                "main(I[Ljava/lang/Object;)[Ljava/lang/Object;",
                2,
                new ArrayObject(arg1, arg2, arg3));
        String ret = dvmObject.getValue()[0].getValue().toString();
        JSONObject jsonObject = JSONObject.parseObject(ret);
        System.out.println(jsonObject.toString(JSONWriter.Feature.PrettyFormat));
    }

报错, 需要补充其他的main2返回值
在这里插入图片描述
补充main2() -> 51 8 40 后生成成功
这里也是生成mtgsig里的a7和a8, 这里使用了固定(你用中文都行)
根据源码分析和别人的分析应该是服务器返回,还具有失效时间 只能构造出来 但是不能正常请求接口(最后有写原因)

				if (cmd == 51) {
                    //StringBuilder sb = new StringBuilder();
                    //sb.append(b.b());
                    //return sb.toString();
                    return new StringObject(vm, "2");
                }
                if (cmd == 8) {
                    // return MTGuard.DfpId;
                    return new StringObject(vm, "DAD72CE6E36048C4F132B89C1A61D3388C232E4BEE586E86458EDC83".toLowerCase());
                }
                if (cmd == 40) {
                    // a7,xid
                    return new StringObject(vm, "ChC0KOFzrx6Q9nknGeNbKBnVmztAE6LW2nMYbiTF+hjmHkf/ToA8SDLjODLlnPYhI4dat1iPZmZtHGm7NiPJLkhSwZzjBhbcTSP7RkK7kNk=");
                }

在这里插入图片描述
可以看到a7和a8就是上面填写的 一模一样

0x3 后续分析

相对于初始化,多出来的3个参数

参数实际参数作用(猜测)
51b.b()获取的应该是应用是否在运行, 返回2运行中 1没有运行
8MTGuard.DfpId设备指纹信息
40SyncStoreManager.sXid.id跟上面DfpId一样,也是一种设备Id

main2(51) 获取的应该是应用是否在运行, 返回2运行中 1没有运行
在这里插入图片描述
在这里插入图片描述
main2(8) 获取设备指纹信息, 可以看出就是mtgsig里a7的值,这里的dfpid应该是服务器生成再下发给客户端的, 在文章美团mtgsig2.1和mtgsig1.5区别里也提到 a7,a8是后台返回的
在这里插入图片描述
main2(40)获取xid, 也是跟设备信息有关,具有失效时间。这里也是作为a7的值
所以只能生成mtgsig,用于请求的话a7, a8字段肯定是error的, 会返回403
所以只能生成mtgsig,用于请求的话a7, a8字段肯定是error的, 会返回403
所以只能生成mtgsig,用于请求的话a7, a8字段肯定是error的, 会返回403

通过main也可以生成xid和dfpId,由于是服务端下发,当然也不会生效

	public String getRandomDfpId() {
        ArrayObject dvmObject = NBridge.callStaticJniMethodObject(emulator, "main(I[Ljava/lang/Object;)[Ljava/lang/Object;", 47, new ArrayObject());
        String dfpId = dvmObject.getValue()[0].getValue().toString();
        System.out.println("dfpId: " + dfpId);
        return dfpId.toLowerCase(Locale.ROOT);
    }

    public String getRandomXid() {
        ArrayObject dvmObject = NBridge.callStaticJniMethodObject(emulator, "main(I[Ljava/lang/Object;)[Ljava/lang/Object;", 48, new ArrayObject());
        String xid = dvmObject.getValue()[0].getValue().toString();
        System.out.println("xid: " + xid);
        return xid;
    }

相当于main(47) main(48) 是生成a7 a8
main(8) main(40) 是获取a7 a8
但是生成的a7 a8可能是要经过服务器的 你直接构造出来是无法使用的

;