网络通信协议分析

要解决的问题:

如何对自定义协议进行逆向分析?

发送参数被加密,如何快速完成参数处理流程的定位?

加密算法复杂,如何主动调用完成对数据包的处理和重放?

逆向分析思想:

堆栈回溯思想:Java层、JNI层

控制流分析与数据流分析相结合思想

关键字符串、关键API定位思想

1. Java层Socket抓包

在Socket通信过程中,Android系统框架层到底有哪些API参与到建立连接发送数据当中去呢?又是在Java层利用哪些API来完成对数据的抓取?

1.1 TCP

追踪安卓源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java.net.Socket类构造函数:new Socket(ip, port)->
Socket(InetAddress[] addresses, int port, SocketAddress localAddr, boolean stream)->
impl->java.net.SocksSocketImpl

建立连接:connect(SocketAddress endpoint)

接收数据:java.net.SocketInputStream.read(byte[])->
read(b, 0, b.length)->
read(b, off, length, impl.getTimeout())->
socketRead(fd, b, off, length, timeout)->
socketRead0(fd, b, off, length, timeout)(jni函数)

发送数据:java.net.SocketOutputStream.write(byte[])->
socketWrite(b, 0, b.length)->
socketWrite0(fd, b, off, len)(jni函数)

我们可以hook这些API来得到发送和接收原始的数据包。

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 isprintable(value){
if (value>=32 && value<=126){
return true
}
return false
}

function hooktcp(){
Java.perform(function(){
var SocketClass = Java.use("java.net.Socket")
SocketClass.$init.overload('java.lang.String', 'int').implementation = function(arg0, arg1){
console.log("[" + Process.getCurrentThreadId() + "]new Socket connection:" + arg0 + "port:" + arg1)
return this.$init(arg0, arg1)
}

var SocketInputStreamClass = Java.use("java.net.SocketInputStream")
SocketInputStreamClass.socketRead0.implementation = function(arg0, arg1, arg2, arg3, arg4){
var size = this.socketRead0(arg0, arg1, arg2, arg3, arg4)
//console.log("[" + Process.getCurrentThreadId() + "]socketRead0:size:" + size + "--content:" + JSON.stringify(arg1))
var bytearray = Java.array('byte', arg1)
var content = ''
for(var i=0; i<size; i++){
if(isprintable(bytearray[i]))
content = content + String.fromCharCode(bytearray[i])
}
var socketimpl = this.impl.value
var address = socketimpl.address.value
var port = socketimpl.port.value
console.log("\naddress:" + address + ",port:" + port + "\n" + JSON.stringify(this.socket.value) + "\n[" + Process.getCurrentThreadId() + "]reveive:" + content)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
return size
}

var SocketOutputStreamClass = Java.use("java.net.SocketOutputStream")
SocketOutputStreamClass.socketWrite0.implementation = function(arg0, arg1, arg2, arg3){
var result = this.socketWrite0(arg0, arg1, arg2, arg3)
//console.log("[" + Process.getCurrentThreadId() + "]socketWrite0:len:" + arg3 + "--content:" + JSON.stringify(arg1))
var bytearray = Java.array('byte', arg1)
var content = ''
for(var i=0; i<arg3; i++){
if(isprintable(bytearray[i]))
content = content + String.fromCharCode(bytearray[i])
}
var socketimpl = this.impl.value
var address = socketimpl.address.value
var port = socketimpl.port.value
console.log("\nsend address:" + address + ",port:" + port + "\n" + JSON.stringify(this.socket.value) + "\n[" + Process.getCurrentThreadId() + "]send:" + content)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
return result
}
})
}

function main(){
hooktcp()
}
setImmediate(main)

1.2 UDP

追踪安卓源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
发送数据:java.net.DatagramSocket.send->
PlainDatagramSocketImpl.send->
IoBridge.sendto(fd, p.getData(), p.getOffset(), p.getLength(), 0, address, port)->
Libcore.os.sendto(fd, bytes, byteOffset, byteCount, flags, inetAddress, port)->
sendtoBytes(FileDescriptor fd, Object buffer, int byteOffset, int byteCount, int flags, InetAddress inetAddress, int port)(jni函数)

sendtoBytes(FileDescriptor fd, Object buffer, int byteOffset, int byteCount, int flags, SocketAddress address)(jni函数)

接收数据:java.net.DatagramSocket.receive->
AbstractPlainDatagramSocketImpl.reveive->
PlainDatagramSocketImpl.reveive0->
doRecv->
IoBridge.recvfrom->
BlockGuardOs.recvfrom->
Linux.recvfrom->
recvfromBytes(FileDescriptor fd, Object buffer, int byteOffset, int byteCount, int flags, InetSocketAddress srcAddress)(jni函数)

