网络通讯协议分析

概述

下述实验所基于的Android系统为Android8.1.0_r1,以传输层协议Tcp和Udp以及应用层加密协议Https为例,通过对其Java层和C层的调用链进行分析,以寻求Android系统框架层的最佳Hook点来拦截打印App收发包数据.

Tcp

服务端代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import socket
import threading

IP = '0.0.0.0'
PORT = 9999

def main():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((IP, PORT))
    server.listen(5)
    print(f'[*] Listening on {IP}:{PORT}')

    while True:
        client, address = server.accept()
        print(f'[*] Accepted connection from {address[0]}:{address[1]}')
        client_handler = threading.Thread(target=handle_client, args=(client,))
        client_handler.start()

def handle_client(client_socket):
    with client_socket as sock:
        request = sock.recv(1024)
        print(f'[*] Received: {request.decode("utf-8")}')
        sock.send(b'ACKKK')

if __name__ == '__main__':
    main()

客户端代码

  1. 在AndroidManifest.xml文件赋予权限:
1
<uses-permission android:name="android.permission.INTERNET"/>
  1. MainActivity.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.example.luotcp;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.example.luotcp.databinding.ActivityMainBinding;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;

public class MainActivity extends AppCompatActivity {
    static final String TAG = "XiaLuoHun";

