首页
关于
案例站
tools
记录
推荐
我的B站
Search
1
安卓逆向-unidbg辅助算法还原
34 阅读
2
安卓逆向-Bilibili播放量接口
29 阅读
3
JS逆向-知网滑块
21 阅读
4
安卓逆向-五菱白盒AES
16 阅读
5
安卓逆向-瑞幸咖啡白盒AES
14 阅读
JS逆向
安卓逆向
小程序逆向
登录
Search
标签搜索
安卓逆向
JS逆向
密码学
unidbg
sana
累计撰写
12
篇文章
累计收到
6
条评论
首页
栏目
JS逆向
安卓逆向
小程序逆向
页面
关于
案例站
tools
记录
推荐
我的B站
搜索到
12
篇与
的结果
2025-01-02
安卓逆向-瑞幸咖啡白盒AES
一、瑞幸咖啡白盒AES样本其实已经有很多大佬分享过了,之所以写这样一篇文档是希望理清整体的流程,加深逆向的思想; 仅作技术交流 请勿恶意请求 侵权麻烦联系删除1. 前置[!sana]APP:瑞幸咖啡包名:com.lucky.luckyclient版本:5.0.01加固:360加固目标:q参数这里选择的版本不是新版,因为新版有frida对抗,考虑到复现问题使用了旧版本,这个版本是没有frida检测的;需要前置知晓的知识点大致如下:unidbg简单使用与补环境;AES算法详细流程;参考文档:https://www.yuque.com/nanren-w8l2z/xgu63m/uoff9ovsmhqki9wh白龙大佬的文章,请多多站在巨人的肩膀;差分故障攻击(DFA);在此默认读者已经有逆向环境所以不会提到环境的东西;2. 抓包抓包环境:charles + socksdroid转发;在我的环境下直接就可以抓包,有对抗的话自行解决抓包问题;这里主要的目标就是q参数,sign也是native层,篇幅足够的话会讨论;关于壳在前置提到过,它是有一个360的加固,如图:在这里不涉及脱壳的内容,其实也非常好脱,我在这里使用的是fart脱的壳;3. 代码定位脱完壳后直接搜索sign,但是并搜不到什么有价值的东西;很明显这是人家的东西,所以这里用其他的方式定位;在这里使用hook hashmap的put方法,可以直接定位到,hook代码如下:function showStacks() { console.log( Java.use("android.util.Log") .getStackTraceString( Java.use("java.lang.Throwable").$new() ) ); } Java.perform(function () { var hashMap = Java.use("java.util.HashMap"); hashMap.put.implementation = function (a, b) { if(a!=null && a.equals("q")){ showStacks(); console.log("hashMap.put: ", a, b); } return this.put(a, b); } })在这里可以先将打印的地方注释掉,因为这个app是有双进程保护的,所以需要重启注入,这样就可以避免还没点击登录却输出了过多信息,hook结果如下:java.lang.Throwable at java.util.HashMap.put(Native Method) at com.lucky.lib.http2.r.a(SourceFile:7) at com.lucky.lib.http2.AbstractLcRequest.getRequestParams(SourceFile:14) at com.lucky.lib.http2.m.b(SourceFile:14) at com.lucky.lib.http2.m.getRequest(SourceFile:1) at com.lucky.lib.http2.AbstractLcRequest.async(SourceFile:5) ···省略部分 at butterknife.internal.c.onClick(SourceFile:4) at android.view.View.performClick(View.java:7259) at android.view.View.performClickInternal(View.java:7236) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) hashMap.put: q 8pR_VhJIrWrz_t·····省略部分SLoqiKvmgkJGOXxo=根据堆栈信息去找,发现getRequestParams的位置比较像put的位置,而且也有cid的字眼;但这里并不知道它的键究竟是不是我们需要的,而它放入的是看不懂的东西,其实这里是加密了,如何进行测试呢?主动调用即可,getString2这个方法按理来说就应该是解密字符串的方法,传入相应的参数即可;主动调用代码如下:function dec_str(str){ Java.perform(function (){ let StubApp = Java.use("com.stub.StubApp"); let result = StubApp["getString2"](str); console.log("字符串解密结果: " + result); }) }将上图加密的字符串分别解密结果如下:[Pixel 4::com.lucky.luckyclient ]-> dec_str(7719) 字符串解密结果: sign [Pixel 4::com.lucky.luckyclient ]-> dec_str(16944) 字符串解密结果: uid [Pixel 4::com.lucky.luckyclient ]-> dec_str(4005) 字符串解密结果: cid [Pixel 4::com.lucky.luckyclient ]-> dec_str(457) 字符串解密结果: q当然直接hook这个解密方法输出参数和结果也是可以找到需要的结果的;那么这里正是我们需要的位置,接下来就可以去找生成q参数的native方法到底在哪;没有什么难度,一路向下跟即可;4. hook验证这里其实是有分支的,也就是说这两个圈起来的方法是都有可能走的,所以可以都hook一下,在这里测试过是走的下面的方法,也就不再复现了,hook代码如下:function call_aes() { Java.perform(function () { let ByteString = Java.use("com.android.okhttp.okio.ByteString"); let CryptoHelper = Java.use("com.luckincoffee.safeboxlib.CryptoHelper"); CryptoHelper["localAESWork4Api"].implementation = function (bArr, i2) { console.log('参数1:' + Java.use("java.lang.String").$new(bArr)); console.log('参数2:' + i2); let result = this["localAESWork4Api"](bArr, i2); console.log(`result=${ByteString.of(result).base64()}`); return result; }; }) } call_aes()hook的时候有几个需要注意的点,最后在刚启动时注释掉方法调用,否则有壳的防护可能会找不到类,其次注意只在点击登录之前将注释解掉,这样输出也会少很多方便分析,最后hook到的结果如下:参数1:{"blackBox":"eyJvcyI6ImFuZHJvaWQiLCJ2ZXJzaW9uIjoiMy4zLjciLCJwYWNrYWdlcyI6ImNvbS5sdWNreS5sdWNreWNsaWVudComNS4wLjAxIiwicHJvZmlsZV90aW1lIjozNDQsImluddmFsX3RpbWUiOjc5OCwidG9rZW5faWQiOiJuT1wvZWFscmNCRjZZTnBQYXR0OThDaVNmWmZMdlEyMEFnWHNrUk9OdVVNUXE4dTV0RFl1RWN0aUk5WUM4MHRxSVlaME8zS3BFeUsrNWhvdnNCdjVTcnp6N0NIeFk1d0lhanYyUXVFM2pZNkk9In0=","uniqueCode":"DUStsTlxA16lKIyUaLR_SGOxaGnmBnMTXJ1dRFVTdHNUbHhBMTZsS0l5VWFMUl9TR094YUdubUJuTVRYSjFkc2h1","regionId":"CO0001","mobile":"15244448888","countryNo":"86","validateCode":"456921","regId":"","appversion":"5001","type":1,"deviceId":"android_lucky_beb03f432402baaa","systemVersion":"29","deviceBrand":"google"} 参数2:0 result=8pR/VhJIrWrz/tZtV6BCHViaW+VcsIem1HE6z7xNRYyVtK9SqWgrrgfb3iFkKIEGsfrSjR3dQhXSTLVGCRHEigsSIniYTUc1GbWaPMS1lOY9vZ2x3z0mK7gNza9lU9h8N+SzBmY1heejAwpKAaGGZXcpEjvXKst3PxekVny9uCld8NXdBRh0Ur37jcyK5Ik8PQ/w5pNwRmJY2om/+ilph/d5FkrbdUtZf3pAmSiTcCNKJoI6zCBs1RIXN4y0yFw59a/PpV1vy0giAu8DocmLRpQT/vDriJd+tbsWfV9LqgSieNuAUiK1TKKM01fusC8ByR/zFDoMhEMZKbj7+Ln4p0y/Jy/gYLANWarDm7WUES6zf3rrG+i sYx1JzigdgHk9B+9qVo1gf4ftktwvQV0YHsFOPF6qdOkAZcXDCwDwsNG+rXlSAnxEgRun7ZyumWdOIJqGaZKmleHTI/GnMuAPVk4epMt0Gc8K1WL2s3x1eK1AUcYuK6bksvVa7E6poqshlcHdrtthW9QR5AxOKAUew0708bBlhZO/93tA9b7796JdbeHt pyKviexhfiDpIqeJ4y0qdhsrkvTlIW7hx+/ku6IBdHCBWw4Q1p11i56orROO6YEcTXEmoRzjgZAKrGJUkmD5kLVw8E1ufsTbTfVIzwQ/BCOJJiUQe6/Badbcx4vkPJ6fR9MIeIVOA6N5vn5Jll+itsF+ptKuERsP9IH3gxzRozBQAdU3gvpJhukvv2y/UTfJs+yreXXUj8/mHlyiX6ugE0mhGw+BTdeMEjR1iJ2BiK3FBfLCpP9KVPPVnWG9UDybqS4WKiHACrAZmjJ38MmnSPga6XagdgggDjtZYAokm1WSLoqiKvmgkJGOXxo=与抓包结果对比是对的上的,这也就证明走的方法是没问题的,而且在hook的同时我们可以发现有些参数2是0,而有些是1,当它是1的时候结果解码之后其实就是请求的结果,也就是响应解密的结果,可以大概看出这个参数可能代表的就是加密解密;这里的结果并不完全对的上,在返回的时候做了一些处理;参数中有个blackBox是同盾的风控,在这里不在讨论范畴;5. 算法分析5.1 定位so接下来就应该去分析so文件了,同样的道理,可以去调用解密函数来知道so文件是哪一个;另外直接hook也可以,我这里偷懒就直接选择hook动态注册了,脚本如下:function hook_RegisterNatives() { var RegisterNatives_addr = null; var symbols = Process.findModuleByName("libart.so").enumerateSymbols(); for (var i = 0; i < symbols.length; i++) { var symbol = symbols[i].name; if ((symbol.indexOf("CheckJNI") == -1) && (symbol.indexOf("JNI") >= 0)) { if (symbol.indexOf("RegisterNatives") >= 0) { RegisterNatives_addr = symbols[i].address; console.log("RegisterNatives_addr: ", RegisterNatives_addr); } } } Interceptor.attach(RegisterNatives_addr, { onEnter: function (args) { var env = args[0]; var jclass = args[1]; var class_name = Java.vm.tryGetEnv().getClassName(jclass); var methods_ptr = ptr(args[2]); var method_count = args[3].toInt32(); console.log("RegisterNatives method counts: ", method_count); for (var i = 0; i < method_count; i++) { var name = methods_ptr.add(i * Process.pointerSize * 3).readPointer().readCString(); var sig = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize).readPointer().readCString(); var fnPtr_ptr = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2).readPointer(); var find_module = Process.findModuleByAddress(fnPtr_ptr); console.log("类: ", class_name, "方法: ", name, "签名: ", sig, "函数地址: ", fnPtr_ptr, "模块名: ", find_module.name, "函数偏移: ", ptr(fnPtr_ptr).sub(find_module.base)); } }, onLeave: function (retval) {} }); } hook_RegisterNatives()hook结果如下,有报错不用管,也可以自行修复:RegisterNatives method counts: 4 方法: localAESWork 签名: ([BI[B)[B 函数地址: 0xc113684d 模块名: libcryptoDD.so 函数偏移: 0x1984d 方法: localConnectWork 签名: ([B[B)[B 函数地址: 0xc113678d 模块名: libcryptoDD.so 函数偏移: 0x1978d 方法: md5_crypt 签名: ([BI)[B 函数地址: 0xc1137981 模块名: libcryptoDD.so 函数偏移: 0x1a981 方法: localAESWork4Api 签名: ([BI)[B 函数地址: 0xc11381cd 模块名: libcryptoDD.so 函数偏移: 0x1b1cd5.2 白盒AES涉及到的四个native函数都找到了,so是libcryptoDD.so,函数地址也都找到了,比较方便;去看一下so长什么样,此样本只有32位的so,不过9.0也不区分了,没有关系;可以看到不是平时看到的那些状态,这是做了混淆,但也不影响,而且也没用Java开头的函数,所以我们直接hook动态注册是正确的;上面已经得到了函数的偏移,在IDA按下G键,跳转到对应的偏移位置;字面意思可以看出一点端倪,wb也就是白盒的缩写,所以它有可能是一个白盒AES,当然这也只是一个猜测而已,不过逆向就是要大胆猜测小心求证,猜一下无伤大雅;进入到方法里面,实际上比较关键的点也就是这一个方法而已;可以发现通篇看下来无非就是这两个方法,虽然不同的位置,但方法是同一个,管他什么逻辑进去看看先,因为被混淆了所以两个位置有一个位置条件永远不成立也不是不可能;进入方法后进行分析,截取几个重要的位置做参考;位置1:位置2:位置3:位置4:通篇看下来其实是没多少内容的,而且这个样本之所以不算特别难,是因为它的符号大多都在,这大大减少了我们在分析的难度,甚至可以直接告诉我们很多信息;大概重要的就是位置4,可以看出大概是一个ecb模式的加密,但不能完全相信这个结果,我们可以hook一下这个函数,hook代码如下,32位的so地址需要+1;function call_so() { var base_addr = Module.findBaseAddress("libcryptoDD.so"); let funcPtr = base_addr.add(0x17BD4 + 1) Interceptor.attach(funcPtr, { onEnter: function (args) { console.log("参数0:" + args[0].readCString()); console.log("参数1:" + args[1].toInt32()); this.len = args[1].toInt32() this.args2 = args[2]; console.log("参数3:" + args[3].toInt32()); }, onLeave: function (retval) { var hexdumpString = hexdump(this.args2, { offset: 0, length: this.len, header: true, ansi: true }); console.log("参数2返回时(hexdump):\n" + hexdumpString); } }); } call_so()hook结果如下:java入参: 参数1:{"blackBox":"eyJvcyI6ImFuZHJvaWQiLCJ2ZXJzaW9uIjoiMy4zLjciLCJwYWNrYWdlcyI6ImNvbS5sdWNreS5sdWNreWNsaWVudComNS4wLjAxIiwicHJvZmlsZV90aW1lIjozMDEsImludGVydmFsXbWUiOjc5NSwidG9rZW5faWQiOiJuT1wvZWFscmNCRjZZTnBQYXR0OThDbmZhSWVPTlBWaW92ZHBMb09CZG13eTdpRmk4Ukp2clhwNlVZZTBYTmRuYUxVY3R0QzlBczUreXBGdDNyOFRaWGxOOXFoZHNKUFdtbDBqWnJGWjE1K1k9In0=","uniqueCode":"DUStsTlxA16lKIyUaLR_SGOxaGnmBnMTXJ1dRFVTdHNUbHhBMTZsS0l5VWFMUl9TR094YUdubUJuTVRYSjFkc2h1","regionId":"CO0001","mobile":"15211115555","countryNo":"86","validateCode":"123456","regId":"","appversion":"5001","type":1,"deviceId":"android_lucky_beb03f432402baaa","systemVersion":"29","deviceBrand":"google"} 参数2:0 native入参: 参数0:{"blackBox":"eyJvcyI6ImFuZHJvaWQiLCJ2ZXJzaW9uIjoiMy4zLjciLCJwYWNrYWdlcyI6ImNvbS5sdWNreS5sdWNreWNsaWVudComNS4wLjAxIiwicHJvZmlsZV90aW1lIjozMDEsImludGVydmFsX3RpbWUiOjc5NSwidG9rZW5faWQiOiJuT1wvZWcmNCRjZZTnBQYXR0OThDbmZhSWVPTlBWaW92ZHBMb09CZG13eTdpRmk4Ukp2clhwNlVZZTBYTmRuYUxVY3R0QzlBczUreXBGdDNyOFRaWGxOOXFoZHNKUFdtbDBqWnJGWjE1K1k9In0=","uniqueCode":"DUStsTlxA16lKIyUaLR_SGOxaGnmBnMTXJ1dRFVTdHNUbHhBMTZsS0l5VWFMUl9TR094YUdubUJuTVRYSjFkc2h1","regionId":"CO0001","mobile":"15211115555","countryNo":"86","validateCode":"123456","regId":"","appversion":"5001","type":1,"deviceId":"android_lucky_beb03f432402baaa","systemVersion":"29","deviceBrand":"google"}������������������������������������������������ 参数1:656 参数3:0 参数2返回时(hexdump): 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF c297e100 f2 94 7f 56 12 48 ad 6a f3 fe d6 6d 57 a0 42 1d ...V.H.j...mW.B. ···省略部分内容 c297e380 ae 46 23 46 05 9e 4c 11 53 d3 ca 59 ab a5 51 08 .F#F..L.S..Y..Q.经过验证返回的内容就是抓包的结果,也就证明了函数是对的方向,而这个明文与java层的明文大致是一样的,只是后面多了一些不可见字符,我推测可能是末尾的填充,后续留个心眼;也就是说接下来重点就在于这个wbaes_encrypt_ecb方法;依旧是找几个关键点做分析;位置1:位置2:选择第一个进去观察一下,看看是否会有白盒的特征;这里看见了一个比较重要的字眼,循环左移,这个需要了解aes的算法流程;除了这个以外,还有很多的操作,很符合查表的特征;去看看表的大小;很显然普通的查表法aes的表是没有这么大的,这也很符合白盒aes的特征;DFA的攻击点在哪里?这个也需要读者了解,这里直接给出;也就是图中的四个攻击点,原理这里不展开;而在这里我们一共发现了两个关键函数,点进去查看它的循环左移符号均未抹去,在结合上图的攻击点,不是正好有个点是ShiftRows吗,那我们就可以对它做一些文章;循环左移的入参是state块,左移后结果也存在里面,所以DFA就可以攻击这个位置;如果你了解AES的算法细节,你会知道它的加密轮数有10轮(仅在此案例讨论,具体轮数不定),攻击点就在8-9轮中间,而这里循环左移是最合适的,因为它符号并没有抹去,就可以很畅快的注入DFA;准备好以后就可以进行故障攻击了;5.3 DFA这里由于不需要补环境,所以在后面就使用unidbg来攻击;首先搭好基本的框架;package com.xyt.sana; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; 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.linux.android.dvm.jni.ProxyDvmObject; import com.github.unidbg.memory.Memory; import java.io.File; public class RuiXin extends AbstractJni { public static AndroidEmulator emulator; public static Memory memory; public static VM vm; public static Module module; public RuiXin(){ emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.lucky.luckyclient").build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("apks/ruixin/rxkf5.0.01.apk")); vm.setJni(this); vm.setVerbose(true); DalvikModule dm = vm.loadLibrary("cryptoDD", true); dm.callJNI_OnLoad(emulator); module = dm.getModule(); } public static void main(String[] args) { RuiXin rxkf = new RuiXin(); } }不需要补环境,直接上DFA代码,在这里需要读者了解unidbg的hook,unidbg完整代码如下:package com.xyt.sana; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Emulator; import com.github.unidbg.Module; import com.github.unidbg.arm.context.RegisterContext; import com.github.unidbg.debugger.BreakPointCallback; 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.linux.android.dvm.jni.ProxyDvmObject; import com.github.unidbg.memory.Memory; import com.github.unidbg.memory.MemoryBlock; import com.github.unidbg.pointer.UnidbgPointer; import javax.xml.bind.DatatypeConverter; import java.io.File; import java.util.Random; public class RuiXin extends AbstractJni { public static AndroidEmulator emulator; public static Memory memory; public static VM vm; public static Module module; public RuiXin() { emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.lucky.luckyclient").build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("apks/ruixin/rxkf5.0.01.apk")); vm.setJni(this); vm.setVerbose(true); DalvikModule dm = vm.loadLibrary("cryptoDD", true); dm.callJNI_OnLoad(emulator); module = dm.getModule(); } public static String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { int unsignedInt = b & 0xff; String hex = Integer.toHexString(unsignedInt); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); } public static byte[] hexToBytes(String hexString) { return DatatypeConverter.parseHexBinary(hexString); } public static int randint(int min, int max) { Random rand = new Random(); return rand.nextInt((max - min) + 1) + min; } public void calldfa() { Debugger debugger = emulator.attach(); debugger.addBreakPoint(module.base + 0x14f98, new BreakPointCallback() { UnidbgPointer pointer; RegisterContext context = emulator.getContext(); int num = 1; @Override public boolean onHit(Emulator<?> emulator, long address) { pointer = context.getPointerArg(0); if (num % 9 == 0) { pointer.setByte(randint(0, 15), (byte) randint(0, 0xff)); } num += 1; return true; } }); } public void call_wbaes() { // 主动调用 MemoryBlock inputBlock = emulator.getMemory().malloc(16, true); UnidbgPointer inputPtr = inputBlock.getPointer(); MemoryBlock ouputBlock = emulator.getMemory().malloc(16, true); UnidbgPointer ouputPtr = ouputBlock.getPointer(); byte[] byteArray = hexToBytes("55669988555269996541441122554411"); assert byteArray != null; inputPtr.write(0, byteArray, 0, byteArray.length); module.callFunction(emulator, 0x17bd5, inputPtr, 16, ouputPtr, 0); String res = bytesToHex(ouputPtr.getByteArray(0, 0x10)); // System.out.println("白盒结果:" + res); System.out.println(res); inputBlock.free(); ouputBlock.free(); } public static void main(String[] args) { RuiXin rxkf = new RuiXin(); // rxkf.calldfa(); // rxkf.call_wbaes(); for (int i = 0; i < 300; i++) { rxkf.calldfa(); rxkf.call_wbaes(); } } } 首先主动调用一次工具,对比一下正确密文和故障密文有什么差别;60 9b 2ab3 8e fb53e028d8b4 5a 191a f3 13 60 10 2ab3 3d fb53e028d8b4 3f 191a bf 13可以发现有四个字节是不一样的,这个需要读者了解差分故障攻击,同时我会将涉及到的东西放在文件里;接下来就是循环几百次,收集多个故障密文,再使用phoenixAES去推导第十轮的密钥,再去使用轮密钥推导主密钥,其中一些结果如下图:使用phoenixAES库得到K10密钥,使用方法如下:# pip install phoenixAES import phoenixAES phoenixAES.crack_file('tracefile.txt', [], True, False, 3)tracefile.txt放得到的故障密文,第一行是正确的密文,其余的是故障密文;它推导的k10:869D92BBB700D0D25BD9FD3E224B5DF2,再使用k10去推导主密钥;推导结果如下:所以它的原始密钥就是 644A4C64434A69566E44764D394A5570;这里去测试结果是一样的,以下是加密的结果:hook的结果:对比来看也是对的上的;至此q参数分析完毕,这篇文章的重点也就在此,一个简单的白盒AES;本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!6. 总结很少写长篇幅的东西,很多东西也来不及仔细的写到文档,如有讲解不到位的欢迎指出;技术交流+vx:HeiYuKuaiDou23
2025年01月02日
14 阅读
2 评论
0 点赞
2024-07-17
JS逆向-产业大数据平台
一、产业政策大数据平台1.前置网址:aHR0cDovL3d3dy5zcG9saWN5LmNvbS90eXBlUG9saWN5P2lkPTYmbmFtZT0lRTUlODUlQUMlRTclQTQlQkElRTUlODUlQUMlRTUlOTElOEE=目标:表单参数2.逆向思路首先打开抓包页面就可以看到一个无限debugger,在这里我们使用hook的方式来过;hook代码如下:// 变量 构造器 AAA = Function.prototype.constructor; Function.prototype.constructor = function (a) { if (a == 'debugger'){ return function (){}; } return AAA(a); };或者去李玺老师博客找更好的hook代码:http://cnlans.com/lx/tools/jshook将这段代码复制到控制台运行或者新建代码段执行,我推荐第二种;执行后即可过掉debuuger;我们向下翻页,可以发现一个没有见过的加密参数;有一点像乱码,那么我们就要去找到它的生成位置;在以往来说,这种表单参数没有关键词的时候,且是异步加载时,我们可以去找他的拦截器关键词,但在这里没有办法直接搜索,因为它是经过混淆的,但这个思想在其他地方适用;在这里我们就可以去下xhr断点,之后进行分页,此时断住;点击异步栈的第一栈,也就是箭头的指向;进入之后打上断点,并将xhr断点放掉;此时观察s,它里面会有拦截器,进入内部看,可以看出是经过混淆的;我们需要的是请求拦截器,所以我们就需要看当前是否是我们需要的位置;很明显这就是请求拦截器,也可以去其他几个方法看看,因为它有时候不止一个拦截器,我们就不去看了,把外面的断点放掉,然后进入这个断点;可以看到此时我们的参数还是明文,那我们就需要点击跳出函数来看它在哪里变成的密文;点击跳出一次后参数任然是明文,所以继续跳出;此时作用域没有值了,我们再跳一次;再跳一次就可以发现我们的参数生成了,那我们就需要去前面找加密方法;那么有没有可能是明文消失的前一栈呢,很有可能,我们找拦截器的原因就是生成参数的位置大多都会在那里,既然明文消失的最后一次出现是在拦截器,那我们理应去那里找,再次触发请求;来到上一个位置就可以发现参数就是在这里加的密,那我们依次在控制台还原里面的混淆;将改写后的代码都拿下来;这即是我们的加密方法,那么此时我们需要做的是就是找到这三个方法是吧,首先去找这个encode方法,点击进去;进入一个虚拟文件,那么我们的方法就是这个policy,将它拿下来,注意此时不要拿return,只拿方法;我们观察这个方法,传进去两个参数,将断点打进去,可以看到m是我们的参数,w不知道,那我们可以看到它说如果w不存在,就创建,那我们直接不传,让它生成就好了;但这里会报错创建方法不存在,那我们也需要进去看看;进入之后可以看到一个很熟悉的东西,webpack,那在这里我们的思路其实就是将它导出再调用;我们就需要去找到他所在的大方法里,也就是顶格的方法;在这里我的方法是将整段复制到pad++里,然后比较方便找,语言选择js,然后折叠所有层次;再搜索我们的方法;这样就比较好拿我们的方法,我们拿下来后进行改写;此时运行一下会报错commonjsGlobal,我们在控制台输出它,发现他就是一个window对象,那我们直接全改成window,然后再定义一个window对象;此时我们先注释掉其他的代码,只留下这个webpack看看能否运行,发现是没有报错的,那我们就可以导出函数了,在顶部顶一个zt变量,用来接受加载器;此时我们就可以试着打印一下zt,看看有没有接收到方法;可以看到成功的拿到了方法,所以create函数就可以改写为zt..Writer.create()了,此时直接打印一下看能否出值;可以看到出值了,那么后面两个方法应该就在上面的webpack里,当然这里还会有一个报错,会说参数类型错误,经过调试其实就是参数中的 "policyType": '6',这个6必须是字符串形式;和网页上进行对比看看是否是正确的;可以看到结果是一致的,那么参数逆向也就完成了;使用python进行请求,可以成功拿到数据;
2024年07月17日
13 阅读
0 评论
0 点赞
2024-07-17
JS逆向-马蜂窝加速乐ck
一、马蜂窝1.前置:网址:aHR0cHM6Ly93d3cubWFmZW5nd28uY24v需要逆向的参数:cookie/加速乐;2.逆向流程:首先我们需要理解加速乐的过程,整体逻辑如下:首先进行第一次页面请求,它会返回一个响应的cookie,以及一段简单的js代码,那么第一次请求就会得到两个cookie,分别为 __jsluid_s和__jsl_clearance_s,第一次请求结束之后,带着这两个cookie去进行第二次请求,会得到一个js代码。它会生成一个新的cookie值,将它赋给__jsl_clearance_s,然后再带着这两个cookie进行第三次请求,就能够得到最终的响应;理解了整体的逻辑我们就可以进行第一步了;首先查看第一次请求;可以看到返回了一小段js代码,这段代码可以直接在控制台执行得到结果的,我们肯定需要进行第一次请求拿到这个结果,以及服务器为我们响应的cookie;其次再进行第二次的请求,带着第一次请求得到的两个cookie去请求,会得到另一段比较长的js代码;可以看到是经过混淆的代码,那么我们就需要去看这段js代码了,我们在这里打上一个事件断点;然后将cookie清空进行刷新,此时就可以断住,来到我们的第二段js代码;我们将代码折叠,来进行相应的分析;其实它是经过了ob混淆的,第一行是一个大数组,第二行一个自执行,第三行解密函数,用来解混淆,那么重要的也就是后面的东西,结尾可以看到它调用了go函数,往里面传了参数,那么我们就来看看go函数;搜索document,打上断点,因为cookie就是与document相关的,我们让程序执行过来;里面其实就是cookie,那么后面的就是我们的值;那么我们就可以去找值的生成位置了,对于加速乐来说,直接全部拿下来就可以了,进行简单的补环境即可;需要注意的是,这里的参数里的hs也就是里面的hash算法,他一共有三种,我们需要进行一个判断,然后写三种加密,分别为md5、sha1、sha256,然后进行一个简单的判断;我们就可以得到第二次请求需要携带的cookie,然后我们进行相应的请求进行测试;请求成功的结果如下:py代码如下:# coding: utf-8 # File: 马蜂窝加速乐.py # Author: 下雨天 # Date: 2024/01/20 16:50 from calendar import c import re from urllib import response import execjs import requests url = "https://www.mafengwo.cn/" headers = { ··· } def first_req(): response = requests.get(url, headers=headers) cookie1 = re.findall('document.cookie=(.*?);location', response.text)[0] __jsl_clearance_s = execjs.eval(cookie1).split('clearance_s=')[-1] __jsluid_s = response.cookies.get_dict()['__jsluid_s'] return __jsluid_s,__jsl_clearance_s def sec_req(): __jsluid_s,__jsl_clearance_s = first_req() cookies = { "__jsluid_s": __jsluid_s, "__jsl_clearance_s": __jsl_clearance_s } response = requests.get(url, headers=headers, cookies=cookies).text res = execjs.eval(re.findall('};go\((.*?)\)</scri',response)[0]) # 通过正则匹配到js代码,然后用execjs.eval()将得到的值变成字典 print(res) cook3 = execjs.compile(open('马蜂窝加速乐.js', 'r', encoding='utf-8').read()).call('go', res) cookies = { "__jsluid_s": __jsluid_s, "__jsl_clearance_s": cook3.split('ce_s=')[-1], } return cookies def third_req(): cookies = sec_req() response = requests.get(url, headers=headers, cookies=cookies) print(response.text) if __name__ == '__main__': third_req()
2024年07月17日
5 阅读
0 评论
0 点赞
2024-07-10
安卓逆向-泡泡聊天
一、泡泡聊天1. 逆向前置1.1 逆向目标目标:账号密码登录版本:v1.7.4时间:2024-06-101.2 简要分析关于泡泡聊天比较重要的就是抓包证书以及360的壳,直接抓包结果如下:这是做了服务端证书的校验,所以我们需要拿到证书并且导入到charles,具体步骤如下:- 获取 bks 或 p12证书 文件 - 获取证书相关密码 - 将证书导入到charles,可以实现抓包(bks格式需要转换p12格式) - 用requests发送请求时,携带证书去发送请求2. 逆向实现2.1 获取证书&密码使用frida脚本进行hook相应的证书与密码,这是上述步骤的前两步;可以看到加载了不止一个证书,这是正常的,但是只有一个有密码,所以无疑就是这个了;那么它的证书就在asset目录下的bks文件;那么此时我们并不知道具体是哪一个bks文件,所以在此直接保存当前获取到的流文件并且保存下来;Java.perform(function () { var KeyStore = Java.use("java.security.KeyStore"); var String = Java.use("java.lang.String"); KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (inputStream, v2) { var pwd = String.$new(v2); console.log('\n--------------------------------------------------------------') console.log("密码:" + pwd, this.getType()); if (this.getType() === "BKS") { var myArray = new Array(1024); for (var i = 0; i < myArray.length; i++) { myArray[i] = 0x0; } var buffer = Java.array('byte', myArray); var file = Java.use("java.io.File").$new("/sdcard/Download/paopao-" + new Date().getTime() + ".bks"); var out = Java.use("java.io.FileOutputStream").$new(file); var r; while ((r = inputStream.read(buffer)) > 0) { out.write(buffer, 0, r); } console.log("save success!") out.close() } var res = this.load(inputStream, v2); return res; }; }); // frida -U -f com.paopaotalk.im -l 2.hook_save.js在进行这一步之前需将app赋予可读写的权限;随后去观察是否保存成功;至此证书以及密码我们获取到了;后续就需要导出证书并且导入到charles;2.2 导出&导入证书扩展:有时进行hook时会失败,提示找不到类等,是因为有壳导致,可以加上延迟;此时我们需要将bks证书转换成p12的格式,才支持;使用portecle-1.11工具;将选择的文件导出,选择p12格式;此时根据指引输入密码后即可导出;接下来即可将证书导入到charles中;选择客户端证书,并且添加;导入p12证书;端口等选择所有;接下来再度抓包即可成功;2.3 代码实现具体逻辑比较简单,脱壳使用frida-dumpdex大致实现如下:frida-dexdump -U -f 包名# File:01-泡泡聊天登录实现.py # Author:下雨天 # Date:2024-06-10 21:50 import hashlib import random from requests_pkcs12 import get, post import json import uuid import urllib3 urllib3.disable_warnings() def md5_hash(password): md5_obj = hashlib.md5() md5_obj.update(password.encode('utf-8')) md5_hash = md5_obj.hexdigest() return md5_hash def double_md5_hash(password, openId): # 第一次MD5加密 md5_obj = hashlib.md5() md5_obj.update(password.encode('utf-8')) first_md5_hash = md5_obj.hexdigest() # 将第一次MD5加密结果与openId和"MOSGRAM"拼接起来 combined_string = first_md5_hash + openId + "MOSGRAM" # 第二次MD5加密 md5_obj = hashlib.md5() md5_obj.update(combined_string.encode('utf-8')) second_md5_hash = md5_obj.hexdigest() return second_md5_hash def login(): password = "aa12121323" openId = "008615222225555" timestamp = random.randint(100, 999) headers = { "bundle_id": "com.paopaotalk.im", "version": "1.7.4", "timestamp": str(timestamp), "sign": md5_hash(str(timestamp)), "app_id": "qiyunxin", "Accept-Language": "zh-CN", "package": "com.paopaotalk.im", "Content-Type": "application/json; charset=UTF-8", "Host": "api.vvchat.im", "User-Agent": "okhttp/4.8.1" } url = "https://api.vvchat.im/userservices/v2/user/login" device_id = md5_hash(str(uuid.uuid4()).replace("-", "")) data = { "device_type": "app", "username": openId, "password": double_md5_hash(password, openId), "device_id": device_id, "device_name": "Google Pixel 2 XL", "device_model": "Pixel 2 XL" } data = json.dumps(data, separators=(',', ':')) response = post(url, headers=headers, data=data, pkcs12_filename='Client.p12', pkcs12_password='111111', verify=False ) print(response.text) print(response) if __name__ == '__main__': login()值得注意的就是发送请求时依然需要携带证书;
2024年07月10日
8 阅读
0 评论
0 点赞
2024-07-09
JS逆向-知网滑块
一、知网滑块1. 前置网址:https://bar.cnki.net/bar/dist/index.html?platform=NZKPT&returnUrl=https://kns.cnki.net/kcms2/article/abstract?v=tJ8vF22QX-q7kidyidVkTi-15296WO2bIZkecNQnvATomVsZzFd4vsj-3nwGsNPXaBqCOjgkuJE-Ik7jGSynrXwWxD4eTmqK7wwk-xox4OLeV639rCKCz8NKDn3bxBwoROTns2CYDbk=&uniplatform=NZKPT&language=CHS需要逆向的参数:表单参数 pointJson2. 逆向流程链接后续为随意一个论文或期刊的详情页链接;首先我们的思路就是先拿到验证码的图片,我们进行抓包分析;点击刷新验证码按钮发起请求,可以看到相应的请求信息,我们查看他的返回;可以看到这两组值应该就是图片的base64格式,那么我们验证一下;可以看到是没有问题的,那么我们的思路就是将他们拿下来,然后进行距离的识别即可;所以我们第一件事情就是请求验证码,我们看看它的请求需要哪一些东西;可以看到对于验证码的请求并没有其他的参数,所以我们可以去发起请求;在这里我们拿到了大小图的二进制数据,此时我们去进行滑动,触发一下校验请求;观察参数,发现token是请求验证码返回的,代表着这张验证码给谁了,所以我们校验的时候带着这个token,那么就代表cookie应该是不被需要的,但是同时也可以看到有一个pointjson参数,那么大概率会是验证码相关的东西;所以我们搜索它看看能否找到加密位置;很容易就找到加密位置,所以我们就需要对它进行分析;可以看到,这里执行的就是:左边的代码,所以我们这里需要找的就是i,在上方就有它的生成;选中的其实就是我们滑动的距离,所以我们对它进行处理;所以我们这里需要拿的就是u方法,其实就是AES;那么这个加密就比较简单了,所以我们需要的就是滑动距离;使用ddddocr进行识别,然后得到需要的pointJson参数,然后就可以去进行验证了;结果如下,在这里需要注意的是这里的参数是字符串,并且json转的时候是会有空格的,所以我们进行了替换;
2024年07月09日
21 阅读
0 评论
0 点赞
1
2
3