2021-09-03

Big data security introduction Android r0capture source code key point tracking

  • Mobile phone environment: Android 8.1.0
  • frida_version:12.8.0

The focus of this article

This article mainly introduces how to discover a Hook point in the packet capture part of the r0capture project. You can go to the r0capture project address to download and experience: https://github.com/r0ysue/r0capture

Introduction to r0capture

  • Android platform only, available for testing Android 7, 8, 9, 10, 11;
  • Ignore all certificate verification or binding, do not consider any certificate;
  • Kill all protocols in the application layer in the TCP/IP four-layer model;
  • Killing protocols include: Http, WebSocket, Ftp, Xmpp, Imap, Smtp, Protobuf, etc., and their SSL versions;
  • Kill all application layer frameworks, including HttpUrlConnection, Okhttp1/3/4, Retrofit/Volley, etc.;
  • Ignore the reinforcement, whether it is the overall shell or the second-generation shell or VMP, do not consider the reinforcement;

We can see that r0capture has many advantages. When we use other packet capture software, it is easy to be tortured by certificates, and there are various detection methods for packet capture. At this time, we can use r0capture to hook and capture packets. , ignoring various certificate issues. So today we will take a look at how r0capture is written.

Use Socket for Http access

Why use Socket for Http access? 通杀It is not easy to achieve these two words, because the platform version will be updated and iterated, and many apps will choose to use third-party frameworks to make network requests, with different versions and different versions. So if we want to find a common point, we need to find it from a lower level. And Http is an application built on top of the Tcp protocol, so we have to start from Tcp, no matter how excellent the framework is, it is inseparable from the network interface provided by the system to send and receive data.

Below we give an example of using Java for Socket for Http access:

//http://www.dtasecurity.cn:18080/demo01/getNotice
private void request() {
    
    
 try {
    
    
 final String host = "www.dtasecurity.cn";
 final int port = 18080;
 final String path = "/demo01/getNotice";
 
 Socket socket = new Socket(host,port);

 StringBuilder sb = new StringBuilder();
 sb.append("GET "+path+" HTTP/1.1\r\n");
 sb.append("user-Agent: test\r\n");
 sb.append("Host: "+host+"\r\n");
 sb.append("\r\n");
 Log.d("DTA===>", sb.toString());

 OutputStream outputStream = socket.getOutputStream();
 outputStream.write(sb.toString().getBytes());

 InputStream inputStream = socket.getInputStream();
 byte[] buffer = new byte[1024];
 int len;
 while( ( len = inputStream.read(buffer,0,buffer.length) ) != -1 ){
    
    
 Log.d("DTA===>", new String(Arrays.copyOf(buffer,len)));
        }
    }catch (Exception e){
    
    
 e.printStackTrace();
    }
}

First create a Socket object, specify the Server we want to access through host and port, then construct an HTTP request message according to the format agreed by the Http protocol, send the constructed data through the Socket pipeline, and then read the server Return the data and print the Log.

The program is relatively simple, but it can reflect that the most basic work to be done for an Http request is to create a Socket object, and then construct an Http message to send. Then we will use this code to analyze the flow of our data message. Where, and where can make a better Hook point.

Tracking of Http request key points

Let’s clarify our goals first, and analyze them according to the goals. What we want to do is a Hook packet capture, right, so we just want to intercept the plaintext data of the HTTP(S) request made by the APP. This is a core point of the Hook packet capture, because we need to capture the data in any plaintext state. DUMP. Then just follow the data packet we constructed and see where it flows

OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());

The write method of the OutputStream class is called, and the data message is passed in, but the OutputStream here is an abstract class, so we need to see which specific class the outputStream object is. We can make a breakpoint in this line, and we can directly see to the type of the current object

We can see that the specific implementation class is the SocketOutputStream class, then we will go directly to an implementation of the write method of this class

public void write(byte b[]) throws IOException {
    
    
 socketWrite(b, 0, b.length);
}

The socketWrite method is called again