hook API:

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 isprintable(value){
if (value>=32 && value<=126){
return true
}
return false
}

function hookudp(){
Java.perform(function(){
var LinuxClass = Java.use("libcore.io.Linux")
LinuxClass.recvfromBytes.implementation = function(arg0, arg1, arg2, arg3, arg4, arg5){
var size = this.recvfromBytes(arg0, arg1, arg2, arg3, arg4, arg5)
var bytearray = Java.array('byte', arg1)
var content = ""
for(var i=0; i<size; i++){
if(isprintable(bytearray[i]))
content = content + String.fromCharCode(bytearray[i])
}
console.log("address:" + arg5 + "\n[" + Process.getCurrentThreadId() + "]recvfromBytes:size:" + size + "--content:" + JSON.stringify(arg1) + "\n" + content)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
return size
}

LinuxClass.sendtoBytes.overload('java.io.FileDescriptor','java.lang.Object','int','int','int','java.net.InetAddress','int').implementation = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6){
var size = this.sendtoBytes(arg0, arg1, arg2, arg3, arg4, arg5, arg6)
var bytearray = Java.array('byte', arg1)
var content = ""
for(var i=0; i<size; i++){
if(isprintable(bytearray[i]))
content = content + String.fromCharCode(bytearray[i])
}
console.log("address:" + arg5 + ",port:" + arg6 + "\n[" + Process.getCurrentThreadId() + "]sendtoBytes1:len:" + size + "--content:" + JSON.stringify(arg1) + "\n" + content)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
return size
}

LinuxClass.sendtoBytes.overload('java.io.FileDescriptor','java.lang.Object','int','int','int','java.net.SocketAddress').implementation = function(arg0, arg1, arg2, arg3, arg4, arg5){
var size = this.sendtoBytes(arg0, arg1, arg2, arg3, arg4, arg5)
var bytearray = Java.array('byte', arg1)
var content = ""
for(var i=0; i<size; i++){
if(isprintable(bytearray[i]))
content = content + String.fromCharCode(bytearray[i])
}
console.log("address" + arg5 + "\n[" + Process.getCurrentThreadId() + "]sendtoBytes2:len:" + size + "--content:" + JSON.stringify(arg1) + "\n" + content)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
return size
}
})
}

function main(){
hookudp()
}
setImmediate(main)

2. Java层SSL通信抓包

在安卓8.0上追踪okHttp框架层源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
sslSocket->com.android.org.conscrypt.OpenSSLSocketImplWrapper

发送数据:com.android.rog.conscrypt.OpenSSLSocketImpl$SSLSocketOutputStream->
public void write(int oneByte)

public void write(byte[] buf, int offset, int byteCount)->
NativeCrypto.SSL_write(sslNativePointer, Platform.getFileDescriptor(socket), OpenSSLSocketImpl.this, buf, offset, byteCount, writeTimeoutMilliseconds)(jni函数)->com.android.org.conscrypt.NativeCrypto

接收数据:com.android.rog.conscrypt.OpenSSLSocketImpl$SSLSocketInputStream->
public int read()

public int read(byte[] buf, int offset, int byteCount)->
NativeCrypto.SSL_read(sslNativePointer, Platform.getFileDescriptor(socket), OpenSSLSocektImpl.this, buf, offset, byteCount, getSoTimeout())(jni函数)->com.android.org.conscrypt.NativeCrypto

如果不知道它的包名是什么,可以用枚举列出该程序的完整包名。

1
2
3
4
5
6
7
8
function enumerate(){
Java.perform(function(){
Java.enumerateLoadedClassesSync().forEach(function(classname){
if (classname.indexOf("NativeCrypto") >= 0)
console.log(classname)
})
})
}

hook ssl:

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
function isprintable(value){
if (value>=32 && value<=126){
return true
}
return false
}

function hookssl(){
Java.perform(function(){
var NativeCryptoClass = Java.use("com.android.org.conscrypt.NativeCrypto")
NativeCryptoClass.SSL_read.implementation = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6){
var size = this.SSL_read(arg0, arg1, arg2, arg3, arg4, arg5, arg6)
var bytearray = Java.array('byte', arg3)
var content = ''
for(var i=0; i<size; i++){
if(isprintable(bytearray[i]))
content = content + String.fromCharCode(bytearray[i])
}
console.log("\n[" + Process.getCurrentThreadId() + "]ssl_read:" + content)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
return size
}

NativeCryptoClass.SSL_write.implementation = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6){
var result = this.SSL_write(arg0, arg1, arg2, arg3, arg4, arg5, arg6)
var bytearray = Java.array('byte', arg3)
var content = ''
for(var i=0; i<arg5; i++){
if(isprintable(bytearray[i]))
content = content + String.fromCharCode(bytearray[i])
}
console.log("\n[" + Process.getCurrentThreadId() + "]ssl_write:" + content)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
return result
}
})
}