    // Used to load the 'luotcp' library on application startup.
    static {
        System.loadLibrary("luotcp");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI());

        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    while (true){
                        DoTcp("10.67.16.180", 9999);
                        Thread.sleep(3000);
                    }
                }
                catch(IOException | InterruptedException e){
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public static void DoTcp(String strIP, int nPort) throws IOException, InterruptedException {
        Socket socket = new Socket(strIP, nPort);
        socket.setSoTimeout(10000);
        //发送数据给服务端
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello,server".getBytes("UTF-8"));
        Thread.sleep(2000);
        socket.shutdownOutput();
        //读取数据
        InputStream in = socket.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(in));
        String line = br.readLine();
        //打印读取到的数据
        Log.d(TAG, "tcp server recv:" + line);
        br.close();
        socket.close();
    }

    /**
     * A native method that is implemented by the 'luotcp' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}
注意
如果是在本机跑服务端代码,手机端跑客户端代码,要保证客户端能够ping通服务端IP,如果ping不通,需要关闭下PC端的防火墙.

Java层抓包

观察上述开发流程,我们发现以下几个关键函数:

  1. Socket的构造函数,包含了要连接的IP和Port

  2. 发包函数,OutputStream.write()

  3. 收包函数,BufferedReader.readLine()

跟踪Socket构造函数

1
2
3
4
5
6
7
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/Socket.java
public Socket(String host, int port)
->private Socket(InetAddress[] addresses, int port, SocketAddress localAddr,boolean stream) 

//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/InetSocketAddress.java
->public InetSocketAddress(InetAddress addr, int port)
->private InetSocketAddressHolder(String hostname, InetAddress addr, int port)

到这里就找到了Socket构造函数最终调用的地方,至此可写出Hook代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function hookSocketInit() {
    var Socket = Java.use('java.net.Socket')
    // Socket.$init.overload('java.lang.String', 'int').implementation = function (host, port) {
    //     console.log('Host:', host, 'Port:', port)
    //     return this.$init(host, port)
    // }

    // Socket.$init.overload('[Ljava.net.InetAddress;', 'int', 'java.net.SocketAddress', 'boolean').implementation = function (addresses, port, localAddr, stream) {
    //     console.log('IP:', addresses[0].toString(), 'Port:', port)
    //     return this.$init(addresses, port, localAddr, stream)
    // }

    var InetSocketAddressHolder = Java.use('java.net.InetSocketAddress$InetSocketAddressHolder')
    InetSocketAddressHolder.$init.overload('java.lang.String', 'java.net.InetAddress', 'int').implementation = function (hostname, addr, port) {
        console.log('IP:', addr.toString(), 'Port:', port)
        //console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
        return this.$init(hostname, addr, port)
    }
}

function hookTcpJava() {
    Java.perform(function () {
        hookSocketInit()
    })
}

setImmediate(hookTcpJava())

hookSocketInit

跟踪发包函数

接下来跟踪OutputStream类的write函数.

首先注意到OutputStream是一个抽象类,需要找到它的具体实现类.

OutputStream抽象类

我们既可以通过源码级调试,确定OutputStream的具体实现类为java.net.SocketOutputStream

OutputStream具体实现类

也可以利用下述Objection命令,确定OutputStream的具体实现类为java.net.SocketOutputStream

1
2
objection.exe -g com.example.luotcp explore
android hooking watch class_method java.net.Socket.getOutputStream --dump-args --dump-return --dump-backtrace

OutputStream具体实现类1

接下来跟踪SocketOutputStream类的write函数.

1
2
3
4
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/SocketOutputStream.java
public void write(byte b[])
->private void socketWrite(byte b[], int off, int len)
->private native void socketWrite0(FileDescriptor fd, byte[] b, int off,int len)

至此可写出Socket发包函数的Hook代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
function getAddrInfo(socket, isRead) {
    var src_addr = ''
    var src_port = ''
    var dst_addr = ''
    var dst_port = ''

    if (isRead) {
        src_addr = socket.getRemoteSocketAddress().toString().split(":")[0].split("/").pop()
        src_port = socket.getRemoteSocketAddress().toString().split(":").pop()
        dst_addr = socket.getLocalAddress().toString().split(":")[0].split("/").pop()
        dst_port = socket.getLocalPort().toString()
    } else {
        src_addr = socket.getLocalAddress().toString().split(":")[0].split("/").pop()
        src_port = socket.getLocalPort().toString()
        dst_addr = socket.getRemoteSocketAddress().toString().split(":")[0].split("/").pop()
        dst_port = socket.getRemoteSocketAddress().toString().split(":").pop()
    }

    return src_addr + ':' + src_port + ' --> ' + dst_addr + ':' + dst_port
}

function hookWrite() {
    var SocketOutputStream = Java.use('java.net.SocketOutputStream')
    SocketOutputStream.socketWrite0.implementation = function (fd, b, off, len) {
        //打印数据包的源和目标地址
        var socket = this.socket.value
        var msg = getAddrInfo(socket, false)
        console.log('socketWrite0', msg)

        //打印发包内容
        var bufLen = len
        var ptr = Memory.alloc(bufLen);
        for (var i = 0; i < bufLen; ++i)
            Memory.writeS8(ptr.add(i), b[off + i]);
        console.log(hexdump(ptr, {
            offset: 0,
            length: bufLen,
            header: false,
            ansi: false
        }));

        //打印调用栈
        console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

        return this.socketWrite0(fd, b, off, len)
    }
}

function hookTcpJava() {
    Java.perform(function () {
        hookWrite()
    })
}

setImmediate(hookTcpJava())

hookTcpJava_write

跟踪收包函数

接下来跟踪BufferedReader类的readLine函数.

1
2
3
4
5
6
7
8
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/io/BufferedReader.java
public String readLine()
->String readLine(boolean ignoreLF)
->private void fill(){
    //---
    in.read(cb, dst, cb.length - dst);
    //---
}

看Android源码可知上面in对应的类型为InputStream.

这里注意到InputStream是一个抽象类,需要找到它的具体实现类.

InputStream抽象类

通过源码级调试,确定InputStream的具体实现类为java.net.SocketInputStream

InputStream具体实现类

接下来跟踪SocketInputStream类的read函数.

1
2
3
4
5
6
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/SocketInputStream.java
public int read(byte b[])
->public int read(byte b[], int off, int length) 
->int read(byte b[], int off, int length, int timeout)
->private int socketRead(FileDescriptor fd,byte b[], int off, int len,int timeout)
->private native int socketRead0(FileDescriptor fd,byte b[], int off, int len,int timeout)

至此可写出Socket收包函数的Hook代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function getAddrInfo(socket, isRead) {
    var src_addr = ''
    var src_port = ''
    var dst_addr = ''
    var dst_port = ''

    if (isRead) {
        src_addr = socket.getRemoteSocketAddress().toString().split(":")[0].split("/").pop()
        src_port = socket.getRemoteSocketAddress().toString().split(":").pop()
        dst_addr = socket.getLocalAddress().toString().split(":")[0].split("/").pop()
        dst_port = socket.getLocalPort().toString()
    } else {
        src_addr = socket.getLocalAddress().toString().split(":")[0].split("/").pop()
        src_port = socket.getLocalPort().toString()
        dst_addr = socket.getRemoteSocketAddress().toString().split(":")[0].split("/").pop()
        dst_port = socket.getRemoteSocketAddress().toString().split(":").pop()
    }

    return src_addr + ':' + src_port + ' --> ' + dst_addr + ':' + dst_port
}

function hookRead() {
    var SocketInputStream = Java.use('java.net.SocketInputStream')
    SocketInputStream.socketRead0.implementation = function (fd, b, off, len, timeout) {
        var result = this.socketRead0(fd, b, off, len, timeout);
        if (result > 0) {
            //打印数据包的源和目标地址
            var socket = this.socket.value
            var msg = getAddrInfo(socket, true)
            console.log('socketRead0', msg)

            //打印收包内容
            var ptr = Memory.alloc(result);
            for (var i = 0; i < result; ++i)
                Memory.writeS8(ptr.add(i), b[off + i]);
            console.log(hexdump(ptr, {
                offset: 0,
                length: result,
                header: false,
                ansi: false
            }));

            //打印调用栈
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
        }
        return result
    }
}

function hookTcpJava() {
    Java.perform(function () {
        hookRead()
    })
}

setImmediate(hookTcpJava())

hookTcpJava_read

Jni层抓包

在上面Java层抓包中,我们找到了Tcp Java层最深处的发包函数(socketWrite0)和收包函数(socketRead0),这里我们继续沿着这两个函数跟踪C层的调用链,以寻求最佳Hook点.

跟踪发包函数

1
2
3
4
5
6
7
8
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/SocketOutputStream.java
private native void socketWrite0(FileDescriptor fd, byte[] b, int off,int len)

//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/native/SocketOutputStream.c
SocketOutputStream_socketWrite0(JNIEnv *env, jobject this,jobject fdObj,jbyteArray data,jint off, jint len)

//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/native/linux_close.cpp
->int NET_Send(int s, void *msg, int len, unsigned int flags)

NET_Send

NET_Send1

这里发现在Android源码中不太好跟踪,可通过下述命令进行搜索,查看在哪个so中,用IDA来看.

1
2
3
adb shell
su
grep -ril NET_Send /system/lib64/*

最终发现NET_Send在libopenjdk.so中,通过下述命令将其导出.

1
adb pull /system/lib64/libopenjdk.so

sendto

sendto1

可以发现TCP发包在C层最终调用的是libc库中的sendto函数,至此可写出Hook代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function getAddrInfo(sockfd, isRecv) {
    var message = {}
    var src_dst = ["src", "dst"]
    for (var i = 0; i < src_dst.length; i++) {
        if ((src_dst[i] == "src") ^ isRecv) {
            var sockAddr = Socket.localAddress(sockfd)
        } else {
            var sockAddr = Socket.peerAddress(sockfd)
        }
        if (sockAddr == null) {
            // 网络超时or其他原因可能导致socket被关闭
            message[src_dst[i] + "_port"] = 0
            message[src_dst[i] + "_addr"] = 0
        } else {
            message[src_dst[i] + "_port"] = (sockAddr.port & 0xFFFF)
            message[src_dst[i] + "_addr"] = sockAddr.ip.split(":").pop()
        }
    }
    return message['src_addr'] + ':' + message['src_port'] + ' --> ' + message['dst_addr'] + ':' + message['dst_port']
}

function hookJniSend() {
    var sendto = Module.getExportByName("libc.so", "sendto");
    Interceptor.attach(sendto, {
        onEnter: function (args) {
            //打印数据包的源和目标地址
            var sockfd = args[0].toInt32()
            var msg = getAddrInfo(sockfd, false)
            console.log('sendto', msg)

            //打印发包内容
            var buf = ptr(args[1]).readCString();
            var len = args[2].toInt32();
            console.log(hexdump(args[1], {
                length: len
            }))

            //打印调用栈
            console.log('sendto called from:\n' +
                Thread.backtrace(this.context, Backtracer.FUZZY)
                .map(DebugSymbol.fromAddress).join('\n') + '\n')
        },
        onLeave: function (retval) {}
    })
}

function hookTcpJni() {
    /hookJniSend()
}

setImmediate(hookTcpJni())

hookTcpJni_write

跟踪收包函数

1
2
3
4
5
6
7
8
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/SocketInputStream.java
private native int socketRead0(FileDescriptor fd,byte b[], int off, int len,int timeout)

//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/native/SocketInputStream.c
SocketInputStream_socketRead0(JNIEnv *env, jobject this,jobject fdObj, jbyteArray data,jint off, jint len, jint timeout)
 
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/native/linux_close.cpp
->int NET_Read(int s, void* buf, size_t len)

NET_Read

这里同样在Android源码中不太好跟踪,同理用IDA打开libopenjdk.so库查看NET_Read函数.

recvfrom

可以发现Tcp收包在C层最终调用的是libc库中的recvfrom函数进行收包,至此可写出Hook代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function getAddrInfo(sockfd, isRecv) {
    var message = {}
    var src_dst = ["src", "dst"]
    for (var i = 0; i < src_dst.length; i++) {
        if ((src_dst[i] == "src") ^ isRecv) {
            var sockAddr = Socket.localAddress(sockfd)
        } else {
            var sockAddr = Socket.peerAddress(sockfd)
        }
        if (sockAddr == null) {
            // 网络超时or其他原因可能导致socket被关闭
            message[src_dst[i] + "_port"] = 0
            message[src_dst[i] + "_addr"] = 0
        } else {
            message[src_dst[i] + "_port"] = (sockAddr.port & 0xFFFF)
            message[src_dst[i] + "_addr"] = sockAddr.ip.split(":").pop()
        }
    }
    return message['src_addr'] + ':' + message['src_port'] + ' --> ' + message['dst_addr'] + ':' + message['dst_port']
}

function hookJniRecv() {
    var recvfrom = Module.getExportByName("libc.so", "recvfrom");
    Interceptor.attach(recvfrom, {
        onEnter: function (args) {
            this.fd = args[0];
            this.buff = args[1];
            this.size = args[2];
        },
        onLeave: function (retval) {
            var type = Socket.type(this.fd.toInt32());
            if (retval > 0 && type != null && type != 'unix:stream') {
                //打印数据包的源和目标地址
                var sockfd = this.fd.toInt32()
                var msg = getAddrInfo(sockfd, true)
                console.log('recvfrom', msg)

                //打印收包内容
                console.log(hexdump(this.buff, {
                    length: retval.toInt32()
                }))

                //打印调用栈
                console.log('recvfrom called from:\n' +
                    Thread.backtrace(this.context, Backtracer.FUZZY)
                    .map(DebugSymbol.fromAddress).join('\n') + '\n')
            }
        }
    })
}

function hookTcpJni() {
    hookJniRecv()
}

setImmediate(hookTcpJni())

hookTcpJni_recv

Udp

服务端代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import socket
import threading

IP = '0.0.0.0'
PORT = 9997

def main():
    server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server.bind((IP, PORT))
    print(f'[*] bind on {IP}:{PORT}')

    while True:
        data, address = server.recvfrom(1024)
        print(f'[*] recvfrom connection from {address[0]}:{address[1]}')
        print(f'[*] Received: {data.decode("utf-8")}')
        server.sendto(b'ACKKK', address)

if __name__ == '__main__':
    main()

客户端代码

  1. 在AndroidManifest.xml文件赋予权限:
1
<uses-permission android:name="android.permission.INTERNET"/>
  1. MainActivity.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.example.luoudp;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.example.luoudp.databinding.ActivityMainBinding;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;

public class MainActivity extends AppCompatActivity {
    static final String TAG = "XiaLuoHun";

    // Used to load the 'luoudp' library on application startup.
    static {
        System.loadLibrary("luoudp");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI());

        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    while (true){
                        DoUdp("10.67.16.180", 9997);
                        Thread.sleep(3000);
                    }
                }
                catch(IOException | InterruptedException e){
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public static void DoUdp(String strIP, int nPort) throws IOException {
        DatagramSocket socket = new DatagramSocket();
        //发送数据
        DatagramPacket outPacket = new DatagramPacket(new byte[0], 0, InetAddress.getByName(strIP), nPort);
        outPacket.setData("hello,server".getBytes(StandardCharsets.UTF_8));
        socket.send(outPacket);

        //接收数据
        byte[] inBuff = new byte[4096];
        DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
        socket.receive(inPacket);
        //打印读取到的数据
        Log.d(TAG, new String(inBuff, 0, inPacket.getLength()));
    }

    /**
     * A native method that is implemented by the 'luoudp' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}

Java层抓包

观察上述开发流程,我们发现以下几个关键函数:

  1. DatagramPacket的构造函数,包含了要发送的IP和Port.
  2. 发包函数,DatagramSocket.send()
  3. 收包函数,DatagramSocket.receive()

跟踪DatagramPacket构造函数

1
2
3
4
5
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/DatagramPacket.java
public DatagramPacket(byte buf[], int offset, int length,InetAddress address, int port)
->public synchronized void setAddress(InetAddress iaddr)
public synchronized void setPort(int iport)
//发现IP和Port最终在DatagramPacket的address和Port变量中存储

接下来就可以写Hook代码了.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function hookDatagramPacketInit() {
    var DatagramPacket = Java.use('java.net.DatagramPacket')
    DatagramPacket.$init.overload('[B', 'int', 'java.net.InetAddress', 'int').implementation = function (buf, length, address, port) {
        console.log('IP:', address.toString(), 'Port:', port)
        var result = this.$init(buf, length, address, port)
        //console.log('IP:', this.address.value.toString(), 'Port:', this.port.value)
        return result
    }
}

function hookUdpJava() {
    Java.perform(function () {
        hookDatagramPacketInit()
    })
}

setImmediate(hookUdpJava())

hookUdpJava_DatagramPacket

跟踪发包函数

这里跟踪的是DatagramSocket类的send函数.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/DatagramSocket.java
public void send(DatagramPacket p){
    //---
    //这里发现调用了一个抽象类的send方法,通过动态调试看堆栈调用,可以确定其具体实现类为PlainDatagramSocketImpl
    getImpl().send(p);
    //---
}

//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/PlainDatagramSocketImpl.java
->protected void send(DatagramPacket p)

//http://androidxref.com/8.1.0_r33/xref/libcore/luni/src/main/java/libcore/io/IoBridge.java
->public static int sendto(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, InetAddress inetAddress, int port)

//http://androidxref.com/8.1.0_r33/xref/libcore/luni/src/main/java/libcore/io/Linux.java
->public int sendto(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, InetAddress inetAddress, int port)

->private native int sendtoBytes(FileDescriptor fd, Object buffer, int byteOffset, int byteCount, int flags, InetAddress inetAddress, int port)
private native int sendtoBytes(FileDescriptor fd, Object buffer, int byteOffset, int byteCount, int flags, SocketAddress address)

通过对Android源码的跟踪,可以发现Udp的发包函数最终调用的是Linux类的sendtoBytes这两个重载函数进行发包,至此可以写出Hook代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function hookUdpSend() {
    var linux = Java.use("libcore.io.Linux")
    linux.sendtoBytes.overload('java.io.FileDescriptor', 'java.lang.Object', 'int', 'int', 'int', 'java.net.InetAddress', 'int').implementation = function (fd, byteAry, byteOffset, byteCount, flags, inetAddress, port) {
        //打印数据包的源和目标地址
        var sockname = this.getsockname(fd)
        //console.log('sockname', JSON.stringify(sockname))
        var InetSocketAddressObj = Java.cast(sockname, Java.use("java.net.InetSocketAddress"))
        var src_addr = InetSocketAddressObj.holder.value.addr.value.toString()
        var src_port = InetSocketAddressObj.holder.value.port.value
        var dst_addr = inetAddress.toString()
        var dst_port = port
        var msg = src_addr + ':' + src_port + ' --> ' + dst_addr + ':' + dst_port
        console.log('sendtoBytes', msg)

        //打印发包内容
        var b = Java.array("byte", byteAry);
        var bufLen = byteCount
        var ptr = Memory.alloc(bufLen);
        for (var i = 0; i < bufLen; ++i)
            Memory.writeS8(ptr.add(i), b[byteOffset + i]);
        console.log(hexdump(ptr, {
            offset: 0,
            length: bufLen,
            header: false,
            ansi: false
        }));

        //打印调用栈
        console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
        return this.sendtoBytes(fd, byteAry, byteOffset, byteCount, flags, inetAddress, port)
    }

    linux.sendtoBytes.overload('java.io.FileDescriptor', 'java.lang.Object', 'int', 'int', 'int', 'java.net.SocketAddress').implementation = function (fd, byteAry, byteOffset, byteCount, flags, address) {
        return this.sendtoBytes(fd, byteAry, byteOffset, byteCount, flags, address);
    }
}

function hookUdpJava() {
    Java.perform(function () {
        //hookDatagramPacketInit()
        hookUdpSend()
    })
}

setImmediate(hookUdpJava())

hookUdpJava_sendtoBytes

跟踪收包函数

这里跟踪的是DatagramSocket类的receive函数.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/DatagramSocket.java
public synchronized void receive(DatagramPacket p){
    //---
    //这里发现调用了一个抽象类的receive方法,通过动态调试看堆栈调用,可以确定其具体实现类为PlainDatagramSocketImpl
     getImpl().receive(p);
    //---
}
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/PlainDatagramSocketImpl.java
protected synchronized void receive0(DatagramPacket p)
->private void doRecv(DatagramPacket p, int flags)

//http://androidxref.com/8.1.0_r33/xref/libcore/luni/src/main/java/libcore/io/IoBridge.java
->public static int recvfrom(boolean isRead, FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, DatagramPacket packet, boolean isConnected)
 
//http://androidxref.com/8.1.0_r33/xref/libcore/luni/src/main/java/libcore/io/Linux.java    
->public int recvfrom(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, InetSocketAddress srcAddress)

->private native int recvfromBytes(FileDescriptor fd, Object buffer, int byteOffset, int byteCount, int flags, InetSocketAddress srcAddress)

通过对Android源码的跟踪,发现Udp的收包函数最终调用的是Linux类的recvfromBytes函数进行发包,至此可写出Hook代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function hookUdpRecv() {
    var linux = Java.use("libcore.io.Linux")
    linux.recvfromBytes.implementation = function (fd, buffer, byteOffset, byteCount, flags, srcAddress) {
        var result = this.recvfromBytes(fd, buffer, byteOffset, byteCount, flags, srcAddress)
        //打印数据包的源和目标地址
        var src_addr = srcAddress.holder.value.addr.value.toString()
        var src_port = srcAddress.holder.value.port.value
        var sockname = this.getsockname(fd)
        //console.log('sockname', JSON.stringify(sockname))
        var InetSocketAddressObj = Java.cast(sockname, Java.use("java.net.InetSocketAddress"))
        var dst_addr = InetSocketAddressObj.holder.value.addr.value.toString()
        var dst_port = InetSocketAddressObj.holder.value.port.value
        var msg = src_addr + ':' + src_port + ' --> ' + dst_addr + ':' + dst_port
        console.log('recvfromBytes', msg)

        //打印收包内容
        var b = Java.array("byte", buffer);
        var ptr = Memory.alloc(result);
        for (var i = 0; i < result; ++i)
            Memory.writeS8(ptr.add(i), b[byteOffset + i]);
        console.log(hexdump(ptr, {
            offset: 0,
            length: result,
            header: false,
            ansi: false
        }));

        //打印调用栈
        console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
        return result
    }
}

function hookUdpJava() {
    Java.perform(function () {
        hookUdpRecv()
    })
}

setImmediate(hookUdpJava())

hookUdpJava_recvfromBytes

Jni层抓包

在上面Java层抓包中,我们找到了Udp Java层最深处的发包函数(sendtoBytes)和收包函数(recvfromBytes),这里我们继续沿着这两个函数跟踪C层的调用链,以寻求最佳Hook点.

跟踪发包函数

1
2
//http://androidxref.com/8.1.0_r33/xref/libcore/luni/src/main/native/libcore_io_Linux.cpp
static jint Linux_sendtoBytes(JNIEnv* env, jobject, jobject javaFd, jobject javaBytes, jint byteOffset, jint byteCount, jint flags, jobject javaInetAddress, jint port)

send1

send2

这里发现在Android源码中不太好跟踪,又Linux_sendtoBytes这个函数并没有导出,通过下述命令进行搜索不太好使.

1
2
3
adb shell
su
grep -ril Linux_sendtoBytes /system/lib64/*

那我们接下来该如何继续下去呢? 这里有3种方案:

  1. 猜测,我们在上面发现TCP发包在C层最终调用的是libc库中的sendto函数,那Udp是不是呢?(可以用上面Tcp的Jni Hook代码进行测试)

  2. 正向写一个C层的Udp发包代码示例,进行跟踪.

  3. 修改Android源码,将Linux_sendtoBytes函数导出,编译Android源码,将Linux_sendtoBytes所在的so导出,用IDA进行查看.

这里为了看清楚点,我选择了第3种方案,在Android源码libcore_io_Linux.cpp中新增一个Linux_sendtoBytes1函数并将其导出,重新编译Android源码,生成Android镜像并将其刷入手机中.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//http://androidxref.com/8.1.0_r33/xref/libcore/luni/src/main/native/libcore_io_Linux.cpp
extern "C" JNIEXPORT jint Linux_sendtoBytes1(JNIEnv* env, jobject, jobject javaFd, jobject javaBytes, jint byteOffset, jint byteCount, jint flags, jobject javaInetAddress, jint port) {
    ScopedBytesRO bytes(env, javaBytes);
    if (bytes.get() == NULL) {
        return -1;
    }

    return NET_IPV4_FALLBACK(env, ssize_t, sendto, javaFd, javaInetAddress, port,
                             NULL_ADDR_OK, bytes.get() + byteOffset, byteCount, flags);
}

通过搜索Linux_sendtoBytes1,发现在libjavacore.so中,将其导出用IDA进行查看.

Linux_sendtoBytes1

Linux_sendtoBytes2

Udp_Sendto

从上面可以看到UDP发包在C层最终调用的也是libc库中的sendto函数,那这个跟Tcp发包底层调用的sendto有什么区别?

观察上面Tcp发包的sendto函数是直接将后两个参数(addr和addr_len)置0,而Udp则是将后两个参数填充为发包的目标IP和Port

再通过在IDA中查看libjavacore.so中的Linux_sendtoBytes1函数发现,在调用sendto之前调用了inetAddressToSockaddr函数进行地址转换,后续将其作为sendto函数的后两个参数传入.

接下来在Android源码中查看inetAddressToSockaddr函数做了什么.

1
2
3
4
5
//http://androidxref.com/8.1.0_r33/xref/libcore/luni/src/main/native/NetworkUtilities.cpp
bool inetAddressToSockaddr(JNIEnv* env, jobject inetAddress, int port, sockaddr_storage& ss, socklen_t& sa_len) {
    return inetAddressToSockaddr(env, inetAddress, port, ss, sa_len, true);
}
->static bool inetAddressToSockaddr(JNIEnv* env, jobject inetAddress, int port, sockaddr_storage& ss, socklen_t& sa_len, bool map)

inetAddressToSockaddr1

inetAddressToSockaddr2

inetAddressToSockaddr3

可以看到是由inetAddressToSockaddr的最后一个参数,决定是将Java层的IP和Port转换成了C层的结构体sockaddr_in还是sockaddr_in6

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//大小为16
struct sockaddr_in
{
 uint16 sin_family;          /* Address family AF_INET */ 
 uint16 sin_port;            /* Port number.  */
 uint32 sin_addr.s_addr;     /* Internet address.  */
 unsigned char sin_zero[8];  /* Pad to size of `struct sockaddr'.  */
};

