安卓逆向分析遇到加密算法放在native层或者算法过于隐蔽还原不出来时,只能采用hook的形式来调用了。在电脑端,可以采用Frida+RPC+虚拟机来实现,不过电脑上安装安卓虚拟机实在是太吃力了,于是考虑在手机端搭建签名服务器。

接口请求通过HTTP协议来的,因此在安卓端搭建的服务器收发数据按照HTTP协议来完成就行了。

手机开java服务器 手机搭建java服务器_安全

 需要建立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)访问该接口,有能力的可以开启内网穿透。

手机开java服务器 手机搭建java服务器_手机开java服务器_02

调试信息

 

手机开java服务器 手机搭建java服务器_android_03

 局域网内接口调用结果