function main(){
hookssl()
}
setImmediate(main)

发送HTTPS注册包时也可以抓到相关信息:

但是SSL_write()SSL_read()并没有提供IP地址与端口,所以我们还要hook能拿到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
function isprintable(value){
if (value>=32 && value<=126){
return true
}
return false
}

function hookssl2(){
Java.perform(function(){
var SSLInputStreamClass = Java.use("com.android.org.conscrypt.OpenSSLSocketImpl$SSLInputStream")
SSLInputStreamClass.read.overload('[B','int','int').implementation = function(arg0, arg1, arg2){
//获得内部类对象所属的外部类对象
var OpenSSLSocektImplobj = this.this$0.value
var socketobj = OpenSSLSocektImplobj.socket.value

var size = this.read(arg0, arg1, arg2)
var bytearray = Java.array('byte', arg0)
var content = ''
for(var i=0; i<size; i++){
if(isprintable(bytearray[i]))
content = content + String.fromCharCode(bytearray[i])
}
console.log("\naddress:" + socketobj + "\n[" + Process.getCurrentThreadId() + "]SSLInputStream.read:" + content)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
return size
}

var SSLOutputStreamClass = Java.use("com.android.org.conscrypt.OpenSSLSocketImpl$SSLOutputStream")
SSLOutputStreamClass.write.overload('[B','int','int').implementation = function(arg0, arg1, arg2){
//获得内部类对象所属的外部类对象
var OpenSSLSocektImplobj = this.this$0.value
var socketobj = OpenSSLSocektImplobj.socket.value

var result = this.SSL_write(arg0, arg1, arg2)
var bytearray = Java.array('byte', arg0)
var content = ''
for(var i=0; i<arg2; i++){
if(isprintable(bytearray[i]))
content = content + String.fromCharCode(bytearray[i])
}
console.log("\naddress:" + socketobj + "\n" + JSON.stringify(this.socket.value) + "\n[" + Process.getCurrentThreadId() + "]SSLOutputStream.write:" + content)
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
return result
}
})
}

function main(){
hookssl2()
}
setImmediate(main)

如果一个App有多个进程,在当前进程抓不到包的情况下可以hook App的其它进程试试。

1
frida -U -p [PID] -l hookssl2.js --no-pause -o log.txt

3. JNI层Socket抓包

JNI函数是Java层和JNI层的桥梁。

3.1 TCP

比如在TCP中跟踪到的JNI函数是socketRead0()socketWrite0(),如何更进一步去到JNI层呢?在Google安卓源码当中,JNI函数有独特的命名规则,即类名_方法名。这种命名方法表示动态注册,即需要JNI_OnLoad。

那它们是在哪个so文件中呢?知道在哪个so文件中后,用IDA交叉引用功能快速知道通过libc哪个API完成数据的发送与接收。

在编译的安卓源码中找到哪里调用到了C文件,再打开mk文件查看是否有模块名。如果没有再查看哪里调用了该mk文件,打开新找到的mk文件查看是否有模块名,以此类推。

最后可以看到模块名为libopenjdkd

可以去手机当中将这个so文件导出来:

1
2
adb pull /system/lib/libopenjdk.so
adb pull /system/lib64/libopenjdk.so libopenjdk64.so//重命名为libopenjdk64.so

加载进IDA中,由于是动态注册的,所以直接找JNI_OnLoad。

SocketInputStream_socketRead0()中调用了j_NET_Read()读取数据;在SocketOutputStream_socketWrite0()中调用了j_NET_Send()发送数据。

在1.1中只拿到了Java层的调用栈,可以继续补充JNI层的函数得到从Java层到JNI层的完整的调用栈。

1
2
3
4
5
6
7
8
9
10
11
接收数据:
socketRead0(jni函数)->
NET_Read->
ssize_t recvfrom(int fd, void *buf, size_t n, int flags, struct sockaddr *addr, socklen_t *addr_len)(libc函数)
//进入libc.so中在导出函数表找到recvfrom,系统调用号为292,syscall:292

发送数据:
socketWrite0(jni函数)->
NET_Send->
ssize_t sendto(int fd, const void *buf, size_t n, int flags, struct sockaddr *addr, socklen_t *addr_len)(libc函数)
//syscall:290