//大小为28
struct sockaddr_in6
{
 uint16 sin6_family;         /* Address family AF_INET6 */
 uint16 sin6_port;           /* Transport layer port # */
 uint32 sin6_flowinfo;       /* IPv6 flow information */
 uint8  sin6_addr[16];       /* IPv6 address */
 uint32 sin6_scope_id;       /* IPv6 scope-id */
};

从Android源码来看,inetAddressToSockaddr的最后一个参数默认传了true,当然我们也可以来打印libc.so库中的sendto函数的后两个参数,来看究竟是将其转换成了上面哪个结构体.

打印sendto后两个参数

从这里可以看到我们这个例子是将Java层的IP和Port转换成了C层的sockaddr_in6结构体,这样就得到了IP的格式,可以打印数据包的地址信息了,至此可写出Hook代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function hookJniSend() {
    var sendto = Module.getExportByName("libc.so", "sendto");
    Interceptor.attach(sendto, {
        onEnter: function (args) {
            var fd = args[0].toInt32()
            var buf = args[1]
            var n = args[2]
            var flags = args[3]
            var addr = args[4]
            var addr_len = args[5].toInt32()

            //打印Udp sendto的后两个参数
            // console.log(hexdump(addr, {
            //     length: addr_len
            // }))
            // console.log('len', addr_len)

            //打印数据包的源和目标地址
            var sockfd = fd
            var sockAddr = Socket.localAddress(sockfd)
            var src_addr = sockAddr.ip.split(":").pop()
            var src_port = (sockAddr.port & 0xFFFF)

            //解析sockaddr_in6结构
            var ipbase = ptr(addr).add(0x14);
            var dst_addr = ipbase.readU8() + "." + ipbase.add(1).readU8() + "." + ipbase.add(2).readU8() + "." + ipbase.add(3).readU8()
            var port_high = ptr(addr).add(2).readU8();
            var port_low = ptr(addr).add(3).readU8();
            var dst_port = port_high * 0x100 + port_low;

            var msg = src_addr + ':' + src_port + ' --> ' + dst_addr + ':' + dst_port
            console.log('sendto', msg)

            //打印发包内容
            var len = args[2].toInt32();
            console.log(hexdump(buf, {
                length: len
            }))

            //打印调用栈
            console.log('sendto called from:\n' +
                Thread.backtrace(this.context, Backtracer.FUZZY)
                .map(DebugSymbol.fromAddress).join('\n') + '\n')
        },
        onLeave: function (retval) {}
    })
}

