概述
下述实验所基于的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 ()
客户端代码
在AndroidManifest.xml文件赋予权限:
1
<uses-permission android:name= "android.permission.INTERNET" />
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层抓包
观察上述开发流程,我们发现以下几个关键函数:
Socket的构造函数,包含了要连接的IP和Port
发包函数,OutputStream.write()
收包函数,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 ())
跟踪发包函数
接下来跟踪OutputStream类的write函数.
首先注意到OutputStream是一个抽象类,需要找到它的具体实现类.
我们既可以通过源码级调试,确定OutputStream的具体实现类为java.net.SocketOutputStream
也可以利用下述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
接下来跟踪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 ())
跟踪收包函数
接下来跟踪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的具体实现类为java.net.SocketInputStream
接下来跟踪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 ())
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 )
这里发现在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
可以发现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 ())
跟踪收包函数
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 )
这里同样在Android源码中不太好跟踪,同理用IDA打开libopenjdk.so库查看NET_Read函数.
可以发现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 ())
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 ()
客户端代码
在AndroidManifest.xml文件赋予权限:
1
<uses-permission android:name= "android.permission.INTERNET" />
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层抓包
观察上述开发流程,我们发现以下几个关键函数:
DatagramPacket的构造函数,包含了要发送的IP和Port.
发包函数,DatagramSocket.send()
收包函数,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 ())
跟踪发包函数
这里跟踪的是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 ())
跟踪收包函数
这里跟踪的是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 ())
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 )
这里发现在Android源码中不太好跟踪,又Linux_sendtoBytes这个函数并没有导出,通过下述命令进行搜索不太好使.
1
2
3
adb shell
su
grep -ril Linux_sendtoBytes /system/lib64/*
那我们接下来该如何继续下去呢? 这里有3种方案:
猜测,我们在上面发现TCP发包 在C层最终调用的是libc库中的sendto 函数,那Udp是不是呢?(可以用上面Tcp的Jni Hook代码进行测试)
正向写一个C层的Udp发包代码示例,进行跟踪.
修改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进行查看.
从上面可以看到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 )
可以看到是由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函数的后两个参数,来看究竟是将其转换成了上面哪个结构体.
从这里可以看到我们这个例子是将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 ())
跟踪收包函数
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 )
这里同样在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
从上面可以看到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 ())
Https
客户端代码
在AndroidManifest.xml文件赋予权限:
1
<uses-permission android:name= "android.permission.INTERNET" />
布局
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>
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层抓包
观察上述开发流程,我们发现以下几个关键函数:
URL类的构造函数,其包含了目标网址的字符串
收包函数,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的具体实现类为com.android.okhttp.internal.huc.HttpsURLConnectionImpl
接下来跟踪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
对于上面的代码,看起来是有点懵的,那该如何弄呢?
我们可以换种思路,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
前期研究
点击App界面上的发送请求按钮后,使用Objection搜索内存中Socket相关的类
1
2
objection.exe -g com.example.luohttps explore
android hooking search classes socket
将搜索出来的结果复制到1.txt文件中,并在每一行前添加android hooking watch class
结束上述App进程,利用objection -c参数批量Hook Socket相关类(如果出现崩溃现象,可以删除或将导致崩溃的类移到另一个文件中待下次执行即可)
1
objection.exe -g com.example.luohttps explore -c .\1.txt
点击App界面上的"发送请求"按钮,会发现下图中的几个与Socket相关的类被调用了
观察上图,发现上面两个看着很可疑的函数,可以进一步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 ())
跟踪发包函数
接下来跟踪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 ())
跟踪收包函数
接下来跟踪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 ())
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 ())
参考链接
<安卓Frida逆向与抓包实战>
r0capture