如果App使用Java框架层的API来进行通信,Java层抓包可以直接打印出Java层的调用栈信息,而不需要关心JNI层的调用栈信息。但对于一些使用libc系统层函数进行通信的App,使用Java层就会抓不到包,所以必须对JNI层进行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
function printNativeStack(context, name){
var array = Thread.backtrace(context, Backtracer.ACCURATE)
var first = DebugSymbol.fromAddress(array[0])
//在Java层没有找到才打印JNI层中的信息,避免重复打印
if(first.toString().indexOf("libopenjdk.so!NET_Send") < 0){
var trace = Thread.backtrace(context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n")
console.log("\n[" + Process.getCurrentThreadId() + "]start:" + name)
console.log("\n[" + Process.getCurrentThreadId() + "]" + trace)
console.log("\n[" + Process.getCurrentThreadId() + "]end:" + name)
}
}

function getsocketdetail(fd){
var result = ""
var type = Socket.type(fd)
if (type != null){
result = result + "type:" + type
var peer = Socket.peerAddress(fd)
var local = Socket.localAddress(fd)
result = result + ",address:" + JSON.stringify(peer) + ",local" + JSON.stringify(local)
}else{
result = "unknown"
}
return result
}

function hooklibc(){
var libcmoudle = Process.getModuleByName("libc.so")
var recvfrom_addr = libcmoudle.getExportByName("recvfrom")
var sendto_addr = libcmoudle.getExportByName("sendto")
console.log(recvfrom_addr + "---" + sendto_addr)

Interceptor.attach(recvfrom_addr, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
console.log("\n[" + Process.getCurrentThreadId() + "]" + "go into libc.so->recvfrom")
printNativeStack(this.context,"recvfrom")
},onLeave:function(retval){
var size = retval.toInt32()
if(size>0){
var result = getsocketdetail(this.arg0.toInt32())
console.log(result + "---libc.so->recvfrom:\n" + hexdump(this.arg1,{
length:size
}))
}
console.log("\n[" + Process.getCurrentThreadId() + "]" + "leave libc.so->recvfrom")
}
})

Interceptor.attach(sendto_addr, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
console.log("\n[" + Process.getCurrentThreadId() + "]go into libc.so->sendto")
printNativeStack(this.context,"sendto")
},onLeave:function(retval){
var size = ptr(this.arg2).toInt32()
if(size > 0){
var result = getsocketdetail(this.arg0.toInt32())
console.log(result + "---libc.so->sendto:\n" + hexdump(this.arg1,{
length:size
}))
}
console.log("\n[" + Process.getCurrentThreadId() + "]leave libc.so->sendto")
}
})
}

function main(){
hooklibc()
}
setImmediate(main)

使用Socket通信时,TCP通常用libc.so中的send()recv()通信,而UDP通常用libc.so中的sendto()recvfrom()通信。但hook sendto()recvfrom()为什么能抓到TCP的包呢?是因为在libc.so当中,send()recv()里面调用的就是sendto()recvfrom()

3.2 UDP

1
2
3
4
5
6
7
8
9
发送数据:
sendtoBytes(jni函数)->
NET_IPV4_FALLBACK(env, ssize_t, sendto, javaFd, javaInetAddress, port, NULL_ADDR_OK, bytes.get() + byteOffset, byteCount, flags)->
sendto(libc函数)

接收数据:
recvfromBytes(jni函数)->
NET_FAILURE_RETRY(env, ssize_t, recvfrom, javaFd, bytes.get() + byteOffset, byteCount, flags, from, fromLength)->
recvfrom(libc函数)

可以看到最后还是转到对sendto()recvfrom()上来,所以直接用TCP中hook sendto()recvfrom()脚本即可。

