目标:得到”Encrypt”的算法

逆向的步骤

  • 抓包分析是否有需要逆向的字段

  • 查壳分析是否有加固

  • 查看界面元素,看是否是原生开发的app,以为不同形式app分析方法不一样

  • 跑一下自吐算法插件

  • Hook常用系统函数来定位

  • 找到一些疑似的函数,可以利用hook打印堆栈,来看是否调用了这些函数

  • 关键加密代码的定位

    人肉手工搜索字符串

    ​ 可以搜索连接

    ​ 可以搜索加密的函数

    ​ 可以搜索同一个数据包中没有加密的参数名

使用查壳工具发现没有壳,抓包发现有一个加密字段:

"Encrypt": "NIszaqFPos1vd0pFqKlB42Np5itPxaNH\/\/FDsRnlBfgL4lcVxjXii\/UNcdXYMk0EQMz15fWd0AZ3\n9d4qc0ZgtMCbKF+l+FsXvqFFi4HhQ8Z56cxaZpXIUEhJ5R9IBRCtI97HMUH8l2PmJT4N+mA\/k2lT\nUTN+8LjHbtYGerEukUyVhjQHzeMCIEFbLVavcACyOG7MZVeYVISwA3hNO+DU7\/YXdymd3vpf\n"

FridaHook辅助算法分析

找到一些疑似关键函数,可以通过hook来确认app执行某个操作的时候,是否调用了它们

如果没有触发这些函数,可以考虑以下问题

  • app在执行这个操作的时候,真的没有调用这个函数,换一个思路找关键函数
  • 代码写错了,导致hook函数没执行
  • 一般可以通过主动调用上层函数,来触发这些hook函数

如果触发了这些函数,可以通过hook来打印执行过程中传入函数的参数和返回值

利用以后得到的信息,来进行算法复现

完整分析步骤

抓包

发现有一个Encrypt字段

{
"Encrypt": "NIszaqFPos1vd0pFqKlB42Np5itPxaNH\/\/FDsRnlBfgL4lcVxjXii\/UNcdXYMk0EbKdY0NLSwwFL\nLJfVtM8N6zAdGdzs2bHOVQO\/4ArQGwKPMxbQ\/tOgKvxvHpbTLSTjEt7U0l9Ejuc3Yt1kW3s3tjk8\nn40vRguUqAZ1ygd+5+uuFtuqJfUg0hD6vG4IGR8w6\/ZVWkq2yZSM3KcKxIQurlc98XMQ+ete\n"
}

\代表着转义字符

下面我们便来对生成Encrypt的算法进行复现

Hook分析Encrypt算法

首先直接人肉搜索以下Encrypt字段

搜索到了两个与包名相关的方法,一般与包名相关的我们都需要重点关注,查看这两个函数

hook这两个函数,看看app运行时走了哪一个

hook脚本:

Java.perform(function () {
var JsonRequest = Java.use("com.dodonew.online.http.JsonRequest");
JsonRequest.addRequestMap.overload('java.util.Map', 'int').implementation = function (a, b){
console.log("addRequestMap is called");
this.addRequestMap(a, b);
}
JsonRequest.paraMap.implementation = function(a){
console.log("paraMap is called");
this.paraMap(a);
}

});

跑一下脚本发现走了addRequest函数

那么接下来我们自然要进一步分析这个函数

分析addRequest函数

可以看到

String encrypt = RequestUtil.encodeDesMap(code, this.desKey, this.desIV);

说明encrypt是由RequestUtil类的encodeDesMap产生的

hook以下这个函数看看它的参数与结果

Java.perform(function () {
var JsonRequest = Java.use("com.dodonew.online.http.JsonRequest");
JsonRequest.addRequestMap.overload('java.util.Map', 'int').implementation = function (a, b){
console.log("addRequestMap is called");
this.addRequestMap(a, b);
console.log("============================================================================================");
}
var RequestUtil = Java.use("com.dodonew.online.http.RequestUtil");
RequestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(a, b, c){
console.log("encodeDesMap is called");
console.log("data: ", a);
console.log("desKey: ", b);
console.log("desIV: ", c);
let result = this.encodeDesMap(a, b, c);
console.log("result: ", result);
console.log("============================================================================================");
return result;
}
});

可以看到这个函数的返回值就是我们的encrypt

要进行算法复现发现还有一个sign是未知的值,看到它是在paraMap函数中经过md5加密产生的

自然要hook这个函数一探究竟

