篇幅有限 完整内容及源码关注公众号:ReverseCode,发送 冲
v9.3.7抓包 postern+charles
jadx反编译搜索zzzghostsigh
找到params.put(LoginCommon.HTTP_BASE_PARAM_GHOSTSIGH, ghostSigh)
1 2 3 4 5 private static String appendGhostSighParams(Map<String, String> params, String encodeParams) { String ghostSigh = ghostSigh(encodeParams); params.put(LoginCommon.HTTP_BASE_PARAM_GHOSTSIGH, ghostSigh); return encodeParams + encodeUrl("&") + encodeUrl(LoginCommon.HTTP_BASE_PARAM_GHOSTSIGH) + encodeUrl("=") + encodeUrl(ghostSigh); }
进入ghostSigh
是Authorizer
接口的一个方法
1 2 adb shell dumpsys activity activities|more 拿到包名com.mfw.roadbook frida -UF com.mfw.roadbook -l scripts.js 在searchInterface中使用接口名过滤name.indexOf("Authorizer")
利用frida找到该类的实现,ghostSigh
的逻辑就是获取AuthorizeHelper
实例house调用getSummary
方法
1 2 3 4 5 public String ghostSigh(@Nullable Context context, @Nullable String params) { String summary = AuthorizeHelper.getInstance(LoginCommon.getAppPackageName()).getSummary(context, params); Intrinsics.checkExpressionValueIsNotNull(summary, "AuthorizeHelper.getInsta…tSummary(context, params)"); return summary; }
跟进getSummary
,其中调用了so层的xPreAuthencode
方法
1 2 3 4 5 6 7 private native String xPreAuthencode(Context context, String str, String str2); public String getSummary(Context context, String source) { return xPreAuthencode(context, source, this.packageName); } static { System.loadLibrary("mfw"); }
使用objection
内存漫游跟踪下xPreAuthencode
1 2 objection -g com.mfw.roadbook explore -P ~/.objection/plugins android hooking watch class_method com.mfw.tnative.AuthorizeHelper.xPreAuthencode --dump-args --dump-backtrace --dump-return
它接收三个参数,参数1是一个context,参数2是输入的明文,参数3是app的包名
主动调用 frida 构造context,主动调用动态普通函数xPreAuthencode
返回b76da45d4855c8f405ec1352955fe58d3ef0adca
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function mfw(){ Java.perform(function(){ var currentApplication = Java.use('android.app.ActivityThread').currentApplication(); var context = currentApplication.getApplicationContext(); Java.choose("com.mfw.tnative.AuthorizeHelper",{ onMatch:function(instance){ console.log("found instance =>",instance); console.log("instance showText is =>",instance.xPreAuthencode(context,"onejane","com.mfw.roadbook")) },onComplete:function(){ console.log('Search complete') } }) }) } setImmediate(mfw)
unidbg 搭建Unidbg框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class mfw extends AbstractJni{ private final AndroidEmulator emulator; private final VM vm; private final Module module; mfw() { emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.mfw.roadbook").build(); // 创建模拟器实例 final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口 memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析 vm = emulator.createDalvikVM(new File("mafengwo_ziyouxing.apk")); // 创建Android虚拟机 DalvikModule dm = vm.loadLibrary(new File("libmfw.so"), true); // 加载so到虚拟内存 module = dm.getModule(); //获取本SO模块的句柄 vm.setJni(this); vm.setVerbose(true); dm.callJNI_OnLoad(emulator); }; public static void main(String[] args) throws Exception { mfw test = new mfw(); } }
JNI_OnLoad
运行成功并打印出xPreAuthencode
的地址为0x2e301
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public String xPreAuthencode(){ List<Object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // 第一个参数是env list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。 Object custom = null; DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(custom);// context list.add(vm.addLocalObject(context)); list.add(vm.addLocalObject(new StringObject(vm, "onejane"))); list.add(vm.addLocalObject(new StringObject(vm, "com.mfw.roadbook"))); Number number = module.callFunction(emulator, 0x2e301, list.toArray())[0]; String result = vm.getObject(number.intValue()).getValue().toString(); return result; } public static void main(String[] args) throws Exception { mfw test = new mfw(); System.out.println(test.xPreAuthencode()); }
算法还原 由于明文不论多长输出固定为40位字符串,疑似哈希算法中的SHA1。使用IDA的FindHash跑完获取到自动生成的js
1 frida -UF com.mfw.roadbook -l libmfw_findhash_1638601602.js
g跳转到地址0x30548
,x交叉引用,进入sub_2E300
函数,正是xPreAuthencode
函数的位置0x2e301
y设置变量a1类型为JNIEnv*
,其中sub_30548
一定是签名校验函数,如果失败返回Illegal signature
,这个逻辑在Unidbg模拟执行时传入的apk替我们处理了签名校验。那么加密逻辑一定存在于sub_312E0
或者sub_2E1F4
中。
sub_312E0
第一个参数v9是字符串,第二个参数v13是字符数组,第三个参数v10是字符串长度,使用HookZz尝试对sub_312E0
hook并打印参数
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 public void hook_312E0(){ // 获取HookZz对象 IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz // enable hook hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无 // hook MDStringOld hookZz.wrap(module.base + 0x312E0 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数 @Override // 方法执行前 public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer input = ctx.getPointerArg(0); byte[] inputhex = input.getByteArray(0, ctx.getR2Int()); // 打印第一个参数v9 字符串 Inspector.inspect(inputhex, "input"); // 将第二个参数v13放到context Pointer out = ctx.getPointerArg(1); ctx.push(out); }; @Override // 方法执行后 public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer output = ctx.pop(); // 打印第二个参数v13[20] byte[] outputhex = output.getByteArray(0, 20); Inspector.inspect(outputhex, "output"); } }); hookZz.disable_arm_arm64_b_branch(); }
sub_312E0
第一个参数v9正是我们输入的字符串onejane
,第二个参数返回就是加密的最终结果。进入sub_312E0
函数
h将数值转为十六进制
1 2 3 4 5 v30[1] = 0xEFCDAB89; v30[0] = 0x67452301; v30[2] = 0x98BADCFE; v30[3] = 0x5E4A1F7C; v30[4] = 0x10325476;
查看androidxref的sha1.c 源码,SHA1的魔数的第4个和第5个IV被改变了
我们尝试将sha1.py中的魔数成本例中的结果,很可惜结果并不是完全一致。
在函数sub_312E0
中,n重命名入参,除了定义魔数之外,多处调用了sub_3151C
函数,该函数591行,应该就是函数运算部分,加上sub_312E0
共700多行,其中完成了对函数的魔改。
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 int __fastcall sub_312E0(char *input, int output, int length) { ... v30[1] = 0xEFCDAB89; v30[0] = 0x67452301; v30[2] = 0x98BADCFE; v30[3] = 0x5E4A1F7C; v30[4] = 0x10325476; v4 = 0; v32 = 0; v31 = 0; ... if ( (unsigned int)(v21 + 8) < 0x40 ) { v23 = 0; } else { v27 = 64 - v21; qmemcpy(&v33[v21], v29, 64 - v21); sub_3151C(v30, v33); v23 = v27; v21 = 0; } qmemcpy(&v33[v21], &v29[v23], 8 - v23); for ( k = 0; k != 20; ++k ) { *(_BYTE *)(output + k) = *(unsigned int *)((char *)v30 + (k & 0xFFFFFFFC)) >> (~(_BYTE)v22 & 0x18); v22 += 8; } return _stack_chk_guard - v35; }
一个哈希算法主要流程有数据填充、添加长度、初始化变量、数据处理、输出,我们可以尝试通过hook加密函数,观察入参判断填充部分是否发生改变。
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 public void hook_3151C(){ // 获取HookZz对象 IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz // enable hook hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无 // hook MDStringOld hookZz.wrap(module.base + 0x3151C + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数 @Override // 方法执行前 public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { // 类似于Frida args[0] Pointer input = ctx.getPointerArg(0); byte[] inputhex = input.getByteArray(0, 20); Inspector.inspect(inputhex, "IV"); Pointer text = ctx.getPointerArg(1); byte[] texthex = text.getByteArray(0, 64); Inspector.inspect(texthex, "block"); ctx.push(input); ctx.push(text); }; @Override // 方法执行后 public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer text = ctx.pop(); Pointer IV = ctx.pop(); byte[] IVhex = IV.getByteArray(0, 20); Inspector.inspect(IVhex, "IV"); byte[] outputhex = text.getByteArray(0, 64); Inspector.inspect(outputhex, "block out"); } }); hookZz.disable_arm_arm64_b_branch(); }
运算函数入参正常,都是填充的明文,不存在自定义填充或者对明文变换的可能,而出参直接返回结果,所以算法不是在标准流程后魔改,而是修改了算法本身。
SHA1和MD5采用了相同的结构,每512比特分组需要一轮运算,我们的输入长度不超过一个分组的长度,所以只用考虑一轮运算。一轮运算是80步,每隔20步是一种模式。
HookZz实现Inline hook
1 2 3 4 5 6 7 8 9 10 11 public void hook_315B0(){ IHookZz hookZz = HookZz.getInstance(emulator); hookZz.enable_arm_arm64_b_branch(); hookZz.instrument(module.base + 0x315B0 + 1, new InstrumentCallback<Arm32RegisterContext>() { @Override public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) { // 通过base+offset inline wrap内部函数,在IDA看到为sub_xxx那些 System.out.println("R2:"+ctx.getR2Long()); } }); }
但我们整体上需要进行十数次甚至数十次的inline hook,在这种情况下,用HookZz就略有些不方便,不妨试试Unidbg的console debugger。
样本中80步运算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for t in range(80): if t <= 15: K = 0x5a827999 f = (b & c) ^ (~b & d) elif t <= 19: K = 0x6ed9eba1 f = b ^ c ^ d elif t <= 39: K = 0x8f1bbcdc f = (b & c) ^ (b & d) ^ (c & d) elif t <= 59: K = 0x5a827999 f = (b & c) ^ (~b & d) else: K = 0xca62c1d6 f = b ^ c ^ d
在标准流程中,20步切换一下K和非线性函数,一共4种模式,在样本中,每16步切换一下K和非线性函数,一种五种模式,但本质上依然是标准流程里的四个模式,因为一个模式用了两次。