trace socket
TCP trace
首先我们用flask搭建一个小型服务器供我们收发手机的请求,此处上网随便扒拉一段代码直接run就可以启动了
1 | from flask import Flask |
接着写个程序用于收发TCP socket到这个服务器上,具体过程略
这里创建一个线程,通过new Socket(ip, port)的形式生成一个Socket对象,并获取其OutputStream,InputStream,这两个对象用于后续收发包,要细讲太浪费时间了略过,直接发送一个包。
连接
以下堆栈都在java.net.Socket里
new Socket(ip, port) -> public Socket(String host, int port) -> private Socket(InetAddress[] addresses, int port, SocketAddress localAddr, boolean stream) throws IOException ->new SocksSocketImpl()
这个SocksSocketImpl是Socket类的一个属性,但是具体的操作都是由其完成的。
其创建位置就在上图框住的位置。我们接着走到connect那里,也就是上图的位置,来到这里
public void connect(SocketAddress endpoint) throws IOException -> connect(endpoint, 0); -> public void connect(SocketAddress endpoint, int timeout) throws IOException ->impl.connect(epoint, timeout) -> protected abstract void connect(SocketAddress address, int timeout) throws IOException;
到这里是一个抽象方法,不可能通过这个抽象方法来执行的,这里SocksSocketImpl就开始起作用了。
以下堆栈都在java.net.SocksSocketImpl里
protected void connect(SocketAddress endpoint, int timeout) throws IOException 跟进来
先找一找具体的逻辑。上面这部分没执行
F7步进的时候发现跳到了这里来了protected void connect(SocketAddress var1, int var2) throws IOException
这里函数是在AbstractPlainSocketImpl类里,SocksSocketImpl的父类PlainSocketImpl
继承了这个方法,也就是说
这里被调用了!
然后跳转到java.net.AbstractPlainSocketImpl里的这个方法this.connectToAddress(this.address, this.port, var2);
这里也能作为一个小的hook点,用来获取连接的地址和端口。该方法又会跳转到this.doConnect(InetAddress.getLocalHost(), var2, var3);
-> synchronized void doConnect(InetAddress var1, int var2, int var3) throws IOException
该类又会先检查是否有Tcp已经在连接,若有直接进行复用。
public static void beforeTcpConnect(FileDescriptor var0, InetAddress var1, int var2) throws IOException -> provider.implBeforeTcpConnect(var0, var1, var2);
接着会跳转到provider的实现类sun.net.sdp.SdpProvider
public void implBeforeTcpConnect(FileDescriptor var1, InetAddress var2, int var3) throws IOException ->
检查完之后,倘若没有则会使用java.net.PlainSocketImpl里的
this.socketConnect(var1, var2, var3) -> native void socketConnect(InetAddress var1, int var2, int var3) throws IOException;
以上就是Java层连接的大致流程,简要的分析下可以找到很多hook点,这些hook点主要用来hook连接的ip和port,实际上只需要hook java.net.PlainSocketImpl.socketConnect即可得到,因为这是必经之路是java和jni的分界点,但是如果不走java层的话就不会走上面的这些点,但是jni层也有很多这样的点可以hook到,可以说跟脱壳一样,存在海量的hook点,对抗将一直下去。
Hook一下证明这个点是可以hook到的。
哈哈,没问题~
发送数据
outputstream.write(crypt.getBytes(“utf-8”));
|
V
public void write(byte[] var1) throws IOException
|
V
private void socketWrite(byte[] var1, int var2, int var3) throws IOException
|
V
private native void socketWrite0(FileDescriptor var1, byte[] var2, int var3, int var4) throws IOException;
上面是粗略的调用栈,具体是哪里去实现并发送的呢?刚才上面实际上也说了Socket类的具体发送实际上是OutputStream,InputStream,这两个对象用于后续收发包。我们可以先去看看SocksSocketImpl中的OutputStream
看截图,要获取OutputStream可以从这个函数入手,而这个函数实际上返回的是一个java.net.SocketOutputStream
类,那我们就按上面的发包处看。
public void write(byte[] var1) throws IOException -> this.socketWrite(var1, 0, var1.length); -> private void socketWrite(byte[] var1, int var2, int var3) throws IOException -> this.socketWrite0(var4, var1, var2, var3); -> private native void socketWrite0(FileDescriptor var1, byte[] var2, int var3, int var4) throws IOException;
好家伙,又遍历到java层到native层的分界处了,那理论上来说,这个就是必经的发包处了(除非直接调jni或者系统的函数)!
hook之~
down!
附赠此处完整代码
1 | function hook(){ |
分析是这么个分析了,read也是这样分析的
接收数据
W/System.err: java.net.SocketTimeoutException: Read timed out
W/System.err: at java.net.SocketInputStream.socketRead0(Native Method)
W/System.err: at java.net.SocketInputStream.socketRead(SocketInputStream.java:114)
W/System.err: at java.net.SocketInputStream.read(SocketInputStream.java:170)
W/System.err: at java.net.SocketInputStream.read(SocketInputStream.java:139)
W/System.err: at java.net.SocketInputStream.read(SocketInputStream.java:125)
W/System.err: at com.example.okhttp.TcpClient$3.run(TcpClient.java:108)
W/System.err: at java.lang.Thread.run(Thread.java:764)
关键的代码
1 | private native int socketRead0(FileDescriptor var1, byte[] var2, int var3, int var4, int var5) throws IOException; |
从以上的调用栈我们可以知道hook java.net.SocketInputStream.socketRead0
即可得到信息(除非直接调jni或者系统的函数),就偷懒不写hook代码了,感兴趣的同学可以在找找其他hook点哦~
实际上最近很火的肉师傅的r0capture
也是hook的这两个点,说明这两个点是真的很不错!
var result = this.socketWrite0(fd, bytearry, offset, byteCount);
var result = this.socketRead0(fd, bytearry, offset, byteCount, timeout);
人参数的意思都解释出来了,直接用就完事了~
trace SSL
本次trace采用okhttp3.2.0
okHttpClient.newCall(request); 生成Call对象之后采用异步的形式就用enqueue函数,采用同步就用execute函数。
具体执行是来自Call这个对象,是由OkHttpClient对象newCall生成的Call对象,我们跟进去。
-> return new RealCall(this, request);
然后我们调用了public void enqueue(Callback responseCallback)
; 后面会来到 client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
这里要看看要跟哪里才能找到发包,先到enqueue
是这个executorService()的execute函数执行了call,跟进去看看这个看看ExecutorService是啥
public interface ExecutorService extends Executor,是一个调度器的接口,这里的实现类是由OkHttpClient的内部类Build来创建的实现的。倘若没有在OkHttpClient对象进行Dispatcher赋值,则会默认new Dispatcher()来生成这个对象。
该类的executorService就是以下这些代码。
1 | public synchronized ExecutorService executorService() { |
可以看到这个调度器实际上是一个线程池调度器。其具体的execute函数的逻辑我们可以不去考虑,先看到这个方法的声明:public void execute(Runnable command),其参数是一个Runnable,这里可以跟AsyncCall串起来了final class AsyncCall extends NamedRunnable,实际上AsyncCall就是一个Runnable,其实也就是调用了AsyncCall里的run方法,其实是其父类的run方法。
1 | public final void run() { |
所以可以确定是发送的逻辑在AsyncCall的execute()里面。
1 | protected void execute() { |
也就是这段代码。
看命名可知主要的请求逻辑在这里面
-> getResponseWithInterceptorChain(forWebSocket);
1 | private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException { |
通过监听器来执行。
逻辑在这个ApplicationInterceptorChain
对象里面.
-> public Response proceed(Request request) throws IOException
// No more interceptors. Do HTTP.
return getResponse(request, forWebSocket);
-> Response getResponse(Request request, boolean forWebSocket) throws IOException
engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null);
上面那个函数的上面这段代码超级重要,当时Hook HttpUrlConnection时看到其底层有出现过这个,我们看看下面的代码有调用其sendRequest()
我们跟进去。
下面的代码都在创建Response对象了,说明具体的请求可能就在 Request request = networkRequest(userRequest);
跟进去。
1 | private Request networkRequest(Request request) throws IOException { |
我们看到其实这里只是在构建请求相关的诸如请求头等东西,所以具体的逻辑不在这里,不用往下跟了,往回。
找到这里。 httpStream = connect();
,凭名字盲猜我们想要的东西在里面,越来越接近了。
1 | private HttpStream connect() throws RouteException, RequestException, IOException { |
如果没猜错就是这个了
newStream里!
-> RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
跟进findHealthyConnection
,跟进直到
熟悉么,就是获取SocketImpl的地方。离答案越来越近了。跟进sink()
这里返回的是一个包含完整write方法的一个Sink对象,这里有具体怎么发。我们在往回。
这里有connectTls
看看代码走不走这里!
终于找到tls的握手的地方了!
注意这个对象。ssl相关的对象,
直接在跟进startHandshake()
,见名知意,开始握手。我没找到其具体的实现逻辑,源码只能跟到某个jar包里,跳过这个不分析。
跟到下面
看到截图这些注释,检车证书,然后保存握手,完成这些步骤没报错,则设置success = true
;即为TLS完成。我们要知道okhttp发送实际上是在sink里完成的,source是用来接收的,我们再跟到sink函数里。
终于找到你!!!!这个肯定也必须是发送数据的对象了!搜源码开始!
我搜的是aosp8.1的源码,发现迟迟没有找到OpenSSLSocketImpl的具体实现,结合刚才的CreateSocket找到OpenSSLSocketFactoryImpl
原来OpenSSLSocketImpl在AOSP8.1开始变成了抽象类(AOSP8.0还是普通的类),通过工厂模式对其进行创建。这个真是一个坑,看源码的时候容易掉进去。
创建的时候走的是4个参数的。
也就是这里,所以具体的实现就在这两个当中选的。如果socket当中存在文件描述符则走第一个,没有走第二个,那我们分析随便选一个就行了。毕竟只想要tls握手前的数据。
我们直接找到SSLOutputStream这个内部类,因为后面的具体发送实际上交由这个内部类来完成。
找到三个参数的位置。
HOOK之~
public void write(byte[] b, int off, int len) throws IOException
(中间有个小插曲,我用android7.1.2来hook半天没反应,后面发现自己是真的可爱)
最好看看该APP加载了两个Socket中的哪个。