private void socketWrite(byte b[], int off, int len) throws IOException {
    
    
 if (len <= 0 || off < 0 || len > b.length - off) {
    
    
 if (len == 0) {
    
    
 return;
        }
 throw new ArrayIndexOutOfBoundsException("len == " + len
                + " off == " + off + " buffer length == " + b.length);
    }
 FileDescriptor fd = impl.acquireFD();
 try {
    
    
 // Android-added: Check BlockGuard policy in socketWrite.
 BlockGuard.getThreadPolicy().onNetwork();
 socketWrite0(fd, b, off, len);
    } catch (SocketException se) {
    
    
 if (se instanceof sun.net.ConnectionResetException) {
    
    
 impl.setConnectionResetPending();
            se = new SocketException("Connection reset");
        }
 if (impl.isClosedOrPending()) {
    
    
 throw new SocketException("Socket closed");
        } else {
    
    
 throw se;
        }
    } finally {
    
    
 impl.releaseFD();
    }
}

First, an out-of-bounds and length check is performed on the data. If the length is 0, return directly. If the access is out of bounds, an exception is thrown directly. The key point is the socketWrite0 method. In the process of reading the source code, we must master a method and read the source code with a purpose. For example, here we want to observe the flow of data, so what we need to care about is in which methods our data is processed. passed on. Let's look at an implementation of socketWrite0

private native void socketWrite0(FileDescriptor fd, byte[] b, int off,int len) throws IOException;

Here is a native native method, we will not continue to follow, because a Socket data in the Java layer will eventually pass through here to the native layer, so our Hook method can realize the DUMP of the Java layer Socket data, here It is also the first Hook point of r0capture, let's write a code to test it

Frida Hook socketWrite0

First of all, we want to print the byte[] data, how to print the byte array in frida? We can use a tool class in the framework layer to help us print

function hexdump(bytearry,offset,length){
    
    
 // bytearray => [B
 // offset => I
 // length => I
 var HexDump = Java.use("com.android.internal.util.HexDump")
 console.log(HexDump.dumpHexString(bytearry,offset,length))
}

With the above hexdump method, let's continue to hook socketWrite0 method

Java.use("java.net.SocketOutputStream").socketWrite0.implementation = function(fd,bytes,off,len){
    
    
 hexdump(bytes,off,len)
 this.socketWrite0(fd,bytes,off,len)
}

It can normally hook to the Http request message and perform DUMP, which also achieves our goal

Frida Hook socketRead0

With the above example, we can capture the Http request, now let's look at the Http return data.
We can find the socketRead0 method in the same way as above to find the socketWrite0 method. They are a pair, and we can guess it, so just try the Hook directly.

 Java.use("java.net.SocketInputStream").socketRead0.implementation = function(fd,bytes,off,len,timeout){
    
    
 var result =  this.socketRead0(fd,bytes,off,len,timeout)
 hexdump(bytes,off,len)
 return result
    }

There is no problem, it can be hooked. But here we have to deal with a problem. If the read data exceeds the length of the buffer, it needs to be read multiple times, which will cause the data to be discontinuous. But it doesn't matter, because what we need to pay more attention to is text data, and data like a picture generally exceeds the length of the buffer. And the len here is always the length of the buffer, so here we need to deal with it specially. The result is an int type data, which indicates the current read size.

Java.use("java.net.SocketInputStream").socketRead0.implementation = function(fd,bytes,off,len,timeout){
    
    
 var result = this.socketRead0(fd,bytes,off,len,timeout)
 hexdump(bytes,off,result)
 return result
        }

In this way, our return data can be DUMP out, and the data part of Http is over. But our r0capture also has two important information:

  • Printing the call stack
    The printing of the call stack is very simple. Let's look at the implementation directly. Add this method directly where needed to print the call stack of the current method.
function showStacks() {
    
    
 console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}
  • Print the address of the machine and the address of the server
    Here we can view the SocketOutputStream class and SocketInputStream class, both of which have a member variable socket, which saves our current socket connection, and we can directly obtain the Socket through the variable socket The addresses of both ends of the connection
