网络层拦截可选项

1,130 阅读6分钟

转贴自我家领导的博客:martianpuss-blog.tumblr.com/post/158111…

【1】客户端应用层实现 -> 【2】客户端网络库实现 -> 【3】系统库libc.so -> 【4】(从这里开始是内核态) syscall -> 【5】tcp/ip协议栈 -> 【6】NFQUEUE-> 【7】BPF -> 【8】tun adapter (假网卡) -> 【9】真网卡 -> 【10】外部网络设备

【1】客户端应用层实现

client可以在应用层直接支持proxy: 链接模块直接实现Proxy(Request)并发送到proxy,libcurl是这样做的, 也可以交给更底层的模块去处理, 在这之前, 先列出一个ip包在linux系统下应用层调用send()接口后经历哪些模块到达网卡, 可能会不完整, 仅列出下文会引用到的, 模块之间可理解为调用关系:

想要把本来要发给server的包发送给proxy, 除了应用层自己在发送时修改包和目标地址, 还可以在包到达真网卡之间的某一层拦截并且修改包内容, 这样做的好处有两点: 应用层模块不必关心代理细节甚至不知道是否代理, 代码更简洁; 在远离应用层的代理实现可以被不同的进程加载, 甚至可以同时应用在全局所有进程, 彻底对上层透明.

【2】客户端网络库实现

libcurl等网络库都有良好的(特别是Http)的代理支持, 在应用层设置代理服务器后可以无缝使用, python, java 使用的网络库也有代理支持. 从使用者的角度来看无需详述. 提到这一层是因为无论是libcurl, 还是python的网络模块都还不会直接发数据到网卡, 除了go以外大家普遍使用libc.so的send()接口去发送数据到套接字, 抛开go不谈, 我们来看libc.so上可以做什么.

【3】系统库libc.so

网络库也不是自己真正去发包的, 而是调用系统库send()方法去发送. 可以在进程加载时通过更改环境变量LD_PRELOAD加载一个假的lib_fake_c.so让进程当成libc.so使用. 在这个假的lib_fake_c.so里拦截send()等socket io接口, 封装代理包并调用真的libc.so的send()接口发送给proxy, 就可以不修改产品代码直接使用代理了. 这个假的libc.so不光可以让多个产品透明地使用同一份代理实现, 而且在他们不想使用代理时不需要重新编译, 重新启动, 加载真的libc.so就可以切换回正常的网络使用.

  • proxychains的实现就是使用这种方法在终端进行代理的.
  • 微信的后台libco.so也是用这种方法实现拦截libc接口的.

进程启动时:

export LD_PRELOAD="./lib_fake_c.so"
./my_service

lib_fake_c.so

#define HOOK_SYS_FUNC(name) if( !g_sys_##name##_func ) { g_sys_##name##_func = (name##_pfn_t)dlsym(RTLD_NEXT,#name); }

int close(int fd)
{
    HOOK_SYS_FUNC( close );

    if( !co_is_enable_sys_hook() )
    {
        return g_sys_close_func( fd );
    }

    free_by_fd( fd );
    int ret = g_sys_close_func(fd);

    return ret;
}

【4】syscall

其实libc.so还在用户态运行, 操作系统内核和用户进程之间有一套接口叫做syscall, libc调用syscall进入内核态操作socket, (send() (用户态) -> syscall (中断>>send对应的操作接口>>内核) -> ...). 在内核态还有一个接口叫做ptrace, 它可以拦截syscall 在内核中对socket操作(其实是可以拦截所有内核态操作, 此处只关心send), 拦截了以后切换回用户态, ptrace的实现就像观察者模式一样, 收到了关心的观察信号后在用户态对这个send操作做修改, ptrace的返回会切换回内核态继续发送这个篡改过的内容. 从内核态-用户态-内核态的操作不难推断出, 这种方法是有一定性能损耗的, (todo) gdb就是这种方法拦截所有内核态的操作供调用者单步调试, 在debug的环境下性能损耗可以无视, 在生产环境用这种方法来做代理就需要斟酌了. 我觉得不会有人用这种方式实现代理的.

【5】tcp/ip协议栈

syscall之后这个包的操作就到达了协议栈. 协议栈上的网络包的操作都会经过iptables的处理, iptables的一个著名功能就是ip包的转发规则nat拦截应用层的链接. 我们在这一层可以做的事情就是转发应用层的某个链接的包到本地hack_server或者异地hack_server, 在这个hack_server里收到被转发来的包, 读取它的真实src和真实dst, 进行代理协议封装后发送给proxy.

iptables -t nat -I OUTPUT -p tcp ! -s 10.1.2.3 -j DNAT --to-destination 10.1.2.3:8319
iptables -t nat -I PREROUTING -p tcp ! -s 10.1.2.3 -j DNAT --to-destination 10.1.2.3:8319

【6】NFQUEUE

iptables除了可以把包转发给特定地址特定端口的监听服务外, 还可以转发到一个叫做NFQUEUE的目标上,

iptables -A INPUT -j NFQUEUE --queue-num 0

在包被转发到queue num=0 NFQUEUE队列之后, 某个使用libnetfilter_queue 连接队列0的进程从内核态获取到包的信息, 对包的去向进行裁决. NFQUEUE之后的工作是在用户态完成的, 所以这里也有相应的性能损耗. fqting使用了NFQUEUE对包的内容进行了混淆, 混淆后的包的参数使之在IDS上重组时产生错位, 重组流不为中间路由所知, 在proxy ip不被封禁的情况下可以绕过封禁内容的防火墙.

【7】BPF

在正常情况下, 下一步协议栈就会把包发给网卡驱动了, 但是在有BPF监听的情况下, 网络包会被发送给BPF进行筛选, 并把符合筛选条件的包拷贝到过滤条件对应的进程的缓存. tcpdump从这个缓冲区里读出包的内容, 但是不能修改, 因为该缓冲区的数据是内核数据包的拷贝, 修改它并不能影响内核中的数据.

【8】虚拟网卡

协议栈后网络包被发送给网卡驱动, 此处可以使用tun/tap驱动, tun像一个网卡那样接收tcp/ip协议栈处理好的网络分包, 但并不真正发送, 而是转而把这个网络包发送给任何一个使用tun/tap驱动的进程,由进程重新处理后再发到物理链路中, tap的工作和tun的方向相反. 通过这种方式在监听进程中可以改变网络包发送给proxy. OpenVpn就是这种方式实现的全局代理.

【9】真网卡

driver?

【10】外部网络设备

arp?
dhcp?
dns?