但是对端的IP地址与端口显示null,所以还需要对脚本做一下修改:

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
function printNativeStack(context, name){
var array = Thread.backtrace(context, Backtracer.ACCURATE)
var first = DebugSymbol.fromAddress(array[0])
//在Java层没有找到才打印JNI层中的信息,避免重复打印
if(first.toString().indexOf("libopenjdk.so!NET_Send") < 0){
var trace = Thread.backtrace(context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n")
console.log("\n[" + Process.getCurrentThreadId() + "]start:" + name)
console.log("\n[" + Process.getCurrentThreadId() + "]" + trace)
console.log("\n[" + Process.getCurrentThreadId() + "]end:" + name)
}
}

function getsocketdetail(fd){
var result = ""
var type = Socket.type(fd)
if (type != null){
result = result + "type:" + type
var peer = Socket.peerAddress(fd)
var local = Socket.localAddress(fd)
result = result + ",address:" + JSON.stringify(peer) + ",local" + JSON.stringify(local)
}else{
result = "unknown"
}
return result
}

function getip(ip_ptr){
var result = ptr(ip_ptr).readU8() + "." + ptr(ip_ptr.add(1)).readU8() + "." + ptr(ip_ptr.add(2)).readU8() + "." + ptr(ip_ptr.add(3)).readU8()
return result
}

function getudpaddr(addrptr){
var port_ptr = addrptr.add(2)
var port = ptr(port_ptr).readU8()*256 + ptr(port_ptr.add(1)).readU8()
var ip_ptr = addrptr.add(4)
var ip_addr = getip(ip_ptr)
return "peer:" + ip_addr + ",port:" + port
}

function hooklibc(){
var libcmoudle = Process.getModuleByName("libc.so")
var recvfrom_addr = libcmoudle.getExportByName("recvfrom")
var sendto_addr = libcmoudle.getExportByName("sendto")
console.log(recvfrom_addr + "---" + sendto_addr)

Interceptor.attach(recvfrom_addr, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
this.arg3 = args[3]
this.arg4 = args[4]
this.arg5 = args[5]
console.log("\n[" + Process.getCurrentThreadId() + "]" + "go into libc.so->recvfrom")
printNativeStack(this.context,"recvfrom")
},onLeave:function(retval){
var size = retval.toInt32()
if(size>0){
var result = getsocketdetail(this.arg0.toInt32())
if(result.indexOf("udp") >= 0){
/*struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
}*/
var sockaddr_in_ptr = this.arg4
var sizeof_sockaddr_in = this.arg5
console.log("this is a recvfrom udp->" + getudpaddr(sockaddr_in_ptr) + "---size:" + sizeof_sockaddr_in)
}
console.log(result + "---libc.so->recvfrom:" + hexdump(this.arg1,{
length:size
}))
}
console.log("\n[" + Process.getCurrentThreadId() + "]" + "leave libc.so->recvfrom")
}
})

Interceptor.attach(sendto_addr, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
this.arg3 = args[3]
this.arg4 = args[4]
this.arg5 = args[5]
console.log("\n[" + Process.getCurrentThreadId() + "]go into libc.so->sendto")
printNativeStack(this.context,"sendto")
},onLeave:function(retval){
var size = ptr(this.arg2).toInt32()
if(size > 0){
var result = getsocketdetail(this.arg0.toInt32())
if(result.indexOf("udp") >= 0){
var sockaddr_in_ptr = this.arg4
var sizeof_sockaddr_in = this.arg5
console.log("this is a sendto udp->" + getudpaddr(sockaddr_in_ptr) + "---size:" + sizeof_sockaddr_in)
}
console.log(result + "---libc.so->sendto:" + hexdump(this.arg1,{
length:size
}))
}
console.log("\n[" + Process.getCurrentThreadId() + "]leave libc.so->sendto")
}
})
}

function main(){
hooklibc()
}
setImmediate(main)

4. JNI层SSL通信抓包

在安卓8.0上追踪源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
发送数据:
SSL_write(jni函数)->
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)->
SSL_write(ssl, buf, len)->
int ssl3_write_app_data(SSL *ssl, int *out_needs_handshake, const uint8_t *buf, int len)->
static int do_ssl3_write(SSL *ssl, int type, const uint8_t *buf, unsigned len)(明密文分水岭)->
static int ssl3_write_pending(SSL *ssl, int type, const uint8_t *buf, unsigned int len)->
int ssl_write_buffer_flush(SSL *ssl)->
dtls_write_buffer_flush(ssl)或tls_write_buffer_flush(ssl)->
int BIO_write(BIO *bio, const void *in, int inl)->
static int bio_io(BIO *bio, void *buf, int len, size_t method_offset, int callback_flags, size_t *num)->...->
static int sock_write(BIO *b, const char *in, int inl)

接收数据:
SSL_read(jni函数)->...->
static int sock_read(BIO *b, char *out, int outl)

