要解决的问题:
如何对自定义协议进行逆向分析?
发送参数被加密,如何快速完成参数处理流程的定位?
加密算法复杂,如何主动调用完成对数据包的处理和重放?
逆向分析思想:
堆栈回溯思想: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) 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) 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 ]) 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 ]) 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 ){ 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 ]) 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 ]) 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 ]) 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协议来通信,这就可以用到上面的知识,分三种情况:
Java层Socket通信hook点(socketRead0()
、socketWrite0()
)
JNI层Socket通信hook点(sendto()
、recvfrom()
)
通过系统调用发送接收数据包(自编译openssl)
课时9我称为封神!建议反复观看!
基于unicorn框架:unidbg、AndroidNativeEmu