某商超⼩程序加密算法解析
初⼊道途
抓包分析
⼯具
charles -⽹络抓包
(前提:⼿机和电脑均安装好charles证书)
postman -接⼝调试⼯具
⽀持导⼊cURL,便捷⾼效,导⼊操作如下图
RE⽂件管理器 -android⽂件导出⼯具(需要root权限)
运⾏环境
华为p9 android 6.0
(android7.0以上版本抓包⼯具默认抓不到https请求,因为7.0以上只信任系统级别证书,⽽charles证书是安装到⽤户级⽬录的。
解决⽅式:可将charles证书升级为系统证书,即安装证书到系统证书⽬录下。
抓包接⼝分析
抓取通过经纬度获取门店的接⼝
⼿机上操作该⼩程序,到可以进⾏重新定位的地⽅点击来触发请求以获取附近的门店,随后charles捕捉到相关接⼝请求
选中相关请求右键复制其cURL格式数据,导⼊到postman进⾏调试分析
cURL数据分析:
观察发现是个post请求,请求体是URL编码后的,不易阅读,我们进⾏url解码
(注意这⾥获取的cURL接⼝数据和图例所⽰的不是同⼀个请求,图例所⽰的抓包接⼝被笔者不⼩⼼清除了,于是重新抓了⼀次请求~)
curl -H 'Host: yx.feiniu' -H 'content-type: application/x-www-form-urlencoded' -H 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.7(0x180如下为url解码后的cURL接⼝数据,这下好看多了~
curl -H 'Host: yx.feiniu' -H 'content-type: application/x-www-form-urlencoded' -H 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.7(0x180观察可知有data、h5、paramsMD5三个参数,整理如下:
data: {"apiVersion":"t141","appVersion":"1.5.1","areaCode":"CS000016","channel":"online","clientid":"a7ea53059fc868e2e3e2dd7c04027035","device_id":"tv179yrhs3kv9RXjJv6uJNmdkN6kTbmaUHQE","time":1626080760465,"reRule":"4","token": h5: yx_touch
paramsMD5: iOWz8O+xL9r9GX4k5Te/2U5HGTRk1GQ6YqLnMErWrAI=
⼿机重复操作,经多次调⽤抓包该接⼝后对⽐发现:
h5这个值是固定的yx_touch
paramsMD5通过字⾯意思判断为加密参数,但其数据格式不像MD5,猜测是⽤了MD5后⼜进⾏了其他的编码加密
观察可知获取门店要传⼊的经纬度⼊参也是加密的,正常来说经纬度均是数字
{"longitude":"MTIwLjE1NDc3NQ==","latitude":"MzAuMzA1ODIy"}
破解⽬标
1. paramsMD5加密逻辑
2. 经纬度加密逻辑
初领妙道
逆向之旅
获取⼩程序包wxapkg
所需⼯具
前述提到的RE⽂件管理器app
⼩程序主⼦包判断依据
如今⼩程序单包体积不能超过4M(⼩程序基础依赖包除外),如果项⽬内容过⼤,开发者会使⽤分包模式
拿下图举例来说(下图所⽰⼩程序包是其他应⽤的,⾮本⽂要分析的case)
_2124598774_821.wxapkg 3.3M 主包
_-588782754_76.wxapkg 1.5M ⼦包
_152740959_13.wxapkg 89k ⼦包
_1123949441_552.wxapkg 14M 基础依赖包
操作
打开⼩程序⼀顿操作后,会在⼩程序包存放⽬录下⾃动下载⽣成对应的包
通过re⽂件管理器直捣⼩程序包路径:
/data/t.mm/MicroMsg/"$⽤户MD5"/appbrand/pkg/_*_xxx.wxapkg
通过re⽂件管理器打成zip包发送到个⼈钉钉或者QQ、等,电脑完成⽂件接收
提⽰:若在之前打开过多个⼩程序,可以先进⼊⽬录全部删除,这样好区分⼩程序包的归属
反编译
⼯具
wxUnpacker
原理
笔者太菜,看的不太懂~
具体命令
# 主包反编译
node wxWxapkg.js ../../wxapkg/xxxx/_-2094256841_77.wxapkg
# ⼦包反编译
node wxWxapkg.js -s=/Users/toretto/crack/wxapkg/xxxx/_-2094256841_77 ../../wxapkg/xxxx/_571009734_77.wxapkg
....
#部分⼦包反编译可能会报错,但没关系,不影响后续的加密分析过程
渐⼊佳境
加密分析
⼯具
开发者⼯具
⽤于阅读代码,代码跳转追踪
分析环境预备
1. 导⼊反编译主包⽬录
2. 起个名字,点击测试号⽣成个AppID,创建⼩程序
静态分析
paramsMD5分析
是骡⼦是马拉出来溜溜,不是有个加密叫paramsMD5吗,全局搜索试试看:
js获取json的key和value
1. 关键位置定位
好嘛,定位到2处代码,直觉告诉我选request.js中的,直接定位到⼀个函数getHmacSha256(n)。
再看看这个data结构,含data、h5、paramMD5,和之前接⼝分析的结论⼀致,通⽤格式没的说~
2. getHmacSha256(n)点进去
function getHmacsha256(e) {
var n = JSON.stringify(e) + e.isSimulator + e.viewSize + eworkType + e.time, t = _vironment === _config.ENVIRONMENTS.BETA ? "@yx789*&^DKJ##CC" : "@653yx#*^&HrTy99";
console.log("request.js@32 n: " + n);
return _encBase2.default.stringify((0, _hmacSha2.default)(n, t));
}
变量n:
通过JSON.stringify(e)可以猜测,参数e是接⼝数据中的data,为json对象。n的逻辑基本上可以锁定是data本⾝的字符串再拼接上data的⼏个key对应的value值
变量t:
三⽬运算表达式,为true时貌似表⽰是BETA版本运⾏,那么正常使⽤的版本应该是false,所以猜测t="@653yx#*^&HrTy99",是⼀个固定盐值。
将前述接⼝分析中的参数data进⾏复制,粘贴进来改⼀下代码再加⼀个打印语句执⾏调试看看结果:
var a = {"apiVersion":"t141","appVersion":"1.5.1","areaCode":"CS000016","channel":"online","clientid":"a7ea53059fc868e2e3e2dd7c04027035","device_id":"tv179yrhs3kv9RXjJv6uJNmdkN6kTbmaUHQE","time":1626080760465,"reRule":"4","token function getHmacsha256(e) {
// var n = JSON.stringify(e) + e.isSimulator + e.viewSize + eworkType + e.time, t = _vironment === _config.ENVIRONMENTS.BETA ? "@yx789*&^DKJ##CC" : "@653yx#*^&HrTy99";
var n = JSON.stringify(e) + e.isSimulator + e.viewSize + eworkType + e.time, t = "@653yx#*^&HrTy99";
console.log("request.js@32 n: " + n);
return _encBase2.default.stringify((0, _hmacSha2.default)(n, t));
}
console.log(getHmacsha256(a));
#运⾏
node request.js
# 运⾏结果报错:
regeneratorRuntime is not defined 在 comment.js中
⼩程序使⽤async出现regeneratorRuntime is not defined错误说是少个依赖库,下载之
#⽣成package.json
npm init
npm install regenerator@0.13.1
# 将所缺⽂缺runtime.js移动到项⽬中
cd node_modules/regenerator-runtime/
#与common.js同⽬录
cp runtime.js /Users/toretto/crack/wxapkg/darunfa/_-2094256841_77/service/
修改common.js代码引⼊该包:
// 最上⽅添加
import regeneratorRuntime from './runtime.js'
#再次运⾏
node request.js
#还报错:
can not import modules from outside
#不能从外部导⼊⽂件,没有js基础的我盲猜可能是⼩程序⽆此语法(因为代码全局搜索import关键字后没有任何匹配项)。
解决思路
我看到⼩程序代码中有这样的⽚段:
var _wepy = require("./../npm/wepy/lib/wepy.js"),
看起来就是引⼊库的⽅式,于是我学了下写了这样⼀段:
var regeneratorRuntime = require('./runtime.js');
再次运⾏:
node reuqest.js
iOWz8O+xL9r9GX4k5Te/2U5HGTRk1GQ6YqLnMErWrAI=
//呵,可以运⾏了,照猫画虎成功~
//打印输出的加密和接⼝获取的⼀致
继续分析它的⽣成逻辑,追到相关代码,添加打印语句:
f.Base64 = {
stringify: function(r) {
console.log("\nenc-base64@21 function(r):" + r);
console.log("\nenc-base64@22 rObj:" + JSON.stringify(r));
var e = r.words, t = r.sigBytes, o = this._map;
r.clamp();
for (var n = [], f = 0; f < t; f += 3) for (var i = e[f >>> 2] >>> 24 - f % 4 * 8 & 255, a = e[f + 1 >>> 2] >>> 24 - (f + 1) % 4 * 8 & 255, c = e[f + 2 >>> 2] >>> 24 - (f + 2) % 4 * 8 & 255, p = i << 16 | a << 8 | c, s = 0; s < 4 && f + .75 * s < var u = o.charAt(64);
if (u) for (;n.length % 4; ) n.push(u);
const ret = n.join("");
console.log('\nenc-base64.js@27 n.join(""): ' + ret);
return ret;
}
上述r是个base64对象,且⽤到了它的words和sigBytes两个属性:
toString(r):
88e5b3f0efb12fdafd197e24e537bfd94e47193464d4643a62a2e7304ad6ac02
rObj:
{"words":[-1998212112,-273600550,-48660956,-449331239,1313282356,1691640890,1654843184,1255582722],"sigBytes":32}
继续分析r的⽣成逻辑, 添加⼏⾏打印语句:
_createHmacHelper: function(t) {
return function(n, e) {
console.log("libs/core.js@155 function(n, e) n:" + n);
console.log("libs/core.js@156 function(n, e) e:" + e);
console.log("libs/core.js@156 t:" + JSON.stringify(t));
const ret = new h.HMAC.init(t, e).finalize(n);
console.log("\nlibs/core.js@159 h.HMAC.init(t, e).finalize(n): " + JSON.stringify(ret))
return ret;
};
}
加了⼏句打印语句执⾏看看:
/
/参数n:
n:{"apiVersion":"t141","appVersion":"1.5.1","areaCode":"CS000016","channel":"online","clientid":"a7ea53059fc868e2e3e2dd7c04027035","device_id":"tv179yrhs3kv9RXjJv6uJNmdkN6kTbmaUHQE","time":1626080760465,"reRule":"4","token":"7ae //参数e:
e:
@653yx#*^&HrTy99
很明显了,是⽤“@653yx#*^&HrTy99”作为key种⼦初始化加密对象,然后将拼接的字符串n传⼊进⾏加密
百科了⼀下,该⽅法背后调⽤了著名的加密Hmac-Sha256
看来关于密码学笔者也需要系统地学⼀学~
元神初具
加密翻译
前端能加密,后端⼀定有对应的解密。梳理⼀下上述分析的加密逻辑后,⽤java或者python写个测试demo验证⼀下
//执⾏⼀下发现与前述js源码执⾏的结果⼀致:
88e5b3f0efb12fdafd197e24e537bfd94e47193464d4643a62a2e7304ad6ac02
iOWz8O+xL9r9GX4k5Te/2U5HGTRk1GQ6YqLnMErWrAI=
另⼀种思路
混淆代码阅读性差,且代码量也繁杂,实现加密翻译或许有点吃⼒;那么我们转换思路,由“破译”转为“利⽤”
重新梳理⼀下上述加密流程,将涉及加密的代码整理出来,拷贝到⼀个js⽂件作为⼀个⼯具库来拿到最后的加密结果:
具体过程:
1. 代码中搜索hmac-sha256,发现它来⾃crypto-js⽂件
# 下载
npm install crypto-js
# 复制该库
cp 'node_modules/crypto-js/crypto-js' crypto-js
2. 分析⼩程序中加密后的处理代码,在新的crypto-js⽂件中最后⾯添加下⾯这段逻辑
function stringify (r) {
var e = r.words, t = r.sigBytes, o = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
r.clamp();
for (var n = [], f = 0; f < t; f += 3) for (var i = e[f >>> 2] >>> 24 - f % 4 * 8 & 255, a = e[f + 1 >>> 2] >>> 24 - (f + 1) % 4 * 8 & 255, c = e[f + 2 >>> 2] >>> 24 - (f + 2) % 4 * 8 & 255, p = i << 16 | a << 8 | c, s
= 0; s < 4 && f + .75 * s < t; s++)
var u = o.charAt(64);
if (u) for (;n.length % 4; ) n.push(u);
//const常量不能⽤ Expected an operand but found const
// console.log("\nenc-base64.js@40 n.join(\"\"): " + ret);
return n.join("");
}
function getSignStr (str) {
var hash = CryptoJS.HmacSHA256(str, key);
// let hashInHex= Hex.stringify(hash); //base64_str
return stringify(hash);
}
拿接⼝中的data数据测试⼀下,没问题的话这个⽂件就可以作为获取加密签名的⼯具库了。适合前端开发没有后端基础的同学使⽤。
当然了后端同学⽤java或其他语⾔实现加密翻译较为吃⼒的话,也可以直接使⽤此js⽂件,下⾯说下思路:
1. crypto-js⽂件放到resource/js/下
2. 实现java串通js脚本的接⼝:
public interface JavaScriptInterface {
public String getSignStr(String str);
}
3. 功能测试
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.FileReader;
public class ExecuteScript {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = EngineByName("js");
String key = "{\"apiVersion\":\"t141\",\"appVersion\":\"1.5.1\",\"areaCode\":\"CS000016\",\"channel\":\"online\",\"clientid\":\"a7ea53059fc868e2e3e2dd7c04027035\",\"device_id\":\"tv179yrhs3kv9RXjJv6uJNmdkN6kTbmaUHQE\",\"time\":1626 try {
String path = Thread.currentThread().getContextClassLoader().getResource("").getPath(); // 获取targe路径
System.out.println(path);
// FileReader的参数为所要执⾏的js⽂件的路径
engine.eval(new FileReader(path+ "js/crypto-js.js"));
if (engine instanceof Invocable) {
Invocable invocable = (Invocable) engine;
JavaScriptInterface executeMethod = Interface(JavaScriptInterface.class);
System.out.SignStr(key));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//执⾏结果:⼀致的
iOWz8O+xL9r9GX4k5Te/2U5HGTRk1GQ6YqLnMErWrAI=
那就这样吧,也不失为⼀种解决策略;倘若不是为了爬⾍,完全复刻出java版的加密逻辑⼯作量太⼤没必要
经纬度加密分析
{"longitude":"MTIwLjE1NDc3NQ==","latitude":"MzAuMzA1ODIy"}
经纬度的加密相⽐paramsMD5来说简单太多,这个不难就不展开了,⼤体说⼀下思路:
静态分析
1. 全局所搜latitude,发现有个_base
2.default.decode(e.latitude) ⽅法
2. 观察加密结果格式及结合代码上下⽂发现有base64相关的代码,由此猜测可能是base64加密
3. 相关位置加⼏句打印语句后运⾏验证下,可以成功还原为数字,MzAuMzA1ODIy -> 30.305822,是6位⼩数位,其取值符合我国的维度取值范围,应该是正确的
4. 直接⽤java写段demo反向验证,⽤base64加密尝试下
public static void main(String[] args) {
String ret = Encoder().encodeToString("30.305822".getBytes());
System.out.println(ret);
}
#结果⼀直
MzAuMzA1ODIy
5. 结论:就是单纯的base64加密
妙领天机
⼼得:
1. 逆向需要耐⼼也需要⼤胆的猜想和假设去不断尝试,像⽂本所讲述的⼀些打印调试都是结合代码做了⼤胆的猜想后去验证得出的
2. 逆向⼯作会⽤到的很多好⽤的⼯具,平时注意多收集⼀些好⽤的⼯具或博⽂以事半功倍,本⽂所⽤到的⼯具和相关扩展知识点均贴出了链接,⽅便读者收藏~
3. 本⽂旨在分享⼀些逆向技巧和思路,本⽂所举case相关敏感已打码略去,读者不可利⽤本⽂所述内容进⾏⾮法商业获取利益,若执意带来的法律责任由读者⾃⾏承担。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论