前面写了篇文章《使用iptables为何不能将外部进入的包NAT到127.0.0.1》,牵扯到了很多知识,最终的结论就是不能那么做。这个结论让人有些不舒服,说了半天就是阐述它为何做不到,如果我非要将包NAT到loopback呢。比如为了不将端口以及地址暴露给外部,我就是想让代理服务侦听127.0.0.1这个地址,有没有什么办法做到呢?
因为你用的是Linux,答案无疑是肯定的,所要做的无非就是把路由时候的限制条件给去掉。我们知道,在进和出两个方向,有两个路由查找逻辑,一个是
ip_route_input_slow,另一个是ip_route_output_slow,其中前一个函数的限制在:
if (ipv4_is_lbcast(daddr) || ipv4_is_zeronet(daddr) ||
ipv4_is_loopback(daddr))
goto martian_destination;
而后一个函数限制在其调用的__mkroute_output中:
if (ipv4_is_loopback(fl->fl4_src) && !(dev_out->flags&IFF_LOOPBACK))
return -EINVAL;
我们到底应该怎么绕过去呢?如今我们知道,NAT逻辑已经将目标地址改成了127.0.0.1,而源地址保持不变,这种数据包在进入的时候会被ip_route_input_slow拦截并且由于源地址非loopback而目标为loopback而丢弃,丢弃的时候注意还没有查找路由呢?现在我们假设这个包已经过去了,那么返回包能不能顺利通过ip_route_output_slow呢?答案无疑也是否定的,由于标准路由是基于目标地址的,对于返回包的目标地址其实就是正向包的源地址,路由查找无疑是可以通过的,但是路由前由于还不知道目标设备,因此不能简单地丢弃,只有到了路由之后确定目标设备非loopback之后才能丢弃。
以上就是Linux协议栈路由模块对待“火星地址”的逻辑。
总结一下就是:
1.对于进入包,若是loopback发来的包则不通过路由查找逻辑,凡是通过路由查找的,都是外来包,路由前知道源地址和目标地址,可以根据目标地址是否loopback而判断是否丢弃;
2.对于发出包,无条件(考虑下路由cache,意义是一样的)都要经过路由查找,路由前不一定知道源地址,即使知道源地址也不能确定目标设备是否是loopback设备,只有通过路由查找(local表命中的目标设备为loopback设备)才可以知道全部信息,因此在路由后,生成路由cache之前判断源地址是loopback地址而却是发往非loopback设备而决定丢弃之。
现在我们已经知道了大致的逻辑,我们分两步来绕开它们。
0.需要做的工作
我们需要修改NAT内核模块,涉及修改的有$KERL/net/ipv4/netfilter/nf_nat_standalone.c这个文件。
1.绕开对进入的正向包的地址限制
此即绕开ip_route_input_slow的限制,当然修改协议栈代码是最有效的,可是那样需要重新编译kernel,于是我们在DNAT之后就直接将一个dst_entry附着在这个skb上,以便协议栈认为已经查找过了路由而不再经过路由查找逻辑从而绕开上述的第一个限制。由于对于所有被DNAT到127.0.0.1的包都使用同一个dst_entry,因此我们只需要在系统中保留一份即可:
struct rtable *dummy_rth = (struct rtable*)kmalloc(sizeof(struct rtable), GFP_ATOMIC);
注意,如果是准备在insmod的时候,也就是调用module的init时生成这个rtable的话,大可不必使用GFP_ATOMIC标志。
然后我们在nf_nat_standalone_init中填充其字段:
struct dst_entry dst = dummy_rth->u.dst;
memset(&dummy_rth->u.dst, 0, sizeof(struct dst_entry));
dummy_rth->u.dst.ops = NULL;
dummy_rth->u.dst.path = &dst;
dummy_rth->u.dst.flags= DST_HOST;
dummy_rth->u.dst.flags |= DST_NOXFRM;
dummy_rth->u.dst.flags |= DST_NOPOLICY;
dummy_rth->u.dst.dev = init_net.loopback_dev;
dummy_rth->rt_iif = init_net.loopback_dev->ifindex;
dummy_rth->rt_type = RTN_LOCAL;
dummy_rth->u.dst.input = (int (*)(struct sk_buff*))0xffffffff812734eb; //ip_local_deliver的地址
atomic_set(&dummy_rth->u.dst.__refcnt, 1);
接下来,我们需要在每一个被DNAT到loopback的数据包上应用上述的rtable,那么在哪里应用呢?很显然是在PREROUTING这个HOOK点的NAT之后了,也就是nf_nat_in钩子函数里面:
static unsigned int
nf_nat_in(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
unsigned int ret;
__be32 daddr = ip_hdr(skb)->daddr;
ret = nf_nat_fn(hooknum, skb, in, out, okfn);
if (ret != NF_DROP && ret != NF_STOLEN &&
daddr != ip_hdr(skb)->daddr)
skb_dst_drop(skb);
if (daddr != ip_hdr(skb)->daddr && ipv4_is_loopback(ip_hdr(skb)->daddr)) {
atomic_inc(&dummy_rth->u.dst.__refcnt);//增加一个引用计数。
skb_dst_set(skb, &dummy_rth->u.dst); //将rtable的dst_entry附着到skb上。
}
return ret;
}
以上就完成了正向包的路由前火星地址检查,注意我们并不打算将上述的dummy_rth插入到系统的路由cache哈希表中,大多数的字段也没有填充,这是因为我们从来不想把它用作什么路由cache,也从不想将其删除,也不想被常规的管理,我们只是在NAT模块还在的时候使用它将数据包路由到本地传输层或者以上而已。
2.绕开对返回包的地址限制
和第1点一样,我们也可以通过修改__mkroute_output的源码来做到这一点,然而这不是什么好方法,比较好的方法的思路和第1点类似,那就是通过增加一个rtable来绕开标准的路由查找,然而却比上述第1点要难很多,这是因为这个rtable不能上上面那样做一个dummy,毕竟来源地址可能是不同的,并且几乎所有rtable的字段都需要,这次它是真正被用作路由的,而不是仅仅将述举包导入本地,这就涉及更底层的诸如arp,neighbour等问题了,另外,这个rtable由于不唯一,不固定,因此最好将其纳入到系统的路由cache哈希表中管理。
具体来讲就是在nf_nat_in中再添加一个逻辑:
if (daddr != ip_hdr(skb)->daddr && ipv4_is_loopback(ip_hdr(skb)->daddr)) {
struct rtable *rev_dummy = dst_alloc(&ipv4_dst_ops);
int err;
unsigned hash;
atomic_inc(&dummy_rth->u.dst.__refcnt);//增加一个引用计数。
skb_dst_set(skb, &dummy_rth->u.dst); //将rtable的dst_entry附着到skb上。
//以下构造返回包使用的rtable,注意是反向的,源地址就是目标地址,目标地址成了源地址
atomic_set(&rev_dummy->u.dst.__refcnt, 1);
rev_dummy->u.dst.flags= DST_HOST;
rev_dummy->u.dst.flags |= DST_NOXFRM;
rev_dummy->u.dst.flags |= DST_NOPOLICY;
rev_dummy->fl.fl4_dst= ip_hdr(skb)->saddr;
rev_dummy->fl.fl4_tos= (u32)(RT_TOS(ip_hdr(skb)->tos) & (IPTOS_RT_MASK | RTO_ONLINK));;
rev_dummy->fl.fl4_src= ip_hdr(skb)->daddr;
...
rev_dummy->rt_dst= ip_hdr(skb)->saddr;
rev_dummy->rt_src= ip_hdr(skb)->daddr;
hash = rt_hash(ip_hdr(skb)->saddr, ip_hdr(skb)->daddr, 0,
rt_genid(dev_net(skb->dev)));
err = rt_intern_hash(hash, rev_dummy, rev_dummy, NULL);
}
如此一来也就成了,具体的测试方式很简单,那就是写一个侦听127.0.0.1:abc的TCP程序,重新编译并加载nf_nat.ko以及iptable_nat.ko模块,然后设置以下的规则:
iptables -t nat -A PREROUTING -p tcp --dport 1234 -j DNAT --to-destination 127.0.0.1
即可。
另外如果对Windows上做hack比较熟悉的话,还有一种更加高效的方式来处理上述第2点,那就是“二进制重置”,通过内核符号表找到__mkroute_output的地址,并且通过特征码找到:
if (ipv4_is_loopback(fl->fl4_src) && !(dev_out->flags&IFF_LOOPBACK))
if (ipv4_is_loopback(fl->fl4_src) && !(dev_out->flags&IFF_LOOPBACK))
这个判断,然后用二进制指令覆盖的方式将其重写,直接跳过或者通过增加一个标志判断一下是否是DNAT到loopback的返回包,然后根据判断结果进行抉择...
附:调试中遇到的问题
1.由于是在VMWare虚拟机里面调试的,而且没有安装X系统和VMWare-tools,开始的时候由于rtable的字段没有设置全,比如dev字段没有设置,而后面的tcp处理逻辑还需要这个字段,从而导致了系统panic,打印出了一堆trace,然而由于虚拟机里面分辨率很低,只能显示N行,N行上面的都看不到,而且用sysrq也没有效果,于是想办法提高终端的分辨率,google了一下,发现使用grub的kernel中的vga内核启动参数即可设置,设置了之后,马上看出了问题之所在。
2.很多符号没有导出来,因此必须使用其地址,还要强制转化一下。当然想办法使用导出的符号也不是不可的,然而却需要做更多的工作,比如链接特定的obj文件等,索性还是直接使用地址吧...
3.其实想用kprobe了,然而不知怎么回事总感觉那是Windows程序员的风格,再说又不是没有别的办法,不到万不得已,不用这种。(难道为了将二进制覆盖技术练得炉火纯青,哥必须把所有机器的汇编都掌握了吗?实际上,哥只会x86上的-以前搞Windows的时候学的,x86-64的都不怎么懂啊)