2020 0xGame WriteUp
前言
主办方:南京邮电大学
Misc
flip(波形图 + 摩斯密码 + BinWalk + 倒序)

打开压缩包后得到一个 ZIP 和 MP3

ZIP 需要密码,使用 Audacity 打开 MP3

根据波形长短猜测是摩斯密码
1 | ....- ----- ----- ---.. ..... -.... -.... ----. ----. ...-- ----- ----- ... .. -.. .-. --- .-- ... ... .- .--. |
解密得到
1 | 400856699300sidrowssap |
翻译了一下题目题目名称有翻转的意思,把摩斯密码倒序发现:
1 | passwordis003996658004 |
003996658004 即为压缩包密码
解压后还是压缩包

使用 Kali 的 BinWalk 分离

查看 password.txt 文件发现
1 | ?gnorw eb ot smees gnihtemos tub ,yranib ni elif etirw ot tnaw I |
明显的倒序排列
将所有内容倒序后得到
1 | 01011001 |
二进制转十六进制再转 ASCII 码得到
1 | You are so clever that this problem is just a piece of cake for you, the password is A_pi3ce_0f_C4ke |
使用 A_pi3ce_0f_C4ke 密码提取出压缩包中的 galf_si_siht.png
提示我们打不开文件

使用 010editor 打开该文件
发现文件头不是 PNG 格式

拉到最底下发现还是倒序 黄豆流汗脸.jpg

将整个文件的十六进制倒序后保存为 PNG 图片打开发现二维码,使用二维码扫描工具得到
1 | http://am473ur.com/0xgame/flip/bd055250d3906d1f791d8e83b4396893.php |
访问拿到 flag(估计不在了)
pacp(SQL 盲注流量 + PNG 宽高)

打开流量包发现是盲注流量

过滤 POST 请求发现有上传一个文件
1 | http.request.method == POST |

追踪 HTTP 流复制原始数据

用 WinHex 复制成新文件解压缩文件是一张图片,打开没内容,检查 CRC 发现宽高被更改过

使用随波逐流梭哈出原图

differentPic(图像合成)

解压缩文件是两个一模一样的图片

使用 StegSolve 打开选择 Image Combiner 模式(图像合成)

发现可疑内容

保存后再打开切换通道发现是二维码

扫码拿到 flag

extract(BinWalk + Stegpy)

打开压缩包后发现图片

使用 BinWalk 检查发现有隐藏文件

提取出二维码扫描给出提示工具 Stegpy

使用 Stegpy 成功拿到 flag

easyMisc(拨号+ Stegpy)

解压缩出三个文件

打开音频文件听着像是拨号声音,疑似拨号隐写

把频率设置为低频 697、高频 1633


得到下图

对照拨号隐写表格解密出:2821876761

使用 2821876761 解压得到 flag 图片
再次使用 Stegpy 得到 flag

lowerBase64(Base64 解码)

Base64字符串中的字母被随机转换了大小写
正常的Base64解码器要求严格的大小写,所以我们需要找到正确的大小写组合
Base64 编码原理
1 | # Base64 编码过程: |
由于我们不知道哪些字母应该是大写、哪些应该是小写,所以需要暴力枚举所有可能的大小写组合
1 | for i in range(0, len(c), 4): # 以 4 个字符为一组处理 |
举例说明:
如果当前 chunk 是 "AbC1",那么:
'A'→['a', 'A']'b'→['b', 'B']'C'→['c', 'C']'1'→['1']
通过 product(*pos) 会生成:"abc1", "abC1", "aBc1", "aBC1", "Abc1", "AbC1", "ABc1", "ABC1" 共 8 种组合
最后验证结果
1 | for case in cases: |

threeThousand(ZIP 爆破)

第一次爆出密码是 33

打开发现还有压缩包,且都是加密

写脚本爆破
1 | import zipfile |

签到题(签到)
根据题目描述,在 Rules 页面的比赛须知发现了 flag
1 | 0xGame{Welc0m_to_0xGame2020} |
easyBase(Base 编码)
Base64,Base16 解密即可
QR_repair
用一些在线网站分离出其中的两张图片,最后补上定位符即可

Crypto
Calendar(日期网格坐标密码)

题目给出了一张图片

