知苗已更换加密算法,此加密算法已失效
前言
不多说,舔狗,帮抢,写脚本,懂得都懂。
本篇文章仅为记录学习过程,不提供源码。
准备工作
抓包获取接口
通过Fiddler工具抓包小程序获得整个抢购流程所需要的接口:
- 获取医院列表接口
https://cloud.cn2030.com/sc/wx/HandlerSubscribe.ashx?act=CustomerList
- 查询医院疫苗列表接口
https://cloud.cn2030.com/sc/wx/HandlerSubscribe.ashx?act=CustomerProduct
- 查询疫苗可用日期接口
https://cloud.cn2030.com/sc/wx/HandlerSubscribe.ashx?act=GetCustSubscribeDateAll
- 查询疫苗所选日期可用时间接口
https://cloud.cn2030.com/sc/wx/HandlerSubscribe.ashx?act=GetCustSubscribeDateDetail
- 抢购提交前验证接口
https://cloud.cn2030.com/sc/wx/HandlerSubscribe.ashx?act=GetCaptcha
- 保存订单接口
https://cloud.cn2030.com/sc/api/User/OrderPost
接口地址和接口格式都拿到了,但是我们发现,在每个请求中都带有zftsl
参数,并且在时间接口中的返回结果是加密的。
初步看可以知道,zftsl
大概是一个和md5加密相关的东西,接口加密的方法大概是AES加密,那我们就需要获取加密方式、偏移量和秘钥等数据才能够继续往下操作。因此我们选择反编译小程序。
反编译小程序获取加密方式
之前有发过一期反编译小程序的教程,本期只讲思路不提供工具。
获取知苗易约程序包
我们知道,小程序的本质是一个h5前端,通过开发者工具打包后在微信的浏览器内核展示,因此我们首先需要获取到小程序的包。
打开模拟器,登录微信,打开小程序。
打开文件管理器,进入/data/data/com.tencent.mm/MicroMsg/a077f02aa58ebb02952e923f4c901697/appbrand/pkg
目录,找到我们的包。
反编译包获得压缩后的源码
下载wxappUnpacker
反编译工具,npm install
导入所需要的包:
npm install esprima
npm install css-tree
npm install cssbeautify
npm install vm2
npm install uglify-es
npm install js-beautify
运行wuWxapkg.js
脚本反编译程序:node wuWxapkg.js ~/Documents/1.wxapkg
获得源码。
阅读源码寻找算法和秘钥
将源码导入微信开发者工具。关闭不校验合法域名
选项,让程序成功运行,便于调试。
我们发现程序需要获取位置信息,那我们首先在百度坐标拾取系统中获取位置的经纬度,然后在小程序的Sensor
选项中设置经纬度,并在app.json
中定义相关权限。
设置好相关配置后,不出意外,我们的程序便可以成功开始运行。
获取zftsl
算法
全局搜索zftsl
关键字,我们发现其就是一个以时间戳为种子的MD5字符串,那这个字段的目的应该是为了方式抓包软件重复发送相同数据包。那我们就不多写,直接上Java实现:
DigestUtils.md5Hex(("zfsw_" + (Calendar.getInstance().getTimeInMillis() / 10)).getBytes(StandardCharsets.UTF_8))
获取AES加密算法方式及秘钥
根据代码,的确证实了我们的猜想,这是一个AES加密算法,其中:
- 加密模式是
CBC
- 填充方式是
PKcs7
- iv偏移量是:
1234567890000000
但是秘钥我们没有找到直接的字符串,但是经过对变量的不断溯源(过程不展示了,太枯燥)我们知道秘钥和后端返回的Cookies有关联,那么应该是一个AES动态加密算法,从Cookies中获取的秘钥,我们打开抓包软件,抓取程序的Cookies:
ASP.NET_SessionId=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDQyMTY4MTEuNDE0MTgwNSwiZXhwIjoxNjQ0MjIwNDExLjQxNDE4MDUsInN1YiI6IllOVy5WSVAiLCJqdGkiOiIyMDIyMDIwNzAyNTMzMSIsInZhbCI6Iis4TWRBUUlBQUFBUU5HTTFNbU00TURJM1pEZzBabVV5WlJ4dmNYSTFielZEZFRBNVQzcENiV3RZU0ZoMVdVeE5TSHBzWTNoRkFCeHZcclxuVlRJMldIUXdhbGd6VDAxeFZEbHljRXd4UlRCbWFTMWliSFpOQ3pJM0xqTTRMalV5TGpFMEFBQUFBQUFBQUE9PSJ9.ajMIjCAa6LYvL-V7Lc0pgq50i3j9MyqyP0-IwVg8uSs
前面的ASP.NET_SessionId=
只是一个.net
框架Session管理的一个key前缀,应该和秘钥没什么关系。那我们去掉前缀后发现,所返回的Cookies是一串JWT秘钥,我们先对该秘钥进行解密:
我们在负载中获得一个对象,其中val为加密部分。但是我们发现其加密方法应该是Base64,因此,我们对val进行再次base64解密:
发现其中4c52c8027d84fe2e
正好是16位的,其格式也与秘钥十分相似。因此我们将其作为秘钥进行测试:
发现成功解密!
编写代码
编写工具类
AES解密工具
/**
* @author xin.liu
*/
public class AesService {
private Cipher decryptCipher;
private Cipher encryptCipher;
private static volatile AesService INSTANCE;
private AesService(String keyStr, String ivStr){
try{
byte[] keyBytes = keyStr.getBytes();
int base = 16;
if (keyBytes.length % base != 0) {
int groups = keyBytes.length / base + 1;
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyBytes, 0, temp, 0, keyBytes.length);
keyBytes = temp;
}
Security.addProvider(new BouncyCastleProvider());
Key key = new SecretKeySpec(keyBytes, "AES");
encryptCipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
decryptCipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
encryptCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(ivStr.getBytes()));
decryptCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivStr.getBytes()));
}catch (Exception e){
e.printStackTrace();
}
INSTANCE = null;
}
public static AesService getInstance(String keyStr, String ivStr) {
if (INSTANCE == null){
synchronized (AesService.class){
if (INSTANCE == null){
INSTANCE = new AesService(keyStr, ivStr);
}
}
}
return INSTANCE;
}
/**
* 解密方法
* @param content 要解密的字符串
*/
public String decrypt(String content){
try{
byte[] encryptedText = decryptCipher.doFinal(Hex.decode(content.getBytes()));
return new String(encryptedText);
}catch (Exception e){
e.printStackTrace();
return null;
}
}
/**
* 加密方法
* @param content 要加密的字符串
*/
public String encrypt(String content) {
try{
byte[] encryptedText = encryptCipher.doFinal(content.getBytes());
return new String(Hex.encode(encryptedText)).toUpperCase();
}catch (Exception e){
e.printStackTrace();
return null;
}
}
}
Base64 解密工具
public class Base64Utils {
private static final BASE64Decoder DECODER = new BASE64Decoder();
public static String decode(String base64) {
try{
return new String(DECODER.decodeBuffer(base64), StandardCharsets.UTF_8);
}catch (Exception e){
return null;
}
}
}
Jwt解密工具
可以使用jwt的依赖,也可以直接对jwt令牌进行base64解密:
/**
* @author xin.liu
*/
public class JwtUtils {
/**
* 解析JWT 负荷
* @param token 待解析的jwt
* @param key 需要获取的负荷 key
* @return 解析结果
*/
public static String getTokenPayload(String token, String key) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(key).asString();
}
}
请求工具类
这里我们可以使用HttpClient工具类进行,但是我发现使用工具类时创建隧道连接时会消耗大量的时间,尤其是在抢购的时候,因此最终我选择了使用原生http请求工具:
因为在抢购的过程中,需要进行验证,所以一个抢购会话需要维持Cookies,我们因此没有使用静态方法或者单例模式
/**
* HTTP 请求服务
* @author xin.liu
*/
public class HttpRequestService {
private String cookies = null;
public JSONObject doPost(String url, String body){
try{
return JSONObject.parseObject(proxyHttpRequest(url, "POST", body));
}catch (Exception e){
e.printStackTrace();
return null;
}
}
public JSONObject doGet(String url){
try{
return JSONObject.parseObject(doGetString(url));
}catch (Exception e){
e.printStackTrace();
return null;
}
}
public String doGetString(String url){
return proxyHttpRequest(url, "GET", null);
}
private synchronized HttpURLConnection createConnection(String urlAddress, String method, String body) throws Exception {
URL url = new URL(urlAddress);
trustAllHttpsCertificates();
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setConnectTimeout(KillKey.TIMEOUT);
Map<String, String> headerParameters = UserConfig.getRequestHeaderMaps();
if (headerParameters != null) {
for (String key : headerParameters.keySet()) {
httpConnection.setRequestProperty(key, headerParameters.get(key));
}
}
if (StringUtils.isNotBlank(cookies)){
httpConnection.setRequestProperty("Cookie", cookies);
}
httpConnection.setRequestProperty("zftsl", DigestUtils.md5Hex(("zfsw_" + (Calendar.getInstance().getTimeInMillis() / 10)).getBytes(StandardCharsets.UTF_8)));
httpConnection.setRequestMethod(method);
httpConnection.setDoOutput(true);
httpConnection.setDoInput(true);
if (!(body == null || "".equals(body.trim()))) {
OutputStream writer = httpConnection.getOutputStream();
try {
writer.write(body.getBytes(KillKey.ENCODING));
} finally {
if (writer != null) {
writer.flush();
writer.close();
}
}
}
int responseCode = httpConnection.getResponseCode();
String cookies = httpConnection.getHeaderField("Set-Cookie");
if (StringUtils.isNotBlank(cookies)){
this.cookies = cookies;
}
if (responseCode != KillKey.HTTP_STATUS_OK) {
throw new Exception(responseCode + ":" + inputStream2String(httpConnection.getErrorStream(), KillKey.ENCODING));
}
return httpConnection;
}
public String proxyHttpRequest(String address, String method, String body) {
String result = null;
HttpURLConnection httpConnection = null;
try {
httpConnection = createConnection(address, method, body);
String encoding = "UTF-8";
if (httpConnection.getContentType() != null && httpConnection.getContentType().contains(KillKey.CHARSET)) {
encoding = httpConnection.getContentType().substring(httpConnection.getContentType().indexOf(KillKey.CHARSET) + 8);
}
result = inputStream2String(httpConnection.getInputStream(), encoding);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (httpConnection != null) {
httpConnection.disconnect();
}
}
return result;
}
private String inputStream2String(InputStream input, String encoding) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(input, encoding));
StringBuilder result = new StringBuilder();
String temp;
while ((temp = reader.readLine()) != null) {
result.append(temp);
}
return result.toString();
}
private void trustAllHttpsCertificates() throws Exception {
HttpsURLConnection.setDefaultHostnameVerifier((str, session) -> true);
javax.net.ssl.TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[1];
javax.net.ssl.TrustManager tm = new MiTm();
trustAllCerts[0] = tm;
javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, null);
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
static class MiTm implements javax.net.ssl.TrustManager,javax.net.ssl.X509TrustManager {
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; }
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { }
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { }
}
}
完整源码因为侵权的问题暂时不提供。本篇文章仅为记录学习过程中遇到的问题,仅供学习交流。
如果有疑问,欢迎在文章下评论。