function hookUdpJni() {
    hookJniSend()
}

setImmediate(hookUdpJni())

hookUdpJni_send

跟踪收包函数

1
2
//http://androidxref.com/8.1.0_r33/xref/libcore/luni/src/main/native/libcore_io_Linux.cpp
static jint Linux_recvfromBytes(JNIEnv* env, jobject, jobject javaFd, jobject javaBytes, jint byteOffset, jint byteCount, jint flags, jobject javaInetSocketAddress)

recv1

recv2

这里同样在Android源码中不太好跟踪,同上修改Android源码libcore_io_Linux.cpp文件,新增一个Linux_recvfromBytes1函数并将其导出,重新编译生成镜像并将其刷入手机.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//http://androidxref.com/8.1.0_r33/xref/libcore/luni/src/main/native/libcore_io_Linux.cpp
extern "C" JNIEXPORT jint Linux_recvfromBytes1(JNIEnv* env, jobject, jobject javaFd, jobject javaBytes, jint byteOffset, jint byteCount, jint flags, jobject javaInetSocketAddress) {
    ScopedBytesRW bytes(env, javaBytes);
    if (bytes.get() == NULL) {
        return -1;
    }
    sockaddr_storage ss;
    socklen_t sl = sizeof(ss);
    memset(&ss, 0, sizeof(ss));
    sockaddr* from = (javaInetSocketAddress != NULL) ? reinterpret_cast<sockaddr*>(&ss) : NULL;
    socklen_t* fromLength = (javaInetSocketAddress != NULL) ? &sl : 0;
    jint recvCount = NET_FAILURE_RETRY(env, ssize_t, recvfrom, javaFd, bytes.get() + byteOffset, byteCount, flags, from, fromLength);
    if (recvCount >= 0) {
        // The socket may have performed orderly shutdown and recvCount would return 0 (see man 2
        // recvfrom), in which case ss.ss_family == AF_UNIX and fillInetSocketAddress would fail.
        // Don't fill in the address if recvfrom didn't succeed. http://b/33483694
        if (ss.ss_family == AF_INET || ss.ss_family == AF_INET6) {
            fillInetSocketAddress(env, javaInetSocketAddress, ss);
        }
    }
    return recvCount;
}