function printAddress(socket, isSend){
    
    
 var localAddress = socket.value.getLocalAddress().toString()
 var remoteAddress = socket.value.getRemoteSocketAddress().toString()
 if(isSend){
    
    
 console.log(localAddress +"====>"+ remoteAddress)
            }else{
    
    
 console.log(localAddress +"<===="+ remoteAddress)
            }
        }

So far, our Http part has been analyzed, and then we will look at the Https part

Use Socket for Https access

private void requestHttps() {
    
    
 try {
    
    
 final String host = "www.taobao.com";
 final int port = 443;
 final String path = "/";

 SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
 SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(host,port);

 StringBuilder sb = new StringBuilder();
 sb.append("GET "+path+" HTTP/1.1\r\n");
 sb.append("user-Agent: test\r\n");
 sb.append("Host: "+host+"\r\n");
 sb.append("\r\n");
 Log.d("DTA===>", sb.toString());
 OutputStream outputStream = socket.getOutputStream();
 outputStream.write(sb.toString().getBytes());

 InputStream inputStream = socket.getInputStream();
 byte[] buffer = new byte[1024];
 int len;
 while( ( len = inputStream.read(buffer,0,buffer.length) ) != -1 ){
    
    
 Log.d("DTA===>", new String(Arrays.copyOf(buffer,len)));
        }
    }catch (Exception e){
    
    
 e.printStackTrace();
    }
}

We can find that it is different from Http:

  • The port is different from ordinary ports. The default port for Http is port 80, and the default port for Https is port 443.
  • Use SSLSocket instead of Socket
  • SSLSocket is an abstract class that inherits from the Socket class. So you can't directly new, you need to use the SSLSocketFactory factory class to create an SSLSocket connection

Tracking of Https request key points

Similarly, let's take a look at the specific implementation class of the outputStream object at the next breakpoint

Looking at the implementation of the write method corresponding to the internal class SSLOutputStream class under the ConscryptFileDescriptorSocket class, we found that the internal class did not implement the write method we called, which was inherited from the parent class

public void write(byte b[]) throws IOException {
    
    
 write(b, 0, b.length);
}

The write method with three parameters is still called, and the SSLOutputStream class rewrites this method. Let's look at the implementation

http://aospxref.com/android-8.1.0_r81/xref/external/conscrypt/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java

public void write(byte[] buf, int offset, int byteCount) throws IOException {
    
    
 Platform.blockGuardOnNetwork();
 checkOpen();
 ArrayUtils.checkOffsetAndCount(buf.length, offset, byteCount);
 if (byteCount == 0) {
    
    
 return;
    }

 synchronized (writeLock) {
    
    
 synchronized (stateLock) {
    
    
 if (state == STATE_CLOSED) {
    
    
 throw new SocketException("socket is closed");
            }
 if (DBG_STATE) {
    
    
 assertReadableOrWriteableState();
            }
        }

 ssl.write(Platform.getFileDescriptor(socket), buf, offset, byteCount,
                writeTimeoutMilliseconds);

 synchronized (stateLock) {
    
    
 if (state == STATE_CLOSED) {
    
    
 throw new SocketException("socket is closed");
            }
        }
    }
}

The key method ssl.write. The first parameter is a file descriptor of our socket. The next few parameters do not need to be introduced too much. There is an additional timeout parameter. Let's continue to look at the implementation of this method

http://aospxref.com/android-8.1.0_r81/xref/external/conscrypt/common/src/main/java/org/conscrypt/SslWrapper.java

