-
-
app逆向--马蜂窝参数分析
-
发表于: 2天前 749
-
声明
特别声明:本文章中所有内容仅供学习交流,不可用于任何商业用途和非法用途,否则后果自负,如有侵权,请联系作者立即删除!由于本人水平有限,如有理解或者描述不准确的地方,还望各位大佬指教!!
前言
今天我们要分析的app是马蜂窝。样本下载
1 | https: / / www.wandoujia.com / apps / 492643 / history_v1039 |
抓包分析
第一步抓包分析。通过抓包发现oauth_signature和zzzghostsigh是加密的。而这个zzzghostsigh的长度是40位的,猜测应该是SHA1加密。oauth_signature_method为HMAC-SHA1,由此可以初步判断oauth_signature的加密方式为HMAC-SHA1。
没有壳,直接jadx打开apk,搜索zzzghostsigh,java层不多说,里面有涉及到java的反射,最终找到下图这里
其中xPreAuthencode是生成zzzghostsigh的方法,xAuthencode是生成oauth_signature的方法。接下来,我们打开ida进入到so。
在导出函数里搜索java,发现没搜到,函数是动态加载的。
unidbg辅助分析
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 | package com.mafengwo; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.arm.backend.Unicorn2Factory; import com.github.unidbg.debugger.Debugger; 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 java.io. File ; import java.util.ArrayList; import java.util. List ; public class mfwAlgo extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; private final Module module; public mfwAlgo() { emulator = AndroidEmulatorBuilder .for64Bit() .addBackendFactory(new Unicorn2Factory(true)) .setProcessName( "com.mfw.roadbook" ) .build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver( 23 )); vm = emulator.createDalvikVM(new File ( "mfw.apk" )); vm.setJni(this); vm.setVerbose(true); DalvikModule dm = vm.loadLibrary(new File ( "libmfw.so" ), true); module = dm.getModule(); dm.callJNI_OnLoad(emulator); } public static void main(String[] args) { mfwAlgo mfw = new mfwAlgo(); } } |
运行,我们要分析的两个函数的地址也打印出来了
zzzghostsigh分析
先静态分析一下伪代码,看看能不能获取有用的信息
没有找到什么有用的信息,sub_3C9C4还是签名校验函数,后面是一大串的运算然后给v36赋值。那我们就换个路子,用unidbg把汇编执行流trace下来
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 | package com.mafengwo; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.arm.backend.Unicorn2Factory; import com.github.unidbg.debugger.Debugger; 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 java.io. File ; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util. List ; public class mfwAlgo extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; private final Module module; public mfwAlgo() { emulator = AndroidEmulatorBuilder .for64Bit() .addBackendFactory(new Unicorn2Factory(true)) .setProcessName( "com.mfw.roadbook" ) .build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver( 23 )); vm = emulator.createDalvikVM(new File ( "mfw.apk" )); vm.setJni(this); vm.setVerbose(true); DalvikModule dm = vm.loadLibrary(new File ( "libmfw.so" ), true); module = dm.getModule(); dm.callJNI_OnLoad(emulator); } public void xPreAuthencode() { String traceFile = "mfw_trace.txt" ; PrintStream traceStream = null; try { traceStream = new PrintStream(new FileOutputStream(traceFile), true); } catch (FileNotFoundException e) { e.printStackTrace(); } / / 核心 trace 开启代码,也可以自己指定函数地址和偏移量 emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream); System.out.println( "xPreAuthencode start" ); List < Object > list = new ArrayList< Object >(); list .add(vm.getJNIEnv()); / / 第一个参数是env DvmObject<?> thiz = vm.resolveClass( "com/mfw/tnative/AuthorizeHelper" ).newObject(null); list .add(vm.addLocalObject(thiz)); / / 第⼆个参数,实例⽅法是jobject,静态⽅法是jclass,直接填 0 ,⼀般⽤不到。 DvmObject<?> context = vm.resolveClass( "android/content/Context" ).newObject(null); list .add(vm.addLocalObject(context)); list .add(vm.addLocalObject(new StringObject(vm, "123456" ))); list .add(vm.addLocalObject(new StringObject(vm, "com.mfw.roadbook" ))); Number number = module.callFunction(emulator, 0x396c8 , list .toArray()); String result = vm.getObject(number.intValue()).getValue().toString(); System.out.println( "result=" + result); } public static void main(String[] args) { mfwAlgo mfw = new mfwAlgo(); mfw.xPreAuthencode(); } } |
通过模拟执行出来的结果为:result=2f45b7dd81d48343c79ddbc14cddb756d7353190
去CyberChef上看看是不是标准的SHA1加密,发现对不上;没关系,继续往下分析
在trace里搜索一下前八位0x2f45b7dd,发现这些正是加密结果,我们跳转到0x3f360
发现大量的运算,我们hook一下这个sub_3E1D0函数的入参和出参
这参数1不就是我们的入参吗,记住mx0的内存地址,继续查看结果。使用blr下个临时断点
这不就是结果的小端序吗。我们来到sub_3E1D0函数初始化赋值的地方,可以看到对参数1进行取值
相等于
1 2 3 4 5 | v18 = 0x67452301 v19 = 0xEFCDAB89 V20 = 0x98BADCFE v21 = 0x5E4A1F7C v22 = 0x10325476 |
仔细一看,这好像是SHA1的魔值,下面这个是标准的魔值
1 2 3 4 5 | A = 0x67452301 B = 0xEFCDAB89 C = 0x98BADCFE D = 0x10325476 E = 0xC3D2E1F0 |
通过对比,发现魔改了第4和5,我们在标准的SHA1算法修改一下魔值,对比一下生成的结果
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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | # sha1-v1 import struct bitlen = lambda s: len (s) * 8 def ROL4(x, n): x & = 0xffffffff return ((x << n) | (x >> ( 32 - n))) & 0xffffffff def madd( * args): return sum (args) & 0xffffffff class sha1: block_size = 64 digest_size = 20 def __init__( self , data = b''): if data is None : self ._buffer = b'' elif isinstance (data, bytes): self ._buffer = data elif isinstance (data, str ): self ._buffer = data.encode( 'ascii' ) else : raise TypeError( 'object supporting the buffer API required' ) self ._sign = None def update( self , content): if isinstance (content, bytes): self ._buffer + = content elif isinstance (content, str ): self ._buffer + = content.encode( 'ascii' ) else : raise TypeError( 'object supporting the buffer API required' ) self ._sign = None def copy( self ): other = self .__class__.__new__( self .__class__) other._buffer = self ._buffer return other def hexdigest( self ): result = self .digest() return result. hex () def digest( self ): if not self ._sign: self ._sign = self ._current() return self ._sign def _current( self ): msg = self ._buffer # standard magic number # A = 0x67452301 # B = 0xEFCDAB89 # C = 0x98BADCFE # D = 0x10325476 # E = 0xC3D2E1F0 A = 0x67452301 B = 0xEFCDAB89 C = 0x98BADCFE D = 0x5E4A1F7C E = 0x10325476 msg_len = bitlen(msg) & 0xffffffffffffffff zero_pad = ( 56 - ( len (msg) + 1 ) % 64 ) % 64 msg = msg + b '\x80' msg = msg + b '\x00' * zero_pad + struct.pack( '>Q' , msg_len) for idx in range ( 0 , len (msg), 64 ): W = list (struct.unpack( '>16I' , msg[idx:idx + 64 ])) + [ 0 ] * 64 for t in range ( 16 , 80 ): T = W[t - 3 ] ^ W[t - 8 ] ^ W[t - 14 ] ^ W[t - 16 ] W[t] = ROL4(T, 1 ) a, b, c, d, e = A, B, C, D, E # main loop: for t in range ( 0 , 80 ): if 0 < = t < = 19 : f = (b & c) | ((~b) & d) k = 0x5A827999 elif 20 < = t < = 39 : f = b ^ c ^ d k = 0x6ED9EBA1 elif 40 < = t < = 59 : f = (b & c) | (b & d) | (c & d) k = 0x8F1BBCDC elif 60 < = t < = 79 : f = b ^ c ^ d k = 0xCA62C1D6 S0 = madd(ROL4(a, 5 ), f, e, k, W[t]) print (f 'num: {t},' + '0x%08x' % k + ',0x%08x' % S0) S1 = ROL4(b, 30 ) a, b, c, d, e = S0, a, S1, c, d A = madd(A, a) B = madd(B, b) C = madd(C, c) D = madd(D, d) E = madd(E, e) result = struct.pack( '>5I' , A, B, C, D, E) return result if __name__ = = '__main__' : s = b '12345' s0 = sha1(s).hexdigest() print (s0) |
发现也对不上,说明该样本对SHA1算法进行了魔改,并不是只修改了IV。
SHA1和MD5采用了相同的结构,每512比特分组需要一轮运算,我们的输入长度不超过一个分组的长度,所以只用考虑一轮运算。一轮运算是80步,每隔20步是一种模式。
我们打印出每一步正常情况下应该得到的结果,然后对比tracecode
对比之前我们先看看伪代码,伪代码里面出现了比较多的数字,我们把它摘抄出来
1 2 3 4 | hex ( 1518500249 ) = 0x5a827999 hex ( 1859775393 ) = 0x6ed9eba1 hex ( - 1894007588 & 0xFFFFFFFF ) = 0x8f1bbcdc hex ( - 899497514 & 0xFFFFFFFF ) = 0xca62c1d6 |
可以看到这些数字是算法源码里面的k值,我们在tracecode里搜索0x5a827999
这是1-3轮的运算结果,跟正常得到的是一样的,我们继续往下看
发现在第16轮之后就找不到了,我们回到伪代码中查看下一个k值是什么
下一个是0x6ed9eba1
我们搜索一下看看出现了多少次
出现了4次,也就是说第17-20轮的k值是0x6ed9eba1,继续往下看
按照这个思路第21-40轮的k值是0x8f1bbcdc
第41-60轮的k值又用回0x5a827999
最后20轮的k值跟源码一样是0xca62c1d6
样本的80轮运算为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | for t in range ( 0 , 80 ): if 0 < = t < = 15 : f = (b & c) | ((~b) & d) k = 0x5A827999 elif 16 < = t < = 19 : f = b ^ c ^ d k = 0x6ED9EBA1 elif 20 < = t < = 39 : f = (b & c) | (b & d) | (c & d) k = 0x8F1BBCDC elif 40 < = t < = 59 : f = (b & c) | ((~b) & d) k = 0x5A827999 elif 60 < = t < = 79 : f = b ^ c ^ d k = 0xCA62C1D6 |
修改后的加密结果
1 2 | 69276ad7 3eea0449 9de68070 ec5535d6 3fa7c266 (修改 80 轮后得到的结果) 69276ad7 3eea0449 26c5f358 6375c2ee 3fa7c266 (unidbg得到的结果) |
可以发现中间两段对不上,我们再次回到伪代码里
在函数的末尾我们发现a1的顺序是4-2-3-1-0。我们在最后一次轮换中也按这个顺序,也就是c和d调换顺序
1 2 3 4 | if t = = 79 : a, b, d, c, e = S0, a, S1, c, d else : a, b, c, d, e = S0, a, S1, c, d |
现在完全对的上了。
oauth_signature分析
通过抓包oauth_signature_method为HMAC-SHA1,由此可以初步判断oauth_signature的加密方式为HMAC-SHA1。先跳转到0x3998c看看伪代码
看起来sub_3B168就是对明文进行加密,点进去看看,里面有很多操作看不懂,看不懂没关系,我们hook一下这个函数的入参和出参
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public void xAuthencode() { List < Object > list = new ArrayList< Object >(); list .add(vm.getJNIEnv()); / / 第一个参数是env DvmObject<?> thiz = vm.resolveClass( "com/mfw/tnative/AuthorizeHelper" ).newObject(null); list .add(vm.addLocalObject(thiz)); / / 第⼆个参数,实例⽅法是jobject,静态⽅法是jclass,直接填 0 ,⼀般⽤不到。 DvmObject<?> context = vm.resolveClass( "android/content/Context" ).newObject(null); list .add(vm.addLocalObject(context)); list .add(vm.addLocalObject(new StringObject(vm, "cookies" ))); list .add(vm.addLocalObject(new StringObject(vm, ""))); list .add(vm.addLocalObject(new StringObject(vm, "com.mfw.roadbook" ))); list .add( 0 ); / / false填 0 Number number = module.callFunction(emulator, 0x3998c , list .toArray()); String result = vm.getObject(number.intValue()).getValue().toString(); System.out.println( "result=" + result); } |
这个函数被调用了很多次,我们现查看它的调用次数
1 2 3 4 5 6 7 8 9 10 11 12 | public void hookDebugger() { Debugger debugger = emulator.attach(); debugger.addBreakPoint(module.base + 0x3B168 , new BreakPointCallback() { int num = 0 ; @Override public boolean onHit(Emulator<?> emulator, long address) { num + = 1 ; System.out.println( "num次:" + num); return true; } }); } |
一共调用了5次,并且在第3次之后明文才被取出来
这里,先说一下标准的HMAC-X的加密流程
- 将密钥用 0x00 填充达到分组长度
- 将第一步填充后的密钥和 0x36 做异或运算
- 第二步得到的密钥后拼接明文
- 对第三步得到的数据做对应加密
- 将第一步填充后的密钥和 0x5c 做异或运算
- 将第四步所得结果附加到第五步所得数据的末尾
- 对第六步所得数据做加密,得到最终结果
上面的截图跟加密流程对比,基本上可以确定是HMAC-SHA1加密
跟unidbg模拟执行出来是对的上的,到此这个样本的两个参数已经分析出来了。
结言
算是第二篇app的文章了,这里的源码已经放在了星球,欢迎大家跟我一起讨论,同时星球会丢更多的一些辅助工具。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
赞赏
- app逆向--马蜂窝参数分析 750
- [原创]使用自动化过腾讯滑块和阿里滑块 2316
- app逆向--某视频刷邀请 7497