使用如下命令将libjavacore.so导出,在IDA中查看Linux_recvfromBytes1函数

1
adb pull /system/lib64/libjavacore.so

Linux_recvfromBytes1

从上面可以看到UDP收包在C层最终调用的也是libc库中的recvfrom函数,至此可写出Hook代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function hookJniRecv() {
    var recvfrom = Module.getExportByName("libc.so", "recvfrom")
    Interceptor.attach(recvfrom, {
        onEnter: function (args) {
            this.fd = args[0].toInt32()
            this.buf = args[1]
            this.n = args[2]
            this.flags = args[3]
            this.addr = args[4]
            this.addr_len = args[5].toInt32()
        },
        onLeave: function (retval) {
            if (retval.toInt32() > -1) {
                //console.log('retval', retval)
                //打印Udp recvfrom
                // console.log(hexdump(this.addr, {
                //     length: 0x20
                // }))
                //console.log('len', this.addr_len)

                //打印数据包的源和目标地址
                var sockAddr = Socket.localAddress(this.fd)
                var dst_addr = sockAddr.ip
                var dst_port = (sockAddr.port & 0xFFFF)

                //解析sockaddr_in6结构
                var ipbase = ptr(this.addr).add(0x14);
                var src_addr = ipbase.readU8() + "." + ipbase.add(1).readU8() + "." + ipbase.add(2).readU8() + "." + ipbase.add(3).readU8()
                var porr_high = ptr(this.addr).add(2).readU8();
                var porr_low = ptr(this.addr).add(3).readU8();
                var src_port = porr_high * 0x100 + porr_low;

                var msg = src_addr + ':' + src_port + ' --> ' + dst_addr + ':' + dst_port
                console.log('recvfrom', msg)

                //打印收包内容
                console.log(hexdump(this.buf, {
                    length: retval.toInt32()
                }))

                //打印调用栈
                console.log('sendto called from:\n' +
                    Thread.backtrace(this.context, Backtracer.FUZZY)
                    .map(DebugSymbol.fromAddress).join('\n') + '\n')
            }


        }
    })

}