openssl并没有使用send()sendto()来发送加密的数据,而是使用write()函数发送;同样没有使用recv()recvfrom()来接收数据,而是使用read()函数来接收。所以通过JNI层Socket抓包并不能抓到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
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
function printNativeStack(context, name){
var array = Thread.backtrace(context, Backtracer.ACCURATE)
var first = DebugSymbol.fromAddress(array[0])
//在Java层没有找到才打印JNI层中的信息,避免重复打印
if(first.toString().indexOf("libopenjdk.so!NET_Send") < 0){
var trace = Thread.backtrace(context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n")
console.log("\n[" + Process.getCurrentThreadId() + "]start:" + name)
console.log("\n[" + Process.getCurrentThreadId() + "]" + trace)
console.log("\n[" + Process.getCurrentThreadId() + "]end:" + name)
}
}

function getsocketdetail(fd){
var result = ""
var type = Socket.type(fd)
if (type != null){
result = result + "type:" + type
var peer = Socket.peerAddress(fd)
var local = Socket.localAddress(fd)
result = result + ",address:" + JSON.stringify(peer) + ",local" + JSON.stringify(local)
}else{
result = "unknown"
}
return result
}

function getip(ip_ptr){
var result = ptr(ip_ptr).readU8() + "." + ptr(ip_ptr.add(1)).readU8() + "." + ptr(ip_ptr.add(2)).readU8() + "." + ptr(ip_ptr.add(3)).readU8()
return result
}

function getudpaddr(addrptr){
var port_ptr = addrptr.add(2)
var port = ptr(port_ptr).readU8()*256 + ptr(port_ptr.add(1)).readU8()
var ip_ptr = addrptr.add(4)
var ip_addr = getip(ip_ptr)
return "peer:" + ip_addr + ",port:" + port
}

function hookopenssl(){
var libcmoudle = Process.getModuleByName("libc.so")
var read_addr = libcmoudle.getExportByName("read")
var write_addr = libcmoudle.getExportByName("write")
console.log(read_addr + "---" + write_addr)

Interceptor.attach(read_addr, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
this.socketinfo = getsocketdetail(this.arg0.toInt32())
console.log("\n[" + Process.getCurrentThreadId() + "]" + "go into libc.so->read---" + this.socketinfo)
this.flag = false
if(this.socketinfo.indexOf("tcp") >= 0){
this.flag = true
}
if(this.flag){
printNativeStack(this.context,"read")
}
},onLeave:function(retval){
if(this.flag){
var size = retval.toInt32()
if(size>0){
console.log("[" + Process.getCurrentThreadId() + "]---libc.so->read:" + hexdump(this.arg1,{
length:size
}))
}
}
console.log("\n[" + Process.getCurrentThreadId() + "]" + "leave libc.so->read")
}
})

Interceptor.attach(write_addr, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
this.socketinfo = getsocketdetail(this.arg0.toInt32())
console.log("\n[" + Process.getCurrentThreadId() + "]go into libc.so->write---" + this.socketinfo)
this.flag = false
if(this.socketinfo.indexOf("tcp") >= 0){
this.flag = true
}
if(this.flag){
printNativeStack(this.context,"write")
}
},onLeave:function(retval){
if(this.flag){
var size = retval.toInt32()
if(size>0){
console.log("[" + Process.getCurrentThreadId() + "]---libc.so->write:" + hexdump(this.arg1,{
length:size
}))
}
}
console.log("\n[" + Process.getCurrentThreadId() + "]leave libc.so->write")
}
})
}

function main(){
hookopenssl()
}
setImmediate(main)

hook最深层的write()read()函数(或BIO_write()BIO_read())就可以得到SSL通信的密文,如果想要得到明文可以hook SSL_write()SSL_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
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
function printNativeStack(context, name){
var array = Thread.backtrace(context, Backtracer.ACCURATE)
var first = DebugSymbol.fromAddress(array[0])
//在Java层没有找到才打印JNI层中的信息,避免重复打印
if(first.toString().indexOf("libopenjdk.so!NET_Send") < 0){
var trace = Thread.backtrace(context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n")
console.log("\n[" + Process.getCurrentThreadId() + "]start:" + name)
console.log("\n[" + Process.getCurrentThreadId() + "]" + trace)
console.log("\n[" + Process.getCurrentThreadId() + "]end:" + name)
}
}

function getsocketdetail(fd){
var result = ""
var type = Socket.type(fd)
if (type != null){
result = result + "type:" + type
var peer = Socket.peerAddress(fd)
var local = Socket.localAddress(fd)
result = result + ",address:" + JSON.stringify(peer) + ",local" + JSON.stringify(local)
}else{
result = "unknown"
}
return result
}

function getip(ip_ptr){
var result = ptr(ip_ptr).readU8() + "." + ptr(ip_ptr.add(1)).readU8() + "." + ptr(ip_ptr.add(2)).readU8() + "." + ptr(ip_ptr.add(3)).readU8()
return result
}

function getudpaddr(addrptr){
var port_ptr = addrptr.add(2)
var port = ptr(port_ptr).readU8()*256 + ptr(port_ptr.add(1)).readU8()
var ip_ptr = addrptr.add(4)
var ip_addr = getip(ip_ptr)
return "peer:" + ip_addr + ",port:" + port
}

function hookopenssl2(){
var libcmoudle = Process.getModuleByName("libssl.so")
var read_addr = libcmoudle.getExportByName("SSL_read")
var write_addr = libcmoudle.getExportByName("SSL_write")

Interceptor.attach(read_addr, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
console.log("\n[" + Process.getCurrentThreadId() + "]" + "go into libssl.so->SSL_read")
printNativeStack(this.context,"SSL_read")
},onLeave:function(retval){
var size = retval.toInt32()
if(size>0){
console.log("[" + Process.getCurrentThreadId() + "]---libssl.so->SSL_read:" + hexdump(this.arg1,{
length:size
}))
}
console.log("\n[" + Process.getCurrentThreadId() + "]" + "leave libssl.so->SSL_read")
}
})

