Archive

Archive for December, 2014

ip分片与重组

December 19th, 2014 No comments

最近在做udp pump, 突然手贱的想要知道一个udp包的最大负载的是多少.
于是写了一个简单的程序来测试了一下, client不断的发送payload size不断增加的包, 然后在server端收数据包. 当然了由于IP的最大payload只有65535-sizeof(IP Header).所以Udp的最大负载也就是只有大约65535-20-8 = 65507个. 如果超过这个大小以后,不是收不到, 而是client根本发不出去这个数据包

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import socket
import random
import string
import sys
import hashlib

def sendData(udp, ip,port, size):
    data = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(size))
    md5 = hashlib.new('md5')
    md5.update(data)
    udp.getsockname()
    udp.sendto(data,(ip,port))
    print "%s :: sending %08d bytes to %s:%d   --> %s" % ( udp.getsockname(), len(data), ip,port, md5.hexdigest())


def run(ip,port,begin,inc,end):
    udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_IP)
    udp.bind(("0.0.0.0",0))
    sendData(udp, ip, port, 65507)
    sendData(udp, ip, port, 65508)

用上面的代码发送可以很轻松的验证,发送65507个字节的时候是OK的,但是65508的数据就会得到Message Too Long的错误.

接下来呢, 我做了一件错误的事情,然后导致了后来一系列的事情的发生.
我在server端使用tcpdump来查看网络数据包, udp server listen在6666端口.所以我使用命令来查看数据包

sudo tcpdump -i any -nnn -vvv "port 6666"

注意我的 tcpdump的filter是”port 6666″,也就是采用过滤端口的方式.这个也就是我想当然的认为ip fragment以后的每一个udp packet的port应该和原始数据是一致.

拿到tcpdump的数据以后我下了一大跳

17:06:09.875525 IP (tos 0x0, ttl 61, id 28605, offset 0, flags [+], proto UDP (17), length 1500)
    192.168.80.233.52099 > 10.15.10.50.6666: UDP, length 65507

请看IP的段显示的数据大小为1500, 这个很容易理解,毕竟大部分的网络的MTU都是1500,这里也不例外.但是UDP段显示的length是65507, 这个也说得过去, 我发送的数据就是这么大的. 但是问题来了,为啥只有这么一个数据包. 于是我猜测抓包显示的数据错掉了.我把抓包数据存下来以后发现文件大小也就是1500多一点大.那么很明显这个数据包里面的数据真的只有1500 bytes. 这里的事情被我认为是非常诡异的, 因为我发送数据的时候对数据做了一个md5,udpserver收到数据以后也会做一个md5. 这两个md5是一模一样的,那么很明显证明了server端收到的数据和client发送的数据是一样的.
但是抓的包却只有1500 bytes啊,剩下的数据死到哪里去了呢?
我一度怀疑tcpdump有bug,于是换成了wireshark(tshark). 但是得到的结果是一样,见鬼了. 于是乎一阵google以后找到了使用raw socket来抓数据包的代码在这里.感谢作者的无私分享. 用这个程序跑了一下,只抓udp的包. 从抓包数据看,端口为6666的仍旧只有1500字节的ip数据, 但是很奇怪的是那些非6666端口的其他数据包里面的数据和我发送的数据十分相似–随机的字符数字组合. 然后我发现了一个东西,也是这个东西让我明白了这到底是怎么回事.

IP Header
   |-IP Version        : 4
   |-IP Header Length  : 5 DWORDS or 20 Bytes
   |-Type Of Service   : 0
   |-IP Total Length   : 1500  Bytes(Size of Packet)
   |-Identification    : 63208
   |-Dont Fragment Field   : 0
   |-More Fragment Field   : 0
   |-TTL      : 62
   |-Protocol : 17
   |-Checksum : 35759

这个让我感觉很奇妙的东西就是Identification. 因为抓包数据中的所有包头都含有相同的Identification. 这个不免让我觉得其实IP分片以后实际上是靠Identification来决定是不是同一个上层数据包的内容的. 于是我查看了一下ip分片的实现

/* Find the correct entry in the "incomplete datagrams" queue for
 * this IP datagram, and create new one, if nothing is found.
 */
static inline struct ipq *ip_find(struct net *net, struct iphdr *iph, u32 user)
{
        struct inet_frag_queue *q;
        struct ip4_create_arg arg;
        unsigned int hash;

        arg.iph = iph;
        arg.user = user;

        read_lock(&ip4_frags.lock);
        hash = ipqhashfn(iph->id, iph->saddr, iph->daddr, iph->protocol);

        q = inet_frag_find(&net->ipv4.frags, &ip4_frags, &arg, hash);
        if (q == NULL)
                goto out_nomem;

        return container_of(q, struct ipq, q);

out_nomem:
        LIMIT_NETDEBUG(KERN_ERR "ip_frag_create: no memory left !\n");
        return NULL;
}

上面的ip_find是从ip_local_deliver => ip_defrag => ip_find这个调用链过来的. 我们可以看到这里使用ip header里面的id, source addr, dest addr和protocol来做一个hash, 远没有port啥事.
到这里已经很清楚了, udp发的数据的最大负载是65535-20 -8 = 65507. 但是到了ip层的话还是需要被分片的, 否则过不了交换机. ip分片的依据的ip header中的id, 当然还需要由frags来指明fragment的offset之类的信息.
这个时候再回头来想想当初为何就没有想到, port是udp/tcp层的东西, ip层是没有这个玩意儿的. 那么分片自然就是和port无关的了.