function hookUdpJni() {
    hookJniRecv()
}

setImmediate(hookUdpJni())

hookUdpJni_recv

Https

客户端代码

  1. 在AndroidManifest.xml文件赋予权限:
1
<uses-permission android:name="android.permission.INTERNET"/>
  1. 布局
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center|center_horizontal|center_vertical"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center|center_horizontal|center_vertical"
        android:id="@+id/mybtn"
        android:text="发送请求"
        android:textSize="45sp">
    </Button>

</LinearLayout>
  1. MainActivity.java
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
package com.example.luohttps;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class MainActivity extends AppCompatActivity {
    private static String TAG = "XiaLuoHun";
    private Handler handler = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initHandler();
        // 定位发送请求按钮
        Button btn = findViewById(R.id.mybtn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getResponse("https://www.baidu.com");
            }
        });
    }

    private void httpUrlConnection(String strUrl){
        try {
            URL url = new URL(strUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            //设置Http请求方法
            connection.setRequestMethod("GET");
            //设置请求参数
            connection.setRequestProperty("token","LuoHun");
            //设置连接超时时间
            connection.setConnectTimeout(8000);
            //设置接收超时时间
            connection.setReadTimeout(8000);
            // 开始连接
            connection.connect();
            //得到响应码
            //int responseCode = connection.getResponseCode();
 /*                       if(responseCode == HttpURLConnection.HTTP_OK){
                            //...
                        }*/
            //获取服务器返回的输入流
            InputStream in = connection.getInputStream();
            //if(in.available() > 0){

            // 每次写入1024字节
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            StringBuffer sb = new StringBuffer();
            while ((in.read(buffer)) != -1) {
                sb.append(new String(buffer));
            }
            sendMsg(sb.toString());
            //Log.d("LuoHun", sb.toString());
            //关闭Http连接
            connection.disconnect();
            // }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void getResponse(String strUrl) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                httpUrlConnection(strUrl);
            }
        }).start();
    }

    private void initHandler() {
        handler = new Handler(getMainLooper(), new Handler.Callback() {
            @Override
            public boolean handleMessage(@NonNull Message msg) {
                if (msg.what == 1){
                    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                    builder.setTitle("NOTICE");
                    builder.setMessage((String) msg.obj);
                    builder.setPositiveButton("Confirm",null);
                    builder.create().show();
                }
                return false;
            }
        });
    }

    private void sendMsg(String message){
        Message msg = new Message();
        msg.what = 1;
        msg.obj = message;
        handler.sendMessage(msg);
    }
}

Java层抓包

观察上述开发流程,我们发现以下几个关键函数:

  1. URL类的构造函数,其包含了目标网址的字符串
  2. 收包函数,HttpURLConnection.getInputStream()

跟踪URL构造函数

1
2
//http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/URL.java
public URL(String spec)

很容易写出下面的Hook代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function hookURL() {
    var URL = Java.use('java.net.URL')
    URL.$init.overload('java.lang.String').implementation = function(urlstr){
        console.log('url => ', urlstr)
        return this.$init(urlstr)
    }
}

function hookSSL() {
    Java.perform(function () {
        hookURL()
    })
}

setImmediate(hookSSL())

常规跟踪收包函数

接下来跟踪HttpURLConnection类的getInputStream函数.

首先注意到HttpURLConnection是一个抽象类,需要找到它的具体实现类.

HttpURLConnection抽象类

通过动态调试,确定HttpURLConnection的具体实现类为com.android.okhttp.internal.huc.HttpsURLConnectionImpl

HttpURLConnection实现类

接下来跟踪HttpsURLConnectionImpl类的getInputStream函数.

1
2
3
//http://androidxref.com/8.1.0_r33/xref/external/okhttp/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
这里发现HttpsURLConnectionImpl类中并没有getInputStream函数,去父类看看.
//http://androidxref.com/8.1.0_r33/xref/external/okhttp/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/DelegatingHttpsURLConnection.java

getInputStream1

getInputStream2

对于上面的代码,看起来是有点懵的,那该如何弄呢?

我们可以换种思路,Https属于应用层协议,最终要经过传输层进行传输(即Socket),那我们可以搜索上述Demo内存中与Socket相关的类并全部Hook,重新点击"发送请求"按钮,看会经过哪些比较可疑的类并进一步分析.这里也许有一部分人有些疑问,既然上面我们已经分析了Tcp的调用链,那我们用上面Tcp的Hook代码然后打印调用栈,一步步进行回溯不就可以找到Https加密前和解密后的收发包接口了吗,经测试这种方案是不行的,Https不走上述路径,但是可以用来抓Http数据.

可以参考下述中的sock_read和sock_write函数来解释为什么Https不走我们上述分析的Tcp路径.

http://androidxref.com/8.1.0_r33/xref/external/boringssl/src/crypto/bio/socket.c

前期研究

  1. 点击App界面上的发送请求按钮后,使用Objection搜索内存中Socket相关的类
1
2
objection.exe -g com.example.luohttps explore
android hooking search classes socket

Socket相关的类

  1. 将搜索出来的结果复制到1.txt文件中,并在每一行前添加android hooking watch class

Hook全部Socket

  1. 结束上述App进程,利用objection -c参数批量Hook Socket相关类(如果出现崩溃现象,可以删除或将导致崩溃的类移到另一个文件中待下次执行即可)
1
objection.exe -g com.example.luohttps explore -c .\1.txt
  1. 点击App界面上的"发送请求"按钮,会发现下图中的几个与Socket相关的类被调用了

被调用的Socket

  1. 观察上图,发现上面两个看着很可疑的函数,可以进一步Hook进行确认是否是Https的收发包接口