void write(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)throws IOException {
    
    
 NativeCrypto.SSL_write(ssl, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
}

The NativeCrypto.SSL_write method is called again, continue to follow

Frida Hook SSL_write method

http://aospxref.com/android-8.1.0_r81/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) throws IOException;

This method is a native method, and we still can't follow it down. In the end, the https data comes here, and there is no link to encrypt the data in the middle, so our Https data is still in plaintext here. As for how the native layer handles it, it is nothing more than an ssl layer processing of what we call https, so this point is that the Https data is still in the plaintext state, the last point we can find in the Java layer, let’s come Hook this method. But here is one thing to note, the package name of this type cannot be used directly, it needs to be prefixed with com.android, as for why the author is not clear, it is estimated that all package names under this module are automatically added when the source code is compiled a prefix. As for why I know that the com.android prefix is ​​added, it is also based on the above breakpoint picture, com.android.org.conscrypt.ConscryptFileDescriptorSocket, there is a com.android in front of the fully qualified class name of this class, and when reading the source code In the process, there is also no such prefix. Also, I used Objection to search the class name from memory, and it does have this prefix. So let's continue the Hook method

Java.use("com.android.org.conscrypt.NativeCrypto").SSL_write.implementation = function(sslNativePointer,fd,shc,bytes,off,len,timeout){
    
    
 printHttpsAddress(fd,true)
 hexdump(bytes,off,len)
 showStacks()
 return this.SSL_write(sslNativePointer,fd,shc,bytes,off,len,timeout)
}

Others need no introduction, let's take a look at the printHttpsAddress method

function printHttpsAddress(fd, isSend){
    
    
 var local = Socket.localAddress(fd.getInt$())
 var peer = Socket.peerAddress(fd.getInt$())
 if(isSend){
    
    
 console.log(local.ip+":"+local.port +"====>"+ peer.ip+":"+peer.port)
    }else{
    
    
 console.log(local.ip+":"+local.port +"====>"+ peer.ip+":"+peer.port)
    }
}

This is different from the previous analysis method. The NativeCrypto class does not have the member variable socket, and we can see from the analysis process that when the SSL_write method is reached, the parameters related to the socket are only one sslNativePointer and fd. Here we are Through this fd, the API provided by frida is called to help us resolve to local and peer, thus realizing the function of printing the address

Frida Hook SSL_read method

Both write and read appear in pairs

static native int SSL_read(long sslNativePointer, FileDescriptor fd, SSLHandshakeCallbacks shc, byte[] b, int off, int len, int readTimeoutMillis) throws IOException;

Come directly to the Hook method

Java.use("com.android.org.conscrypt.NativeCrypto").SSL_read.implementation = function(sslNativePointer,fd,shc,bytes,off,len,timeout){
    
    
 var result =  this.SSL_read(sslNativePointer,fd,shc,bytes,off,len,timeout)
 printHttpsAddress(fd,false)
 hexdump(bytes,off,len)
 showStacks()
 return result
}

Summarize

So far we have analyzed how the four hook points of the r0capture packet capture function come from. They all start from a bottom socket, make Http(s) requests, and then follow the data step by step. This is also our static analysis of a process. We can also find the disadvantages of r0capture in the process of finding key points. As long as we do not go through the data of these four methods, we will not be able to capture. Let's take a look at r0ysue's summary of the limitations of r0capture in github:

Some large companies or frameworks with strong development capabilities use their own SSL frameworks, such as WebView, applets, or Flutter, which are currently not supported. Some integrated apps are not Android apps in essence, and cannot be supported because they do not use the framework of the Android system. Of course, this part of App is also a minority. It does not support HTTP/2 or HTTP/3 for the time being. This part of the API has not yet been popularized or deployed on the Android system. It is built-in by the App and cannot be used for general hooking. The architecture, implementation, and environment of various simulators are relatively complex. It is recommended to cherish life and use real devices.

This is just an implementation of the packet capture idea, which has its advantages and disadvantages. Therefore, we have to start from our purpose and find out which tools are more suitable for our needs in order to complete the work efficiently.

If this article is helpful to you, you are welcome to add our planet and share various technologies from time to time. There is always one that is helpful to you. Thank you for reading

Guess you like

Origin blog.csdn.net/u010559109/article/details/120082066