Interceptor.attach(write_addr, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
console.log("\n[" + Process.getCurrentThreadId() + "]go into libssl.so->SSL_write")
printNativeStack(this.context,"SSL_write")
},onLeave:function(retval){
var size = retval.toInt32()
if(size>0){
console.log("[" + Process.getCurrentThreadId() + "]---libssl.so->SSL_write:" + hexdump(this.arg1,{
length:size
}))
}
console.log("\n[" + Process.getCurrentThreadId() + "]leave libssl.so->SSL_write")
}
})
}

function main(){
hookopenssl2()
}
setImmediate(main)

建立Socket通信以后,需要调用SSL_set_fd()完成当前Socket ID和SSL的绑定,结束通信时调用SSL_get_fd()。要获取IP地址则需用到这个API。

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
function printNativeStack(context, name){
var array = Thread.backtrace(context, Backtracer.ACCURATE)
var first = DebugSymbol.fromAddress(array[0])
//在Java层没有找到才打印JNI层中的信息,避免重复打印
if(first.toString().indexOf("libopenjdk.so!NET_Send") < 0){
var trace = Thread.backtrace(context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n")
console.log("\n[" + Process.getCurrentThreadId() + "]start:" + name)
console.log("\n[" + Process.getCurrentThreadId() + "]" + trace)
console.log("\n[" + Process.getCurrentThreadId() + "]end:" + name)
}
}

function getsocketdetail(fd){
var result = ""
var type = Socket.type(fd)
if (type != null){
result = result + "type:" + type
var peer = Socket.peerAddress(fd)
var local = Socket.localAddress(fd)
result = result + ",address:" + JSON.stringify(peer) + ",local" + JSON.stringify(local)
}else{
result = "unknown"
}
return result
}

function getip(ip_ptr){
var result = ptr(ip_ptr).readU8() + "." + ptr(ip_ptr.add(1)).readU8() + "." + ptr(ip_ptr.add(2)).readU8() + "." + ptr(ip_ptr.add(3)).readU8()
return result
}

function getudpaddr(addrptr){
var port_ptr = addrptr.add(2)
var port = ptr(port_ptr).readU8()*256 + ptr(port_ptr.add(1)).readU8()
var ip_ptr = addrptr.add(4)
var ip_addr = getip(ip_ptr)
return "peer:" + ip_addr + ",port:" + port
}

function hookopenssl3(){
var libsslmodule = Process.getModuleByName("libssl.so")
var read_addr = libsslmodule.getExportByName("SSL_read")
var write_addr = libsslmodule.getExportByName("SSL_write")
var SSL_get_rfd_ptr = libsslmodule.getExportByName("SSL_get_rfd")
var SSL_get_rfd = new NativeFunction(SSL_get_rfd_ptr, 'int', ['pointer'])

Interceptor.attach(read_addr, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
console.log("\n[" + Process.getCurrentThreadId() + "]" + "go into libssl.so->SSL_read")
printNativeStack(this.context,"SSL_read")
var size = retval.toInt32()
if(size>0){
var sockfd = SSL_get_rfd(this.arg0)
var socketdetail = getsocketdetail(sockfd)
console.log(socketdetail + "---[" + Process.getCurrentThreadId() + "]---libssl.so->SSL_read:" + hexdump(this.arg1,{
length:size
}))
}
},onLeave:function(retval){
console.log("\n[" + Process.getCurrentThreadId() + "]" + "leave libssl.so->SSL_read")
}
})

Interceptor.attach(write_addr, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
console.log("\n[" + Process.getCurrentThreadId() + "]go into libssl.so->SSL_write")
printNativeStack(this.context,"SSL_write")
var size = retval.toInt32()
if(size>0){
var sockfd = SSL_get_rfd(this.arg0)
var socketdetail = getsocketdetail(sockfd)
console.log(socketdetail + "---[" + Process.getCurrentThreadId() + "]---libssl.so->SSL_write:" + hexdump(this.arg1,{
length:size
}))
}
},onLeave:function(retval){
console.log("\n[" + Process.getCurrentThreadId() + "]leave libssl.so->SSL_write")
}
})
}