1
2
3
//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java
com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write([B, int, int)
com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read([B, int, int)

用下方Hook代码进行测试,确认上述两个函数为Https的收发包接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
function getAddrInfo(socket, isRead) {
    var src_addr = ''
    var src_port = ''
    var dst_addr = ''
    var dst_port = ''

    if (isRead) {
        src_addr = socket.getRemoteSocketAddress().toString().split(":")[0].split("/").pop()
        src_port = socket.getRemoteSocketAddress().toString().split(":").pop()
        dst_addr = socket.getLocalAddress().toString().split(":")[0].split("/").pop()
        dst_port = socket.getLocalPort().toString()
    } else {
        src_addr = socket.getLocalAddress().toString().split(":")[0].split("/").pop()
        src_port = socket.getLocalPort().toString()
        dst_addr = socket.getRemoteSocketAddress().toString().split(":")[0].split("/").pop()
        dst_port = socket.getRemoteSocketAddress().toString().split(":").pop()
    }
    return src_addr + ':' + src_port + ' --> ' + dst_addr + ':' + dst_port
}

function hookSSLOutputStream() {
    //com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write
    Java.use('com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream').write.overload('[B', 'int', 'int').implementation = function (buf, offset, byteCount) {
        var result = this.write(buf, offset, byteCount)
        //console.log('result', result, 'offset', offset, 'byteCount', byteCount)

        //打印数据包的源和目标地址
        var socket = this.this$0.value.socket.value
        var msg = getAddrInfo(socket, false)
        console.log('SSLOutputStream.write', msg)

        //打印发包内容
        var ptr = Memory.alloc(byteCount);
        for (var i = 0; i < byteCount; ++i)
            Memory.writeS8(ptr.add(i), buf[offset + i]);
        console.log(hexdump(ptr, {
            offset: 0,
            length: byteCount,
            header: false,
            ansi: false
        }));

        return result
    }
}

function hookSSLInputStream() {
    //com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read
    Java.use('com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream').read.overload('[B', 'int', 'int').implementation = function (buf, offset, byteCount) {
        var result = this.read(buf, offset, byteCount)

        //打印数据包的源和目标地址
        var socket = this.this$0.value.socket.value
        var msg = getAddrInfo(socket, true)
        console.log('SSLOutputStream.read', msg)

        //打印收包内容
        var ptr = Memory.alloc(result);
        for (var i = 0; i < result; ++i)
            Memory.writeS8(ptr.add(i), buf[offset + i]);
        console.log(hexdump(ptr, {
            offset: 0,
            length: result,
            header: false,
            ansi: false
        }));

        return result
    }
}

function hookSSL() {
    Java.perform(function () {
        hookSSLOutputStream()
        hookSSLInputStream()
    })
}

setImmediate(hookSSL())

hookSSL

跟踪发包函数

接下来跟踪ConscryptFileDescriptorSocket$SSLOutputStream类的write函数.

1
2
3
4
5
6
7
8
//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java
public void write(byte[] buf, int offset, int byteCount)

//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/main/java/org/conscrypt/SslWrapper.java
->void write(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)

//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/main/java/org/conscrypt/NativeCrypto.java
->static native void SSL_write(long sslNativePointer, FileDescriptor fd,SSLHandshakeCallbacks shc, byte[] b, int off, int len, int writeTimeoutMillis)

至此可以写出Https发包函数的Hook代码.(这个地方不太好打印源和目标的IP信息,故对Https来说Java层的发包函数Hook点通常选在com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function hookSSLWrite() {
    var NativeCrypto = Java.use("com.android.org.conscrypt.NativeCrypto")
    NativeCrypto.SSL_write.implementation = function (sslNativePointer, fd, shc, b, off, len, writeTimeoutMillis) {
        var result = this.SSL_write(sslNativePointer, fd, shc, b, off, len, writeTimeoutMillis)
        //console.log('result', result, 'off', off, 'len', len)
        //打印发包内容
        var bufLen = len
        var ptr = Memory.alloc(bufLen);
        for (var i = 0; i < bufLen; ++i)
            Memory.writeS8(ptr.add(i), b[off + i]);
        console.log(hexdump(ptr, {
            offset: 0,
            length: bufLen,
            header: false,
            ansi: false
        }));

        //打印堆栈调用
        console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))

        return result
    }
}

function hookSSL() {
    Java.perform(function () {
        hookSSLWrite()
    })
}

setImmediate(hookSSL())

NativeCrypto_SSL_write

跟踪收包函数

接下来跟踪ConscryptFileDescriptorSocket$SSLInputStream类的read函数.

1
2
3
4
5
6
7
8
//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java
public int read(byte[] buf, int offset, int byteCount)

//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/main/java/org/conscrypt/SslWrapper.java
->int read(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)

//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/main/java/org/conscrypt/NativeCrypto.java
->static native int SSL_read(long sslNativePointer, FileDescriptor fd, SSLHandshakeCallbacks shc,byte[] b, int off, int len, int readTimeoutMillis)

至此可以写出Https收包函数的Hook代码.(这个地方不太好打印源和目标的IP信息,故对Https来说Java层的收包函数Hook点通常选在com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function hookSSLRead() {
    var NativeCrypto = Java.use("com.android.org.conscrypt.NativeCrypto")
    NativeCrypto.SSL_read.implementation = function (sslNativePointer, fd, shc, b, off, len, writeTimeoutMillis) {
        var result = this.SSL_read(sslNativePointer, fd, shc, b, off, len, writeTimeoutMillis)
        //console.log('result', result, 'off', off, 'len', len)
        //打印收包内容
        var bufLen = result
        var ptr = Memory.alloc(bufLen);
        for (var i = 0; i < bufLen; ++i)
            Memory.writeS8(ptr.add(i), b[off + i]);
        console.log(hexdump(ptr, {
            offset: 0,
            length: bufLen,
            header: false,
            ansi: false
        }));

        //打印堆栈调用
        console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))

        return result
    }
}

function hookSSL() {
    Java.perform(function () {
        hookSSLRead()
    })
}

setImmediate(hookSSL())

NativeCrypto_SSL_read

Jni层抓包

在上面Java层抓包中,我们找到了Https Java层最深处的发包函数(SSL_write)和收包函数(SSL_read),这里我们继续沿着这两个函数跟踪C层的调用链,以寻求最佳Hook点.

跟踪发包函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/main/java/org/conscrypt/NativeCrypto.java
static native void SSL_write(long sslNativePointer, FileDescriptor fd,SSLHandshakeCallbacks shc, byte[] b, int off, int len, int writeTimeoutMillis)

//搜索NativeCrypto_SSL_write
//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/jni/main/cpp/NativeCrypto.cpp
static void NativeCrypto_SSL_write(JNIEnv* env, jclass, jlong ssl_address, jobject fdObject,jobject shc, jbyteArray b, jint offset, jint len, jint write_timeout_millis)
->static int sslWrite(JNIEnv* env, SSL* ssl, jobject fdObject, jobject shc, const char* buf, jint len, OpenSslError& sslError, int write_timeout_millis)

//http://androidxref.com/8.1.0_r33/xref/external/boringssl/src/ssl/ssl_lib.cc
//最终编译在libssl.so中
->int SSL_write(SSL *ssl, const void *buf, int num){
    //SSL结构体在下面定义
    //http://androidxref.com/8.1.0_r33/xref/external/boringssl/include/openssl/base.h
    //http://androidxref.com/8.1.0_r33/xref/external/boringssl/src/ssl/internal.h
    
    //---
    //这里根据不同ssl版本调用不同的函数,所以说下面找函数应该带上版本,如ssl3_write_app_data
    ret = ssl->method->write_app_data(ssl, &needs_handshake, (const uint8_t *)buf, num);
    //---
}

