安卓逆向分析遇到加密算法放在native层或者算法过于隐蔽还原不出来时,只能采用hook的形式来调用了。在电脑端,可以采用Frida+RPC+虚拟机来实现,不过电脑上安装安卓虚拟机实在是太吃力了,于是考虑在手机端搭建签名服务器。
接口请求通过HTTP协议来的,因此在安卓端搭建的服务器收发数据按照HTTP协议来完成就行了。
需要建立4个类:
- SocketServer:socket服务类
- Http:http请求数据封装
- HttpUtil:http请求数据解析工具
- HttpServer:http服务类
1.SocketServer
回调基本满足一般需求了,可以自己根据需要增删。
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Enumeration;
import java.util.HashMap;
public class SocketServer {
int DEFAULT_PORT = 8888;
private boolean isRunning;
HashMap<String, Socket> socketHashMap;
ServerSocket serverSocket;
ServerInterface serverInterface;
public SocketServer() {
socketHashMap = new HashMap<>();
}
public SocketServer(int port) {
DEFAULT_PORT = port;
socketHashMap = new HashMap<>();
}
/**
* 开启端口监听
*/
public void start() {
new Thread(new Runnable() {
@Override
public void run() {
try {
serverSocket = new ServerSocket(DEFAULT_PORT);
serverInterface.onServerReady(getIP(), DEFAULT_PORT);
isRunning = true;
while (isRunning) {
Socket socket = serverSocket.accept();
socketHashMap.put(socket.toString(), socket);
serverInterface.onSessionConnect(socket.toString(), socket.getLocalAddress().getHostName());
onSession(socket);
}
} catch (Exception e) {
serverInterface.onServerClose(e.getLocalizedMessage());
isRunning = false;
}
}
}).start();
}
/**
* 对指定会话发送信息,发送完成后自动关闭该会话
*
* @param session 会话ID
* @param message 发送的内容
*/
public void send(String session, String message) {
Socket socket = socketHashMap.get(session);
if (socket != null && socket.isConnected()) {
try {
OutputStream outputStream = socket.getOutputStream();
outputStream.write(message.getBytes());
outputStream.flush();
closeSession(session);
} catch (IOException e) {
closeSession(session);
e.printStackTrace();
}
}
}
/**
* 关闭指定的会话
*
* @param session 会话
*/
public void closeSession(String session) {
Socket socket = socketHashMap.get(session);
if (socket != null) {
try {
socket.close();
socketHashMap.remove(session);
} catch (Exception e) {
socketHashMap.remove(session);
e.printStackTrace();
}
serverInterface.onSessionFinish(session);
}
}
/**
* 处理新会话
*
* @param socket 会话的套接字
*/
private void onSession(Socket socket) {
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024 * 3];
int len = inputStream.read(bytes);
String message = new String(bytes, 0, len).trim();
serverInterface.onSessionMessage(socket.toString(), message);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
/**
* @return 本机的局域网IP
*/
private static String getIP() {
try {
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
NetworkInterface inf = en.nextElement();
for (Enumeration<InetAddress> enumIpAdder = inf.getInetAddresses(); enumIpAdder.hasMoreElements(); ) {
InetAddress inetAddress = enumIpAdder.nextElement();
if (!inetAddress.isLoopbackAddress() && (inetAddress instanceof Inet4Address)) {
return inetAddress.getHostAddress();
}
}
}
} catch (Exception ex) {
return null;
}
return null;
}
/**
* 设置回调接口
*
* @param serverInterface 回调接口
*/
public void setServerInterface(ServerInterface serverInterface) {
this.serverInterface = serverInterface;
}
public interface ServerInterface {
void onServerReady(String ip, int port);
void onServerClose(String reason);
void onSessionConnect(String session, String ip);
void onSessionMessage(String session, String message);
void onSessionFinish(String session);
}
}
2.Http
由于自己使用的签名接口格式都是http://127.0.0.1:8888/sig?str=xxxx,所以封装url和param就够了,请求方法和请求body可以不管
public class Http {
String method;
String url;
String urlParam;
String body;
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUrlParam() {
return urlParam;
}
public void setUrlParam(String urlParam) {
this.urlParam = urlParam;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}
3.HttpUtil
对请求数据按照HTTP协议的格式来解析,这里只解析出url和param,请求方法和请求body不理了,还有就是响应数据也得按照HTTP格式来,不然客户端会请求错误。
import java.net.URLDecoder;
public class HttpUtil {
public static Http parseRequest(String request) {
Http http = new Http();
if (isHttp(request)) {
http.setMethod(getMethod(request));
http.setUrl(getUrl(request));
http.setUrlParam(getUrlParam(request));
}
return http;
}
/**
* 简单判断请求数据是否符合HTTP协议格式
* @param request 请求的数据
* @return
*/
public static boolean isHttp(String request) {
if (request.isEmpty()) {
return false;
}
String header = request.split("\n", 2)[0];
String[] sp = header.split(" ");
return sp.length == 3 && header.contains("HTTP");
}
public static String getMethod(String request) {
if (request.isEmpty()) {
return "";
}
String[] lines = request.split("\n", 1);
if (lines[0].contains("GET ")) {
return "GET";
} else if (lines[0].contains("POST ")) {
return "POST";
} else {
return "";
}
}
public static String getUrl(String request) {
if (request.isEmpty()) {
return "";
}
String header = request.split("\n", 1)[0];
String r = header.split(" ")[1];
if (r.contains("/?")) {
return r.split("/\\?")[0];
} else if (r.contains("?")) {
return r.split("\\?")[0];
} else {
return r;
}
}
public static String getUrlParam(String request) {
if (request.isEmpty()) {
return "";
}
String header = request.split("\n", 1)[0];
String r = header.split(" ")[1];
if (r.contains("?") && r.contains("=")) {
String params = r.split("\\?")[1];
return URLDecoder.decode(params.split("=")[1]);
} else {
return "";
}
}
/**
* 构建响应数据
* @param message 响应内容
* @param code 响应码(200,404,500就够了)
* @return
*/
public static String buildResponse(String message, int code) {
String response = "HTTP/1.1 " + code + "\n";
response += "Content-Type: text\n";
response += "Content-Length: " + message.getBytes().length + "\n";
response += "\n" + message;
return response;
}
}
4.HttpServer
服务器封装类,在此可以按需设置监听的端口,对请求的url进行判断,调用自己的签名函数
public class HttpServer {
String url_game_sig = "/game/sig";
String url_game_encrypt = "/game/encrypt";
SocketServer server;
boolean onServer = false;
public HttpServer() {
startServer();
}
public void startServer() {
server = new SocketServer();
server.setServerInterface(new SocketServer.ServerInterface() {
@Override
public void onServerReady(String ip, int port) {
LogUtil.log("onServerReady:" + ip + ":" + port);
onServer = true;
}
@Override
public void onServerClose(String reason) {
LogUtil.log("onServerClose:" + reason);
onServer = false;
}
@Override
public void onSessionConnect(String session, String ip) {
LogUtil.log("onSessionConnect:" + ip);
}
@Override
public void onSessionMessage(String session, String message) {
handleRequest(session, message);
}
@Override
public void onSessionFinish(String session) {
LogUtil.log("onSessionFinish");
}
});
//开启监听
server.start();
}
/**
* 处理请求
*
* @param session 会话ID
* @param message 请求内容
*/
public void handleRequest(String session, String message) {
//将字符串按照http协议解析成对象
Http http = HttpUtil.parseRequest(message);
//响应的字符串(http协议格式)
String response;
//根据请求的接口响应相应数据
if (http.getUrl().equals(url_ks)) {
if (http.getUrlParam() != null && !http.getUrlParam().isEmpty()) {
response = HttpUtil.buildResponse(SignHelper.getKsSig3(http.getUrlParam()), 200);
} else {
response = HttpUtil.buildResponse("", 402);
}
} else if (http.getUrl().equals(url_game_sig)) {
if (http.getUrlParam() != null && !http.getUrlParam().isEmpty()) {
response = HttpUtil.buildResponse(SignHelper.getGameSig(http.getUrlParam()), 200);
} else {
response = HttpUtil.buildResponse("", 402);
}
} else if (http.getUrl().equals(url_game_encrypt)) {
if (http.getUrlParam() != null && !http.getUrlParam().isEmpty()) {
response = HttpUtil.buildResponse(SignHelper.getGameEncrypt(http.getUrlParam()), 200);
} else {
response = HttpUtil.buildResponse("", 402);
}
} else {
response = HttpUtil.buildResponse("", 404);
}
//发送数据
server.send(session, response);
}
/**
* @return http服务是否在运行
*/
public boolean isOnServer() {
return onServer;
}
}
使用方法很简单,在需要开启的地方(一般在hook到签名函数后):
HttpServer httpServer = new HttpServer();
因为hook常常会被调用几次,建议将类设为静态变量或在开启前调用isOnServer()方法判断下服务是否已经开启了,重复开启可以正常运行,但会提示端口已被占用。服务开启后,可以在局域网内(同一WIFi)访问该接口,有能力的可以开启内网穿透。
调试信息
局域网内接口调用结果