Categories: programming Tags: , , ,

Debug Heap Corrupt

December 12th, 2014 No comments

c/c++代码对内存操作有着极强的控制力, 但是也很容易造成问题。 特别是内存访问越界这类问, 一旦出现了,在很多情况下是很难查找的。 如果软件中的某个模块因为单元测试没有完全覆盖(这个通常也很难)然后集成起来以后出来的这类问题就更加难以查找。 很多时候出现问题的地方根本就不是你看到的地方。 我们来举个例子, 这个也是我们遇到的问题的一个简化版本。 直接上代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <vector>

void corrupt( ) {
    char* p = (char*)malloc(1);
    memset(p,0x47,32);//fill some garbage to corrupt the heap
}

void victim( ) {
    std::vector<int> arr;
    for(int i = 0 ; i < 100; i ++ ) {
        arr.push_back(i);
    }
}

int main(int argc, char* argv[]){
    corrupt();
    victim();
    return 0;
}

上面的代码在corrupt函数中分配了一个字节的空间,接着调用memset向分配的地址写入32个0x47. 那么很明显,写越界了。 这样的操作会把heap的维护链表弄坏,导致以后再次进行内存分配释放的时候出现错误,也可能写入另外一块分配的内存区域。
在vc(visual studio 2005)下面编译运行以后在

        arr.push_back(i);

出现错误。 output窗口(调试模式下面)输出

First-chance exception at 0x7c922bb3 (ntdll.dll) in heap_corrupt.exe: 0xC0000005: Access violation reading location 0x003b6000.

很明显出现错误的地方并不是造成问题的地方, 这个简单的程序你可能很容易就可以看出问题出在什么位置。但是如果是一个很复杂的大型工程,那么很可能就没有那么容易了。如果没有其他的帮助工具的话估计要花很大的时间和精力来逐步收缩出问题的范围。
上面的代码在g++下面(需要增加一些include的头文件)编译运行以后也会显示是arr.push_back(i)这一行除了问题。
为了能有效的快速定位此类问题, 我们通常需要借助其他的工具。 在windows上面我们可以使用gflags来做到这一点。
使用如下命令,monitor heap的操作

gflags /p /enable heap_corrupt.exe /full

然后再来调试改程序,就会在memset(p,0x47,32);这一行发生异常。 那么我们就可以很快的检查出来是因为对p的写越界造成了这个问题。 但并不是有这个工具就万事OK了。如果我们把memset那一句改成memset(p,0x47,2);的话。那么程序在整个调试过程中是不会出错的,当然了,这个很可能是因为malloc(1)的时候得到的内存块的真是大小不是1而是某个最小的bucket的大小.
在linux上面呢, 我们可以使用AddressSanitizer这种神器来快速定位这个问题。因为我们的gcc版本是4.4.7的,还没有AddressSanitizer的支持,所以可以使用clang.

clang++ -g -fsanitize=address -o t test.cpp

然后直接运行程序就可以在输出上看到一堆东西

[hongquan.zhang@devbox Download]$ ./t
=================================================================
==1216==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000f00f at pc 0x48aaab bp 0x7fff4a1687d0 sp 0x7fff4a1687c8
WRITE of size 32 at 0x60200000f00f thread T0
    #0 0x48aaaa in corrupt() /home/hongquan.zhang/Download/test.cpp:10
    #1 0x48b0d0 in main /home/hongquan.zhang/Download/test.cpp:21
    #2 0x3f7201ecdc in __libc_start_main (/lib64/libc.so.6+0x3f7201ecdc)
    #3 0x48a84c in _start (/home/hongquan.zhang/Download/t+0x48a84c)

0x60200000f00f is located 30 bytes to the right of 1-byte region [0x60200000eff0,0x60200000eff1)
allocated by thread T0 here:
    #0 0x473131 in malloc /home/hongquan.zhang/Work/tools/llvm/projects/compiler-rt/lib/asan/asan_malloc_linux.cc:74
    #1 0x48a9cb in corrupt() /home/hongquan.zhang/Download/test.cpp:9
    #2 0x48b0d0 in main /home/hongquan.zhang/Download/test.cpp:21
    #3 0x3f7201ecdc in __libc_start_main (/lib64/libc.so.6+0x3f7201ecdc)

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/hongquan.zhang/Download/test.cpp:10 corrupt()
Shadow bytes around the buggy address:
  0x0c047fff9db0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9dc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9dd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9de0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9df0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa 01 fa
=>0x0c047fff9e00: fa[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e50: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Contiguous container OOB:fc
  ASan internal:           fe
==1216==ABORTING

输出的最开始就表明了出错的地方是corrupt函数,可以非常方便的让我们在第一时间发现真正制造问题的地方。而且即便你把memset调用改成memset(p,0x47,1)也依然有效。

上面的方法也许对你在遇到问题的时候有一些帮助,当然了最好还是在操作内存的时候小心一些。如果可以的话,尽量使用c++已经封装好的class。 如果不得已需要直接操作内存需要慎之又慎。

Categories: programming Tags: ,