以及一段字符
1 | SAT1,THU1,MON3,MON2,WED3,SUN2,THU1,SUN4,FRI3,THU1,MON4,MON4,FRI4,THU3,SUN4,SUN2,TUE4,THU1,FRI1,MON3,MON2 |
观察最后的数字只有 1~4,推测这就是行号,而字符就是对应的列
将得到的数字按照字母表的顺序转换最后得到
1 | 0xGame{calendarpasswordtable} |
easyXor(异或加密)

连续两次异或同一个数结果不变
1 | cipher = [72, 63, 38, 12, 8, 30, 30, 6, 82, 4, 84, 88, 92, 7, 79, 29, 8, 90, 85, 26, 25, 87, 80, 10, 20, 20, 9, 4, 80, 73, 31, 5, 82, 0, 1, 92, 0, 0, 94, 81, 4, 85, 27, 35] |
supperAffine(仿射加密变种)

解题技巧先放前面:
- 识别密码体制: 代码中出现了
affine(x, a, b)和GCD(MOD, ...),立即锁定这是仿射密码及其变种 - 识别模数: 立即进行模数分解,MOD = 62 = 2 * 31
- **代数化简:**看到
f3(f2(f1(x)))这种结构,立即意识到这是可以合并的
给出源代码
1 | from Crypto.Util.number import * |
equationSet(提公因数 p)

解题技巧先放前面:
- 符号化:所有给出的数值用数学符号表示出来,并建立它们之间的关系
- 寻找公因数:如果给出的两个大数 A 和 B 都是由相同的素数因子构成的,那么
GCD(A, B)往往是解密的关键 - 确定解题路径:一旦
p被分解出来,接下来就要想方设法得到q和r的信息
题目给出代码
1 | from Crypto.Util.number import * |
转换成数学公式
1 | n = p * q * r |
我们可以清楚地看到,p 是 n 和 t 的公共因子
那 GCD(n, t) 会正好是 p 吗?会不会包含其他因子?
由于 q 和 r 都是生成的 512 位随机大素数,它们互质,且它们的和 q+r 极大概率与它们的积 qr 互质
因此 GCD(qr, q+r) = 1
结论:直接计算 GCD(n, t) 就能提取出素数 p
观察公因数 是 Crypto 选手的直觉。看到 n 和 t 有明显的公共结构(都含有 p),第一时间想到的就是 GCD
1 | from Crypto.Util.number import * |
Fibonacci(爆破斐波那契数列周期)

