前言
只作学习研究,禁止用于非法用途,否则后果自负,如有侵权,请告知删除,谢谢!
最新的版本应该是2.4还是2.5了, 但是用unidbg调试会失败,所以用老版本2.3,但是api请求依然会403,所以只能构造出mtgsig(具体原因后面分析有写)
更新: 现在这版本的app已经无法正常使用了 让升级最新版本 文章看一看就好
开搞:
环境配置
软件 | 作用 |
---|---|
jadx-gui | 反编译 |
frida-16.0.10 | hook |
参考项目: https://github.com/irabbit666666/unidbg-mt-server23
0x1 jadx反编译分析
搜索mtgsig 找到这个类
应该是在这个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);
参数 | 类型 | 说明 |
---|---|---|
i | int | 初始化为1,生成mtgsig为2,也有其他的功能 |
objArr[0] | string | appKey 固定值 |
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个参数
参数 | 实际参数 | 作用(猜测) |
---|---|---|
51 | b.b() | 获取的应该是应用是否在运行, 返回2运行中 1没有运行 |
8 | MTGuard.DfpId | 设备指纹信息 |
40 | SyncStoreManager.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可能是要经过服务器的 你直接构造出来是无法使用的