//http://androidxref.com/8.1.0_r33/xref/external/boringssl/src/ssl/s3_pkt.cc
->int ssl3_write_app_data(SSL *ssl, int *out_needs_handshake, const uint8_t *buf, int len)
//下面这个函数就是Https 发包过程中明文数据的终点
->static int do_ssl3_write(SSL *ssl, int type, const uint8_t *buf, unsigned len)

跟踪收包函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/main/java/org/conscrypt/NativeCrypto.java
static native int SSL_read(long sslNativePointer, FileDescriptor fd, SSLHandshakeCallbacks shc, byte[] b, int off, int len, int readTimeoutMillis)

//搜索NativeCrypto_SSL_read
//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/jni/main/cpp/NativeCrypto.cpp
static jint NativeCrypto_SSL_read(JNIEnv* env, jclass, jlong ssl_address, jobject fdObject, jobject shc, jbyteArray b, jint offset, jint len, jint read_timeout_millis)
->static int sslRead(JNIEnv* env, SSL* ssl, jobject fdObject, jobject shc, char* buf, jint len, OpenSslError& sslError, int read_timeout_millis)

//http://androidxref.com/8.1.0_r33/xref/external/boringssl/src/ssl/ssl_lib.cc
//最终编译在libssl.so中
->int SSL_read(SSL *ssl, void *buf, int num)
->static int ssl_read_impl(SSL *ssl, void *buf, int num, int peek){
    //---
    //这里根据不同ssl版本调用不同的函数,所以说下面找函数应该带上版本,如ssl3_read_app_data
    int ret = ssl->method->read_app_data(ssl, &got_handshake, (uint8_t *)buf, num, peek);
    //---
}

//http://androidxref.com/8.1.0_r33/xref/external/boringssl/src/ssl/s3_pkt.cc
->int ssl3_read_app_data(SSL *ssl, int *out_got_handshake, uint8_t *buf, int len, int peek)

Hook代码

观察上述调用链,考虑到源和目标地址的获取难易程度以及收发包数据获取的通用性,对Https来说,Jni层的发包函数Hook点通常选择libssl.so的SSL_write函数,收包函数通常选择libssl.so的SSL_read函数.故可写出下述Hook代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
var sslGetFdPtr = null
var SSL_get_sessionPtr = null
var SSL_SESSION_get_idPtr = null
var sslGetFd = null
var SSL_get_session = null
var SSL_SESSION_get_id = null

function initializeGlobals() {
    sslGetFdPtr = Module.getExportByName("libssl.so", "SSL_get_rfd");
    SSL_get_sessionPtr = Module.getExportByName("libssl.so", "SSL_get_session")
    SSL_SESSION_get_idPtr = Module.getExportByName("libssl.so", "SSL_SESSION_get_id")

    sslGetFd = new NativeFunction(sslGetFdPtr, 'int', ['pointer'])
    SSL_get_session = new NativeFunction(SSL_get_sessionPtr, "pointer", ["pointer"])
    SSL_SESSION_get_id = new NativeFunction(SSL_SESSION_get_idPtr, "pointer", ["pointer", "pointer"])
}

function getSslSessionId(ssl) {
    var session = SSL_get_session(ssl);
    if (session == 0) {
        return 0;
    }
    var len = Memory.alloc(4);
    var p = SSL_SESSION_get_id(session, len);
    len = Memory.readU32(len);
    var session_id = "";
    for (var i = 0; i < len; i++) {
        // Read a byte, convert it to a hex string (0xAB ==> "AB"), and append
        // it to session_id.
        session_id +=
            ("0" + Memory.readU8(p.add(i)).toString(16).toUpperCase()).substr(-2);
    }
    return session_id;
}

function getPortsAndAddresses(sockfd, isRead) {
    var message = {}
    var src_dst = ["src", "dst"]
    for (var i = 0; i < src_dst.length; i++) {
        if ((src_dst[i] == "src") ^ isRead) {
            var sockAddr = Socket.localAddress(sockfd)
        } else {
            var sockAddr = Socket.peerAddress(sockfd)
        }
        if (sockAddr == null) {
            // 网络超时or其他原因可能导致socket被关闭
            message[src_dst[i] + "_port"] = 0
            message[src_dst[i] + "_addr"] = 0
        } else {
            message[src_dst[i] + "_port"] = (sockAddr.port & 0xFFFF)
            message[src_dst[i] + "_addr"] = sockAddr.ip.split(":").pop()
        }
    }
    return message['src_addr'] + ':' + message['src_port'] + ' --> ' + message['dst_addr'] + ':' + message['dst_port']
}

function hookSSLWrite() {
    var SSL_write = Module.findExportByName("libssl.so", "SSL_write")
    Interceptor.attach(SSL_write, {
        onEnter: function (args) {
            var sslPtr = args[0]
            var buf = args[1]
            var num = args[2]

            //打印SSLSessionID
            var sslSessionID = getSslSessionId(sslPtr)
            console.log('SSL Session:', sslSessionID)

            //打印数据包的源和目标地址
            var sockfd = sslGetFd(sslPtr)
            var msg = getPortsAndAddresses(sockfd, false)
            console.log('[SSL_write] ', msg)

            //打印发包内容
            console.log(hexdump(buf, {
                length: num.toUInt32()
            }));

            //打印调用栈
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
        },
        onLeave: function (ret) {
            //console.log('SSL_write ret', ret)
        }
    })
}

function hookSSLRead() {
    var SSL_read = Module.findExportByName("libssl.so", "SSL_read")
    Interceptor.attach(SSL_read, {
        onEnter: function (args) {
            this.sslPtr = args[0];
            this.buf = args[1];
            var num = args[2];
            //console.log('SSL_read num', num)
        },
        onLeave: function (ret) {
            //console.log('SSL_read ret', ret)
            if (ret.toInt32() > -1) {
                //打印SSLSessionID
                var sslSessionID = getSslSessionId(this.sslPtr)
                console.log('SSL Session:', sslSessionID)

                //打印数据包的源和目标地址
                var sockfd = sslGetFd(this.sslPtr)
                var msg = getPortsAndAddresses(sockfd, true)
                console.log('[SSL_read]', msg)

                //打印收包内容
                console.log(hexdump(this.buf, {
                    length: ret.toUInt32()
                }));

                //打印调用栈
                console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
            }
        }
    })
}

function hookSSLJni() {
    initializeGlobals()
    hookSSLWrite()
    hookSSLRead()
}

setImmediate(hookSSLJni())

hookSSLWrite

hookSSLRead

参考链接

<安卓Frida逆向与抓包实战>

r0capture


相关内容

0%