解题技巧先放前面:
- 识别计算陷阱与核心矛盾:看到一个天文数字立即判定这是计算陷阱
- **建立 Pisano 周期的知识框架:**了解周期的基本性质
- **掌握求和公式的转化:**一旦找到周期和周期和,必须将求和公式转换为可计算的代数形式
- **大数运算:**CTF 中处理超大数论运算,首选
gmpy2库
题目代码如下
1 | from Crypto.Util.number import * |
现在分析代码
定义了一个斐波那契数列
1 | def F(x): |
斐波那契数列是一个递推数列,它的核心规则是:从第三项起,数列中的每一项都等于前两项之和
$$
\begin{aligned}
&F_n\ =\ F_{n-1}\ +\ F_{n-2}\ (n \geq 3)
\end{aligned}
$$
在 CTF 和数论领域,斐波那契数列的模运算性质至关重要:
Pisano 周期
- 定义: 斐波那契数列对任何正整数 n 取模后,都会变成一个循环数列。这个循环的长度就被称为 Pisano 周期,记作
$$
\begin{aligned}
&\pi(n)
\end{aligned}
$$
小模数 n 的生成
1 | # 2. 小模数 n 的生成 |
巨大的求和上限 r
1 | # 3. 巨大的求和上限 r |
S 的计算
1 | # 4. 关键泄露信息 S 的计算 (S是 p 的基础) |
生成巨大的素数 p
1 | # 5. 生成巨大的素数 p |
生成素数 q
1 | # 6. 生成素数 q |
首先要求出 p -> S -> r 这个顺序
先看 S 的计算方式
$$
\begin{aligned}
&S\ =\ \sum_{i-0}^{r-1}{F((i)\ \ (mod\ n))}
\end{aligned}
$$
求和次数 r 太大,但是,求和时使用的模数 n 却非常小(不到 65536)
这是整个解题的关键步骤。我们不能循环 r 次,但 n 很小,所以我们要找规律
Pisano 周期(重复的舞蹈)
当斐波那契数列对一个小数字 n 取模时,它的结果会不断重复,形成一个循环。这个循环的长度就是 Pisano 周期 T
- 斐波那契数列 mod n 就像一个舞者在一个很小的圆形舞台(模数 n)上跳舞。它跳了一段时间后,姿势和位置一定会回到起点 (1, 1)
- 周期 T 就是这支舞完整跳完一次所需要的步数
通过脚本实现
1 | a, b = 1, 1 |
因为 n 很小,这个循环很快就能结束,我们得到了周期 T 和跳一圈舞的总得分 Sum_{cycle}
现在我们有了舞蹈的规律,就可以计算 r 步的总得分 S:
- 完整周期数 k: k = r // T (看看 r 步能跳多少次完整的舞)
- 剩余步数 rem: rem = r % T (最后多出来没跳完的几步)
$$
\begin{aligned}
&S\ =\ (完整次数\ k\ *\ 一圈的总得分\ Sum_{cycle})\ +\ (剩余几步的得分\ Sum_{cycle})
\end{aligned}
$$
这一步就将天文数字 r 转换成了一个可计算的公式,得到了我们想要的秘密蓝图 S
| 脚本代码 | 作用 | 简单理解 |
|---|---|---|
p = next_prime(S ** 16) |
计算第一个秘密零件 p。 严格按照题目的公式,找到 S^{16} 之后的第一个素数 | |
q = N // p |
计算第二个秘密零件 q。 N 是 p 和 q 的乘积,所以 q = N / p | |
phi = (p - 1) * (q - 1) |
计算制造蓝图 \phi(N) | |
d = inverse(65537, phi) |
计算万能钥匙 d。 inverse 是求一个数在 \pmod{phi} 世界里的“倒数” |
|
m = pow(c, d, N) |
解密! 使用万能钥匙 d 打开保险箱 N 中的密文 c | |
long_to_bytes(m) |
将解密得到的数字 m 转换回 Flag 文本 |
完整代码如下
1 | from Crypto.Util.number import * |
easyRSA(线性组合泄露 d 与 φ(n))

1 | from Crypto.Util.number import * |
RSA 里,我们有:
p,q两个大素数n = p * qφ(n) = (p-1)*(q-1)公钥指数
e = 65537(题目固定)私钥指数
d满足e * d ≡ 1 (mod φ(n))
这等价于存在一个整数 k,使得:
1 | e * d = k * φ(n) + 1 |
即
1 | d = (k * φ(n) + 1) / e |
题目除了给 n 和密文 c,还给了
1 | x = 11 * d + 7 * φ(n) |
也就是说,x 是 d 和 φ(n) 的一个线性组合,系数分别是 11 和 7
将上面 d 的表达式代入 x
1 | x = 11 * [ (kφ + 1)/e ] + 7φ |
两边同时乘以 e,去掉分母
1 | e * x = 11 * (kφ + 1) + 7eφ |
展开
1 | e * x = 11kφ + 11 + 7eφ |
把常数项移到左边,含 φ 的项合并
1 | e * x - 11 = (11k + 7e) φ |
记 T = ex - 11,则
1 | T = (11k + 7e) * φ |
这是一个关键等式,T 是 φ 的整数倍,倍数 A = 11k + 7e 依赖于未知的 k
从 e * d = k φ + 1 得
1 | k = (e * d - 1) / φ |
因为 d < φ(实际上 d 是模 φ 下的逆元,通常比 φ 略小),所以
1 | e * d - 1 < e * φ - 1 → k < e |
所以思路:
计算
T = e * x - 11(已知x和e)对每个可能的
k从 0 到e-1,计算A = 11k + 7e如果
A能整除T,那么φ = T // A就是一个候选的欧拉函数值用这个
φ去分解n,因为p + q = n - φ + 1,p * q = n,所以p和q是二次方程X^2 - (p + q)X + n = 0的两个根判断
p和q是否为整数且满足p * q == n,若是,则分解成功
已知
1 | s = p + q |
根据韦达定理,以 p 和 q 构造一个二次方程
1 | (t-q)(t-p) = 0 |
把已知条件带入得到
1 | t² - s * t + n = 0 |
对于任意二次方程,判别式公式为
1 | at² + bt + c = 0 |
代入得到
1 | a = 1,b = -s,c = n |
如果判别式是完全平方数,方程就有整数解
1 | p = (s + √Δ)/2 |
1 | from Crypto.Util.number import * |
smallModulus(CRT)
1 | module = getPrime(64) |
当用户选择 1 时,服务器会:
随机生成一个 64 位(bit)的素数
p_i计算
r_i = m (mod p_i)将
p_i和r_i以十六进制的形式返回给用户允许无限次循环查询
假设我们查询了 k 次,获得了 k 个不同的 64 位素数 p1, p2, ..., pk 以及对应的余数 r1, r2, ..., rk
我们可以写出如下的模同余方程组:
1 | m ≡ r1 (mod p1) |
由于 pi 全都是不同的素数,因此它们之间必然两两互素(gcd(pi, pj) = 1)
根据中国剩余定理,上述方程组在模 M 的意义下有唯一解,其中