function main(){
hookopenssl3()
}
setImmediate(main)

5. 自编译openssl库抓包

一些App或浏览器并没有使用Android系统提供的ssl库,有可能使用了开源的ssl自编译生成本地库,再去调用本地库完成ssl通信。此时使用上面的API hook无法成功抓包和溯源。

那使用自编译的库进行SSL通信又如何解决呢?对于自编译的openssl库,如果SSL_write()SSL_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
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
function printNativeStack(context, name){
var trace = Thread.backtrace(context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join("\n")
console.log("\n[" + Process.getCurrentThreadId() + "]start:" + name)
console.log("\n[" + Process.getCurrentThreadId() + "]" + trace)
console.log("\n[" + Process.getCurrentThreadId() + "]end:" + name)
}

function getsocketdetail(fd){
var result = ""
var type = Socket.type(fd)
if (type != null){
result = result + "type:" + type
var peer = Socket.peerAddress(fd)
var local = Socket.localAddress(fd)
result = result + ",address:" + JSON.stringify(peer) + ",local" + JSON.stringify(local)
}else{
result = "unknown"
}
return result
}

function hookallssl(){
var libsslmodule = Process.getModuleByName("libssl.so")
var SSL_get_rfd_ptr = libsslmodule.getExportByName("SSL_get_rfd")
var SSL_get_rfd = new NativeFunction(SSL_get_rfd_ptr,'int',['pointer'])
Process.enumerateModules().forEach(function(module){
module.enumerateExports().forEach(function(symbol){
var name = symbol.name
if(name == 'SSL_read'){
Interceptor.attach(symbol.address, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
console.log("go into [" + Process.getCurrentThreadId() + "]---" + JSON.stringify(module) + "---" + JSON.stringify(symbol))
printNativeStack(this.context, Process.getCurrentThreadId() + "---" + JSON.stringify(module) + "---" + JSON.stringify(symbol))
var size = retval.toInt32()
if(size>0){
var sockfd = SSL_get_rfd(this.arg0)
var socketdetail = getsocketdetail(sockfd)
console.log(socketdetail + "---[" + Process.getCurrentThreadId() + "]---" + JSON.stringify(module) + "---" + JSON.stringify(symbol) + hexdump(this.arg1,{
length:size
}))
}
},onLeave:function(retval){
console.log("leave [" + Process.getCurrentThreadId() + "]---" + JSON.stringify(module) + "---" + JSON.stringify(symbol))
}
})
}

if(name == 'SSL_write'){
Interceptor.attach(symbol.address, {
onEnter:function(args){
this.arg0 = args[0]
this.arg1 = args[1]
this.arg2 = args[2]
console.log("go into [" + Process.getCurrentThreadId() + "]---" + JSON.stringify(module) + "---" + JSON.stringify(symbol))
printNativeStack(this.context, Process.getCurrentThreadId() + "---" + JSON.stringify(module) + "---" + JSON.stringify(symbol))
var size = retval.toInt32()
if(size>0){
var sockfd = SSL_get_rfd(this.arg0)
var socketdetail = getsocketdetail(sockfd)
console.log(socketdetail + "---[" + Process.getCurrentThreadId() + "]---" + JSON.stringify(module) + "---" + JSON.stringify(symbol) + hexdump(this.arg1,{
length:size
}))
}
},onLeave:function(retval){
console.log("leave [" + Process.getCurrentThreadId() + "]---" + JSON.stringify(module) + "---" + JSON.stringify(symbol))
}
})
}

})
})
}

function main(){
hookallssl()
}
setImmediate(main)

如果符号被抹去,就不能用上面那种方法定位了。可以通过更深层次的write()read() hook回溯调用栈得到SSL_write()SSL_read(),然后再对函数进行hook,就可以得到发送的明文数据。

6. 协议枚举、暴破及算法模拟

用Wireshark抓包发现没有HTTP或HTTPS,只有TCP和UDP等传输层协议,说明这个App是用了自定义Socket协议来通信,这就可以用到上面的知识,分三种情况:

  1. Java层Socket通信hook点(socketRead0()socketWrite0())
  2. JNI层Socket通信hook点(sendto()recvfrom())
  3. 通过系统调用发送接收数据包(自编译openssl)

课时9我称为封神!建议反复观看!

基于unicorn框架:unidbg、AndroidNativeEmu