Java.perform(function () {
var JsonRequest = Java.use("com.dodonew.online.http.JsonRequest");
JsonRequest.addRequestMap.overload('java.util.Map', 'int').implementation = function (a, b){
console.log("addRequestMap is called");
this.addRequestMap(a, b);
console.log("============================================================================================");
}
var RequestUtil = Java.use("com.dodonew.online.http.RequestUtil");
RequestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(a, b, c){
console.log("encodeDesMap is called");
console.log("data: ", a);
console.log("desKey: ", b);
console.log("desIV: ", c);
let result = this.encodeDesMap(a, b, c);
console.log("result: ", result);
console.log("============================================================================================");
return result;
}
var Utils = Java.use("com.dodonew.online.util.Utils");
Utils.md5.implementation = function (a){
console.log("md5 is called");
console.log("string: ", a);
let result = this.md5(a);
console.log("result: ", result);
console.log("============================================================================================");
return result;
}
});

可以看到我们的sign值得产生方法,就是将传入得参数经过md5加密得到的

接下来我们仔细看看encodeMap的加密函数

可以看到在这个des中将我们传入的密钥进行了一个md5加密,至此已经分析明了

算法总结

encrypt字段的生成方法就是,先将用户输入的账号密码,时间戳以及一些固定值经过md5加密生成sign值;然后再将生成的sign与用户输入的账号密码,时间戳以及一些固定的值经过des加密得到最终答案,在des加密中又将我们传入的密钥进行了一次md5加密作为des的密钥。

算法复现

使用CryptoJS库

var CryptoJS = module.exports;
function getSign(user, pass, time){
var signStr = "equtype=ANDROID&loginImei=Androidnull&timeStamp=" + time + "&userPwd=" + pass +"&username="+ user + "&key=sdlkjsdljf0j2fsjk";
return CryptoJS.MD5(signStr).toString().toUpperCase();//这里的toString是将数字转成16进制字符串
}
function encrypt(user, pass){
var time = new Date().getTime(); //这里注意,进行算法复现时,要将时间戳固定下来才能得到一样的结果,而在进行协议复现时,要取当前的时间戳
var sign = getSign(user, pass, time);
var plainText = '{"equtype":"ANDROID","loginImei":"Androidnull","sign":"' + sign'","timeStamp":"' + time + '","userPwd":"'+ pass +'","username":"'+ time +'"}';
var key = CryptoJS.enc.Hex.parse(CryptoJS.MD5("65102933").toString());
var iv = CryptoJS.enc.UTF8.parse("32028092");
return CryptoJS.DES.encrypt(plainText, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString(); //这里的toString()被重写了,将加密后的数据进行Base64编码
}

协议复现(java)

HttpHelper.java

package org.example;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;

public class HttpHelper {

public static String HttpURLConnection(String method, String url, String outputStr) {
try {
URL u = new URL(url);
HttpURLConnection conn = (HttpURLConnection) u.openConnection(Proxy.NO_PROXY);
conn.setRequestMethod("GET");
conn.setDoInput(true);
conn.setUseCaches(false);
conn.setConnectTimeout(30000);

if(method.equals("POST")){
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
}
conn.setRequestProperty("User-Agent", "Dalvik/2.1.0 (Linux; U; Android 10; Pixel XL Build/QP1A.191005.007.A3)");

if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}

conn.connect();

InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
conn.disconnect();
return buffer.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

dodonew.java

package org.example;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;

public class dodonew {
public static void main(String[] args) throws Exception {

ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript");
engine.eval(readJSFile());
Invocable inv = (Invocable) engine;
String res = (String) inv.invokeFunction("encrypt", "13905020820", "a12345678");
System.out.println("res:" + res);

String str = HttpHelper.HttpURLConnection(
"POST",
"http://api.dodovip.com/api/user/login",
"{\"Encrypt\":\"" + res + "\"}");
System.out.println(str);
String plainText = (String) inv.invokeFunction("decrypt", str);
System.out.println(plainText);

}

private static String readJSFile() throws Exception {
StringBuffer script = new StringBuffer();
String projectPath = System.getProperty("user.dir");
File file = new File(projectPath + "\\JSDir\\demo.js");
FileReader filereader = new FileReader(file);
BufferedReader bufferreader = new BufferedReader(filereader);
String tempString = null;
while ((tempString = bufferreader.readLine()) != null) {
script.append(tempString).append("\n");
}
bufferreader.close();
filereader.close();
return script.toString();
}
}