这意味着,只要 M > m,我们通过 CRT 恢复出来的唯一解 x (mod M) 就一定等于原本的明文 m
已知 m < 2^512。每个素数 p_i 是 64 位的,即 p_i ≈ 2^64
我们希望 M = ∏_{i=1}^{k} p_i > 2^512
根据对数的基本性质
1 | log2(A * B) = log2(A) + log2(B) |
应用到 M 上则有

这是因为题目说每个 pi 都是 64 位素数,也就是 2^64
1 | log2(M) = log2(p1) + log2(p2) + …… + log2(pk) |
1 | 64k ≥ 512 => k ≥ 8 |
因此,我们只需要进行 8 次查询,收集 8 组模数和余数,就能完美恢复出 Flag
1 | from pwn import * |
parityOracle(LSB Oracle Attack)
传统的 LSB Oracle 每次泄露 1 个比特,而本题的 Oracle 每次泄露 2 个比特
1 | # 关键约束 |
这段代码暴露了两个致命的问题:
循环条件确保了
p × q mod 4 ≠ 3时不断重试,直到p × q mod 4 == 3。也就是说,该分发实例中的 RSA 模数n ≡ 3 (mod 4)用户输入任意密文
cip,服务端用私钥d将其解密为m',但只返回m' mod 4的值(结果为0, 1, 2, 3中的一个)
第一轮交互(i=1)的完整代数推导
我们向服务器发送构造密文 c1 = c * 4^e (mod n)。下面是服务器内部解密并取模的完整步骤
步骤 1:解密同态变换
服务器计算 m1 ≡ (c1)^d (mod n)。将 c1 的定义代入:
1 | m1 ≡ (c * 4^e)^d (mod n) |
根据模幂的积同态性质 (A * B)^d ≡ A^d * B^d (mod n),将指数 d 分配进去:
1 | m1 ≡ (c^d) * (4^e)^d (mod n) |
因为 c ≡ m^e (mod n),所以 c^d ≡ m^(e*d)
同理 (4^e)^d ≡ 4^(e*d)。代入上式得到:
1 | m1 ≡ m^(e*d) * 4^(e*d) (mod n) |
又因为 m^(e·d) = m^(1 + k·φ(n)) = m · (m^φ(n))^k ≡ m · 1^k = m (mod n)
所以得到 m1 ≡ m * 4 (mod n)
步骤 2:消除模运算
根据模运算的定义,若 A ≡ B (mod n),则必然存在一个整数 k,使得 A = k*n + B。这里将 A = 4m,B = m1 代入,得到:
1 | 4m = k1 * n + m1 |
步骤 3:限定 k1 的取值范围
已知明文边界为:0 ≤ m < n
不等式各项同时乘以 4:0 ≤ 4m < 4n
将步骤 2 的等式 4m = k1*n + m1 代入上式中间:
1 | 0 ≤ k1*n + m1 < 4n |
因为 m1 是模 n 的余数,所以必然满足 0 ≤ m1 < n。为了让上述不等式成立,整数 k1 只能在以下集合中取值:
1 | k1 ∈ {0, 1, 2, 3} |
步骤 4:两边同时取模 4
将步骤 2 的等式 4m = k1*n + m1 两边同时进行 (mod 4) 运算:
1 | 4m (mod 4) = (k1*n + m1) (mod 4) |
由于 4m 显然是 4 的倍数,所以 4m (mod 4) = 0。左边化简为 0:
1 | 0 ≡ k1*n + m1 (mod 4) |
移项,将 m1 孤立在等式一边
1 | m1 ≡ -k1 * n (mod 4) |
步骤 5:代入服务器特有条件 n ≡ 3 (mod 4)
已知条件中 n ≡ 3 (mod 4),在模 4 运算中,3 ≡ -1 (mod 4)。因此我们可以把 n 替换为 -1:
将 n ≡ -1 代入步骤 4 结尾的方程中:
1 | m1 ≡ -k1 * (-1) (mod 4) |
步骤 6:建立预言机输出与 k1 的等价关系
预言机返回的结果定义为 R1 = m1 (mod 4)。根据步骤 5 的结论,m1 (mod 4) = k1 (mod 4)
由于我们在步骤 3 中已经严格证明了 k1 ∈ {0,1,2,3},且预言机返回值 R1 也必然在 {0,1,2,3} 之间。两个位于 [0,3] 区间内的整数如果模 4 同余,则它们必然严格相等:
1 | R1 = k1 |
任意一轮(第 i 轮)的递推数学证明
在第 i 轮,我们发送 ci = c * (4^i)^e (mod n),解密得到:
1 | Xi ≡ 4^i * m (mod n) |
写成等式形式:
1 | 4^i * m = Ki * n + Xi (0 ≤ Xi < n) |
我们同样将上一轮(第 i-1 轮)的等式列出来:
1 | 4^(i-1) * m = K_(i-1) * n + X_(i-1) (0 ≤ X_(i-1) < n) |
步骤 1:寻找上下两轮之间的递推关系
我们将第 i-1 轮的等式两边同时乘以 4:
1 | 4 * (4^(i-1) * m) = 4 * (K_(i-1) * n + X_(i-1)) |
左边合并幂次,右边展开括号:
1 | 4^i * m = 4K_(i-1) * n + 4X_(i-1) |
现在,我们单独对 4X_(i-1) 这一项进行除以 n 的操作,假设其余数为 Xi,不完全商为 k'
1 | 4X_(i-1) = k' * n + Xi |
由于 0 ≤ X_(i-1) < n,同理可得这个局部商的范围也是:
1 | k' ∈ {0, 1, 2, 3} |
将 4X_(i-1) = k'*n + Xi 代入上面展开的递推式中
1 | 4^i * m = 4K_(i-1) * n + (k'*n + Xi) |
提取公因数 n
1 | 4^i * m = (4K_(i-1) + k') * n + Xi |
步骤 2:对比系数
对比本节开头的定义式 4^i * m = Ki * n + Xi,由于余数 Xi 具有唯一性,我们可以直接对应出 n 的系数相等:
1 | Ki = 4K_(i-1) + k' |
步骤 3:对总商 Ki 取模 4
将上式两边同时 (mod 4):
1 | Ki (mod 4) = (4K_(i-1) + k') (mod 4) |
由于 4K_(i-1) 是 4 的倍数,模 4 为 0,因此式子化简为
1 | Ki ≡ k' (mod 4) |
因为 k' ∈ {0,1,2,3},所以:
1 | Ki (mod 4) = k' |
步骤 4:计算预言机回显与 k' 的关系
第 i 轮预言机收到的解密结果是 Xi,它返回 Ri = Xi (mod 4)。根据本节开头定义式 4^i * m = Ki * n + Xi,变形可得
1 | Xi = 4^i * m - Ki * n |
两边同时取模 4
1 | Xi (mod 4) = (4^i * m - Ki * n) (mod 4) |
由于 i ≥ 1,4^i * m (mod 4) = 0。式子化简为
1 | Xi ≡ -Ki * n (mod 4) |
再次代入 n ≡ -1 (mod 4)
1 | Xi ≡ -Ki * (-1) ≡ Ki (mod 4) |
将步骤 3 的结论 Ki ≡ k' (mod 4) 代入上式
1 | Xi ≡ k' (mod 4) |
因为 Ri = Xi (mod 4),且 Ri 和 k' 都在 [0,3] 范围内,所以它们严格相等
1 | Ri = k' |
最终递推结论:第 i 轮预言机返回的 Ri,正是 4X_(i-1) 除以 n 的商 k'。也就是说,它反应的是上一轮的余数 X_(i-1) 落在 n 的哪一个四分之一区间内
既然知道了 Ri = k' = floor(4X_(i-1) / n),我们就可以据此来对明文真实空间 [lower, upper] 实施精准切分。根据 k' 的值,上一轮的余数空间被划分为四个区间:
当
k' = 0时:0 ≤ 4X_(i-1) < n => X_(i-1) ∈ [0, n/4)当
k' = 1时:n ≤ 4X_(i-1) < 2n => X_(i-1) ∈ [n/4, 2n/4)当
k' = 2时:2n ≤ 4X_(i-1) < 3n => X_(i-1) ∈ [2n/4, 3n/4)当
k' = 3时:3n ≤ 4X_(i-1) < 4n => X_(i-1) ∈ [3n/4, n)
映射回当前动态维护的明文全局边界 [lower, upper],其物理跨度为 W = upper - lower。我们将这个跨度四等分,每一份的权重为 (upper - lower) / 4
当
rev == 0 (k'=0),说明真实值位于当前区间的第一个四分之一。下界不变:lower = lower。上界缩减:upper = lower + 1 * (upper - lower) // 4当
rev == 1 (k'=1),说明真实值位于当前区间的第二个四分之一。下界右移:lower = lower + 1 * (upper - lower) // 4。上界左移:upper = lower + 2 * (upper - lower) // 4当
rev == 2 (k'=2),说明真实值位于当前区间的第三个四分之一。下界右移:lower = lower + 2 * (upper - lower) // 4。上界左移:upper = lower + 3 * (upper - lower) // 4当
rev == 3 (k'=3),说明真实值位于当前区间的第四个四分之一。上界不变:upper = upper。下界右移:lower = lower + 3 * (upper - lower) // 4
1 | #!/usr/bin/env python3 |
Web
????(Shell 通配符绕过)

打开页面提示输入 yulige 获得 flag

但是前端限制了只能输入 4 字符,右键改源代码删掉

提交按钮被禁用了,删掉 disable 属性

提交后给出提示

访问过去给出了黑名单源码

1 | if(preg_match("/[A-Za-ko-z0-9]+/", $cmd)) |
它唯独漏掉了三个小写字母:l、m、n
1 | $blacklist = "~!@#%^$&\/*()()<>《》-_{}[]'/\":,"; |
同样漏掉了空格、?、.
所以我们可以利用 nl 这个命令读取文件,其文件名采用正则的形式代替
1 | # nl fl4g_is_here.php |

robots(robots.txt)

访问 /robots.txt 给出了一个路径,继续访问拿到 flag

view source(源代码泄露)

F12 即可

get&post(HackBar)

根据源代码构造参数即可

wh1sper’s_secret_garden(UA + Referer + XFF 绕过)

提示需要修改 UA 头为 wh1sper

改完后提示改 Referer

完了又提示需要本地访问,改 XFF 头

成功拿到 flag

read flag(签到)

考察 Linux 命令
just_login(签到)

访问 phpinfo.php

intval(正则表达式换行匹配 + intval 绕过)

拿到源码后先分析
1 | if($_GET['0xGame'] !== '20201001' && preg_match('/^20201001$/',$_GET['0xGame'])) |
条件一:
$_GET['0xGame'] !== '20201001'- 使用的是强类型不等于
条件二:
preg_match('/^20201001$/', $_GET['0xGame'])- 要求传入的值必须匹配正则表达式
/^20201001$/
- 要求传入的值必须匹配正则表达式
看起来这两个条件是互斥的,但这里利用了 PHP PCRE 正则表达式的特性
在默认情况下,正则表达式中的锚点 $ 不仅匹配字符串的末尾,还会匹配字符串末尾的换行符(\n)
所以我们传入 20201001%0a,接下来看第二关
1 | $id=intval($_GET['id']); |
条件一:
$_GET['id'] != 1024- 使用的是弱类型不等于
条件二:
$id === 1024- 使用的是强类型等于
这里考察的是 intval() 函数的处理机制以及 PHP 在处理 !=(弱类型比较)时的数字转换
intval() 的特性:当转换一个字符串时,它会从字符串的开头开始解析,直到遇到非数字字符为止(小数点 . 也是非数字字符)
例如:
intval('1024.1')的结果是整数1024例如:
intval('1024abc')的结果也是整数1024
构造 Payload
1 | ?0xGame=20201001%0a&id=1024abc |

edr(CNVD-2020-46552)

参考复现博客:https://www.sqlsec.com/2020/08/edr.html
查看 IP 地址,浏览器直接访问即可访问到前端登录界面

漏洞代码在 /tool/log/c.php 处,首先 $show_form 接受用户输入请求

跟进这个函数,发现是经典的 extract 变量覆盖 ,参数值完全可控
1 |
|

所以最终可以构造如下 Payload
1 | https://xxx.xxx.xx.xx/tool/log/c.php?strip_slashes=system&host=id |

Pwn
欢迎来到 0xGame 平台(签到)

直接 nc 即可
帮我取一个题目名称(Ret2text)

没开金丝雀,read 函数读取大小为 64 字节

变量 s 距离 r 处是 0x28 个字节

后门函数地址是 0x0401162

1 | from pwn import * |
该怎么起名呢(Shellcode)

没开金丝雀

mprotect 的第三个参数 7 代表 RWX 权限,从指针 buf 向前偏移 16 字节 的地址开始,长度为 4096 字节
((buf + 32))(); 表示 buf + 0x20 处会被当成函数调用执行
所以填充垃圾数据覆盖到这个位置再插入 shellcode

1 | from pwn import * |
variable overlap(变量覆盖)

保护全开

"%lld" 表示:读取一个 long long 类型整数(64 位有符号整数,也就是 8 字节)

v4 变量在栈中的布局是 var_14

v5 则是 var_10,相差了 4 字节

在 x86/x64 架构下,数据是以小端序存储的,即:低位字节存放在低地址,高位字节存放在高地址
0x2333 作为一个 8 字节的数,完整的十六进制表达是:0x0000000000002333
如果直接输入,则 v4 的内容变成了 0x00002333,v5 的内容变成了 0x00000000
所以要倒过来输入
1 | from pwn import * |
easy stack(Ret2text)

没有金丝雀

后门函数地址

nc 过去先看看回显什么

开局给了后门函数地址

找到打印的第二句话的位置

观察变量 s 的栈布局

离 r 处有 0x88 + 0x4 的距离,但是 read 函数给了 0x100 的长度,完全够溢出

1 | from pwn import* |
0xPwn(Ret2plt)

没开金丝雀以及 PIE

开局要先输入一个内容存放在 &unk_804C00C

BSS 段不可执行代码,位置是 0x0804C00C

函数 sub_8049192() 中存在栈溢出

溢出大小是 140 字节

因为没有给后门函数,所以思路是调用 _system() 函数,参数放在 BSS 段
注意:如果该内存区域紧接着有残留的垃圾数据(比如 AAAA...),系统函数去读取时,就会把它们当成一个整体,试图去执行 /bin/shAAAA...。这会导致系统找不到该命令而报错
所以我们需要加上 \x00 截断
在 32 位架构中,绝大多数 Linux 程序采用的是 cdecl 调用约定,32 位程序完全通过栈来传递函数参数
当一个正常的 32 位程序使用 call 指令调用函数时,栈的布局必须严格满足以下顺序(从低地址到高地址):
函数的入口地址(触发调用)
函数的返回地址(函数执行完后,下一条指令去哪)
函数的参数 1
函数的参数 2 …
在打 Pwn 时,如果没有要返回的地址,则默认填 0
1 | from pwn import * |
Pwn题滞销,帮帮我好吗_(Ret2csu)

代码只有两行

进入 sub_401132() 函数,看不出来啥问题

查看伪代码
1 | push rbp ; 保存父函数的栈底(Old RBP) |
使用 Pwndbg 动态调试,pwn cyclic 生成垃圾数据

rsi 寄存器在伪代码中为参数 buf 的地址,值为 0x7fffffffda80
rsp 寄存器指向返回地址,值为 0x7fffffffdaa8
我们用这两个真实的动态地址做一下减法,得到要覆盖的垃圾数据为 40 个字节

因为没有后门,所以我们需要调用 syscall 并让系统去执行 execve("/bin/sh", 0, 0)
为了启动这个 syscall,Linux 规定了死命令,必须提前在寄存器里摆好参数:
rax寄存器里必须是 59(调用execve函数)rdi寄存器里必须是"/bin/sh"字符串的内存地址rsi寄存器里必须是 0rdx寄存器里必须是 0
我们可以利用程序里的残余指令,比如 pop rdi; ret。但是查了一遍没给,可惜可惜

先来计算 /bin/sh 的地址
1 | 地址 | 字符 |

所以起始地址就是 0x402016
虽然常规的 pop rdi 没了,但只要是 C 语言编译的程序,系统都会默认自带一个初始化功能,叫 __libc_csu_init
在 __libc_csu_init 函数的尾部,躺着两段连续的汇编指令

1 | .text:0x4011F2 pop rbx ; 弹出给 rbx |
1 | .text:0x4011D8 mov rdx, r14 |
思路如下:
pop rbx:这里的结果会影响到最后的call qword ptr [r15+rbx*8],因为r15本身就由我们控制。选择填 0,这样就直接调用r15指向的函数pop rbp:这里的结果会影响到后面的cmp rbp, rbx,再判断之前就已经add rbx, 1了,所以要填入 1,不然会重新循环pop r12:这里的结果会影响到后面的mov edi, r12d,edi就是rdi,填入/bin/sh地址pop r13:这里的结果会影响到后面的mov rsi, r13,也就是execve("/bin/sh", 0, 0)第二个参数,填 0pop r14:同理,这里是第三个参数,填 0pop r15:这里的结果会影响到后面的call qword ptr [r15+rbx*8],因为call必须得给一个真实的函数地址,否则会报错。我们选择alarm,调用它只是定个闹钟
1 | payload = b'A' * 40 # 垃圾数据填满栈空间 |
到跳转到 0x4011D8 处时,执行过程如下
rdx与rsi为 0,rdi为/bin/sh地址call alarm,r15被填充为了alarm函数地址比较
rbp和rbx,相等则进去进入0x4011EE中执行
1 | .text:0x4011EE add rsp, 8 |
这样又进入了 pop 段中,但是我们已经不需要了,所以填充垃圾数据进去
1 | payload += p64(0) * 7 # 抵消掉这 1 个 add 和 6 个 pop 的干扰 |
此时来到了最后的 .text:0x4011FC retn 处,我们劫持
1 | payload += p64(pop_rax_syscall) |
1 | from pwn import * |
Reverse
签到(签到)

Shift + F12 发现 flag

壳(UPX)

识别出 UPX 壳,直接脱壳

IDA 打开即可

py 一下(PYC 反编译)

反编译看到是异或加密,再异或回去即可
1 | import py_compile |
得到 flag
1 | flag{myy_ls_l73_50b_0f_pwv} |
easybit(位运算)

反编译
Flag 的绝对长度必须是 30 位 (v6 == 30),否则直接输出 out of length 并退出
1 | scanf("%50s", Str); |
在比较之前,程序对输入的每个字符都进行了如下循环处理
1 | for ( i = 0; i < v6; ++i ) |
32 * Str[i] 实际上等价于 Str[i] << 5,因为 32 = 2^5
假设 i = 3
1 | 3 的二进制: 0000 0011 |
对于一个 8 位的字节来说,右移 3 位和左移 5 位,它们异或的结果就是循环右移 3 位
原始:
[A, B, C, D, E, F, G, H]右移三位:
[D, E, F, G, H, 0, 0, 0]左移五位:
[0, 0, 0, 0, 0, A, B, C]异或:
[D, E, F, G, H, A, B, C]
IDA 中查看 check[] 数组的值

解题脚本
1 | check_data = [ |
解方程(Z3 定理证明器)


观察 v5[0] 到 v5[27] 的计算公式,看似有 28 行非常冗长,但如果我们切块来看,会发现极其显著的对称规律:
分组:28 个字符被平均分成了 4 组,每组 7 个字符
第一组:
Str[0]到Str[6]对应v5[0]到v5[6]第二组:
Str[7]到Str[13]对应v5[7]到v5[13]第三组:
Str[14]到Str[20]对应v5[14]到v5[20]第四组:
Str[21]到Str[27]对应v5[21]到v5[27]
系数矩阵:不仅分组了,而且每一组的系数完全一模一样
v5[0]=12*Str[0] + 53*Str[1] + 6*Str[2] + 34*Str[3] + 58*Str[4] + 36*Str[5] + 1*Str[6]v5[7]=12*Str[7] + 53*Str[8] + 6*Str[9] + 34*Str[10] + 58*Str[11] + 36*Str[12] + 1*Str[13]
它们的乘数(12, 53, 6, 34, 58, 36, 1)和对应的相对位置完全一致
1 | from z3 import * |