GeekGame 2024 WriteUp

GeekGame 2024 WriteUp,艰难且更艰难

Rank:0x7f/0x30 (所有选手/其它选手)

做出来一些娱乐向的题目,算是缓解了一点本来要崩溃的精神状态。然后技术力的题目就做出来很少了,这里就放两题吧,

web-copy (hard)

不给 F12 是吧,Selenium 一把梭,我直接输出 page_source 然后 XPATH。

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
import time

from selenium import webdriver
from selenium.webdriver.common.by import By


token = "<your token>"
home_url = f"https://prob05.geekgame.pku.edu.cn/?token={token}"

browser = webdriver.Edge()
browser.get(home_url)

el = browser.find_element(By.XPATH, "/html/body/p[4]/a[1]")
el.click()

els = browser.find_elements(By.XPATH, '//div[@id="centralNoiseContent1"]/div[@class="noiseLine"]')
captcha = "".join(map(lambda el: el.text, els))

el_inp = browser.find_element(By.ID, "noiseInput")
el_btn = browser.find_element(By.ID, "submitBtn")
el_inp.send_keys(captcha)
el_btn.click()

print(browser.page_source)
time.sleep(5)
browser.close()

web-copy (expert)

还不给 F12 是吧,Selenium 一把梭,嗯?啥都没有?view-source: 看一眼发生什么事了。看到

  1. 最后加载了一个 page2.max.js,点开一看全是 _0xffffffff 这样的变量,大概是混淆过了。
  2. 有一个 idroot 的节点,看起来内容像是加密过的,搜索了一下好像叫 Fernet 加密,但是拖到赛博厨子一看要密钥,还得是要破解 page2.max.js 文件。

心里大致有个数,脚本里面挂了一个 onload 侦测器,然后把 <div id="root"></div> 的内容解密,最后挂载上去(具体怎么做到没有节点的不清楚)。那么没得选,开始逆向这个混淆吧。搜索关键词js 反混淆

曲折

看了好久找到了一篇文档,其中介绍了 AST 混淆,看来要啃代码查重大杀器 AST 了。

但是实际上配置了好久 babel 之后,发现语法树太大了,这要是一段段抠,一周都看不完。考虑到这里的js文件是一个静态文件,下载下来本地调试就好了,你不让我 F12,我可以 alert() 呀;你不让我 alert(),我可以 localStorage 或者 sessionStorage。并且,虽然用 babel 解析的语法树非常庞大,但是对文件还算是格式化了,看起来像一点js的样子了。

分析

先看最后

1
2
3
window[a0_0x35fc87(0xf21, 0x16b0)] = function () {
a0_0x5b8baf();
};

不出所料,就是给某个侦测器挂了钩子,那么就从 a0_0x5b8baf() 来分析。

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
function a0_0x5b8baf() {
var a0_0x523a18 = {
_0x2818de: 0x199a,
_0x1a652f: 0x6e6,
_0x24d50d: 0xdb8,
_0x1879fe: 0xac,
_0x2cb4a8: 0xc9e,
_0x49173d: 0x19b9,
_0x15b636: 0x1609,
_0x3e0d99: 0x106f,
_0x1aeec6: 0xb05,
_0x5e86c5: 0x119a,
_0x4b8b9f: 0x1045
},
a0_0x2cad5e = {
_0x313ddb: 0x141
},
_0x4eb293 = {
'zzUXd': _0x358a2f(a0_0x523a18._0x2818de, 0x1135),
'erBjh': _0xe7c12d(0x300, 0xa11),
'QwGoq': _0xe7c12d(0x12d7, a0_0x523a18._0x1a652f),
'PqqCc': _0x4bbdbc(0xe36, a0_0x523a18._0x24d50d),
'kQJej': function (_0x49aeb9, _0x38ac34, _0xe9eda2) {
return _0x49aeb9(_0x38ac34, _0xe9eda2);
},
'NxUhs': function (_0x5574a9, _0xa4b111, _0x3587ae) {
return _0x5574a9(_0xa4b111, _0x3587ae);
}
};
// Tag1
const _0x4b819d = document[_0x358a2f(0x3c7, a0_0x523a18._0x1879fe)](_0x4eb293[_0xe7c12d(a0_0x523a18._0x2cb4a8, 0x119c)]),
_0x17cb53 = _0x4b819d[_0x358a2f(a0_0x523a18._0x49173d, 0xdfe)];
_0x4b819d[_0xe7c12d(0x162c, 0x163b)] = '', _0x4b819d['style'][_0x4bbdbc(0xde0, 0xf2f)] = _0x4eb293[_0xe7c12d(a0_0x523a18._0x15b636, a0_0x523a18._0x3e0d99)];
function _0x4bbdbc(_0x1a1e15, _0x19ed69) {
return a0_0x2e32a9(_0x1a1e15, _0x19ed69 - 0x233);
}
var _0x58d552 = {};
_0x58d552['mode'] = _0x4eb293[_0xe7c12d(a0_0x523a18._0x1aeec6, 0x16c7)];
const _0xab6759 = _0x4b819d['attachShadow'](_0x58d552), _0x332326 = '...';
_0xab6759[_0x4bbdbc(0xf69, 0x119b)] = _0x4bbdbc(-0x87, 0x106);
const _0x3188da = document['createElement'](_0x4eb293['PqqCc']);
_0x3188da['textContent'] = _0x332326, _0xab6759['appendChild'](_0x3188da);
function _0x358a2f(_0x1aadd0, _0x497d2c) {
return a0_0x32746c(_0x497d2c, _0x1aadd0 - 0x24c);
}
[1,2,3].forEach(() => a0_0x504e28(_0xab6759)), _0x4eb293[_0xe7c12d(0x137f, a0_0x523a18._0x4b8b9f)](a0_0x204ab6, _0xab6759, _0x17cb53);
function _0xe7c12d(_0x5411b3, _0x388f1b) {
return a0_0x32746c(_0x388f1b, _0x5411b3 - -a0_0x2cad5e._0x313ddb);
}
_0x4eb293[_0xe7c12d(0xbbd, 0x303)](setInterval, () => a0_0x504e28(_0xab6759), 0x1 * -0x5e1 + -0x242b + 0x494c);
}

看上去好吓人,但是我们一行行来看(IDA不也是这样么)。前面的变量暂时不看,我们也不知道到底想要干什么,反正就是拆字。

看 Tag1,这里有一个 document 的成员函数,用 alert 看看 _0x358a2f(0x3c7, a0_0x523a18._0x1879fe)_0x4eb293[_0xe7c12d(a0_0x523a18._0x2cb4a8, 0x119c)]。很好

1
2
_0x358a2f(0x3c7, a0_0x523a18._0x1879fe) = "getElementById";
_0x4eb293[_0xe7c12d(a0_0x523a18._0x2cb4a8, 0x119c)] = "root";

那么明白了,这里 const _0x4b819d = document.getElementById("root");。这个出来了之后,后面的解析就有点眉目了。下面是大部分解析后的结果(以注释的形式放在语句上,有点长,请耐心读一读):

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
function a0_0x5b8baf() {
var a0_0x523a18 = {
_0x2818de: 0x199a,
_0x1a652f: 0x6e6,
_0x24d50d: 0xdb8,
_0x1879fe: 0xac,
_0x2cb4a8: 0xc9e,
_0x49173d: 0x19b9,
_0x15b636: 0x1609,
_0x3e0d99: 0x106f,
_0x1aeec6: 0xb05,
_0x5e86c5: 0x119a,
_0x4b8b9f: 0x1045
},
a0_0x2cad5e = {
_0x313ddb: 0x141
},
_0x4eb293 = {
'zzUXd': _0x358a2f(a0_0x523a18._0x2818de, 0x1135),
'erBjh': _0xe7c12d(0x300, 0xa11),
'QwGoq': _0xe7c12d(0x12d7, a0_0x523a18._0x1a652f),
'PqqCc': _0x4bbdbc(0xe36, a0_0x523a18._0x24d50d),
'kQJej': function (_0x49aeb9, _0x38ac34, _0xe9eda2) {
return _0x49aeb9(_0x38ac34, _0xe9eda2);
},
'NxUhs': function (_0x5574a9, _0xa4b111, _0x3587ae) {
return _0x5574a9(_0xa4b111, _0x3587ae);
}
};
// _0x358a2f(0x3c7, a0_0x523a18._0x1879fe) = "getElementById";
// _0x4eb293[_0xe7c12d(a0_0x523a18._0x2cb4a8, 0x119c)] = "root";
// const _0x4b819d = document.getElementById("root");
// const rootEl = _0x4b819d; // 设置别名,看得清楚一点,下同
const _0x4b819d = document[_0x358a2f(0x3c7, a0_0x523a18._0x1879fe)](_0x4eb293[_0xe7c12d(a0_0x523a18._0x2cb4a8, 0x119c)]),
// _0x358a2f(a0_0x523a18._0x49173d, 0xdfe) = "textContent";
// _0x17cb53 = rootEl.textContent;
// const cipherContent = _0x17cb53;
_0x17cb53 = _0x4b819d[_0x358a2f(a0_0x523a18._0x49173d, 0xdfe)];

// _0xe7c12d(0x162c, 0x163b) = "innerHTML";
// rootEl.innerHTML = '';
_0x4b819d[_0xe7c12d(0x162c, 0x163b)] = '',
// 设置 style,不管
_0x4b819d['style'][_0x4bbdbc(0xde0, 0xf2f)] = _0x4eb293[_0xe7c12d(a0_0x523a18._0x15b636, a0_0x523a18._0x3e0d99)];

function _0x4bbdbc(_0x1a1e15, _0x19ed69) {
return a0_0x2e32a9(_0x1a1e15, _0x19ed69 - 0x233);
}

var _0x58d552 = {};
// _0x4eb293[_0xe7c12d(a0_0x523a18._0x1aeec6, 0x16c7)] = "closed";
// _0x58d552 = {'mode': 'closed'};
// const opts = _0x58d552;
_0x58d552['mode'] = _0x4eb293[_0xe7c12d(a0_0x523a18._0x1aeec6, 0x16c7)];

// _0xab6759 = rootEl.attachShadow(opts);
// const shadowRootEl = _0xab6759;
const _0xab6759 = _0x4b819d['attachShadow'](_0x58d552), _0x332326 = '...';

_0xab6759[_0x4bbdbc(0xf69, 0x119b)] = _0x4bbdbc(-0x87, 0x106);
const _0x3188da = document['createElement'](_0x4eb293['PqqCc']);
_0x3188da['textContent'] = _0x332326, _0xab6759['appendChild'](_0x3188da);
function _0x358a2f(_0x1aadd0, _0x497d2c) {
return a0_0x32746c(_0x497d2c, _0x1aadd0 - 0x24c);
}

// [0x9*-0x40e+0x5ec+-0x1*-0x1e93,0x3*0x45+0xc16+0xce3*-0x1,-0x11eb*0x1+0x53*0x2c+0x3aa] = [1,2,3]
// _0x4bbdbc(0xd45,a0_0x523a18._0x5e86c5) = "forEach"
// [1,2,3].forEach(() => a0_0x504e28(shadowRootEl));
[0x9*-0x40e+0x5ec+-0x1*-0x1e93,0x3*0x45+0xc16+0xce3*-0x1,-0x11eb*0x1+0x53*0x2c+0x3aa][_0x4bbdbc(0xd45,a0_0x523a18._0x5e86c5)](()=>a0_0x504e28(_0xab6759)),

// 这里虽然没能解析到具体的名称,但是看到最后函数调用的过程中 shadowRootEl
// 和 cipherContent 都被放在了一个函数里面,可以猜想这里大概就是解密+内容挂载了,
// 我们只要在后面截胡就好
_0x4eb293[_0xe7c12d(0x137f,a0_0x523a18._0x4b8b9f)](a0_0x204ab6,_0xab6759,_0x17cb53);

// 这里创建一个新的节点,内容是影子根的内容,然后挂载到页面上,selenium 就能读到了
const d = document.createElement("div");
d.setAttribute("id", "webcopylvl2");
d.innerHTML = _0xab6759.innerHTML;
document.getElementsByTagName("body")[0].appendChild(d);

function _0xe7c12d(_0x5411b3, _0x388f1b) {
return a0_0x32746c(_0x388f1b, _0x5411b3 - -a0_0x2cad5e._0x313ddb);
}
_0x4eb293[_0xe7c12d(0xbbd, 0x303)](setInterval, () => a0_0x504e28(_0xab6759), 0x1 * -0x5e1 + -0x242b + 0x494c);
}

然后用 Selenium 读取一下内容,发现是用 data-xxxxxxxx 和 CSS 的 content 来显示的。本来想解析 DOM 树和 CSS 手动拼接的,但是发现 <style> 节点内容拿不到,还是用 getComputedStyle() 来解决计算后的值罢。

综上

步骤如下:

  1. 修改 js 文件(可以在这里下载修改后的),并放在 ./static/js 目录下
  2. 在当前目录下启动一个 http 服务器,例如 python -m http.server
  3. 执行 Python 脚本,更加具体地:
    1. 获取带有密文的 index.html ,并存储在当前目录下
    2. 操作 Selenium 读取页面
    3. 定位到元素,并执行 getComputedStyle().getPropertyValue('content') 直接获得解码后的结果
    4. 拼接结果并获得 flag

Python 脚本如下

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
import re
import time

import requests
from selenium import webdriver
from selenium.webdriver.common.by import By


token = "<your_token>"
home_url = f"https://prob05.geekgame.pku.edu.cn/?token={token}"
q2_url = "https://prob05.geekgame.pku.edu.cn/page2"
localhost = "http://127.0.0.1:8000"

s = requests.Session()
s.get(home_url)

res = s.get(q2_url)
with open("index.html", 'w', encoding="utf-8") as home:
home.write(res.text)

data = {"response": ""}
_match = re.search(r'<input type="hidden" name="ts" value="(\d+)">', res.text)
data["ts"] = 0
if _match:
data["ts"] = int(_match.group(1))
_match = re.search(r'<input type="hidden" name="certificate" value="([0-9a-f]+)">', res.text)
data["certificate"] = ""
if _match:
data["certificate"] = _match.group(1)

browser = webdriver.Edge()
browser.get(localhost)
time.sleep(2)

els = browser.find_elements(By.XPATH, "//div[@id='centralNoiseContainer']/div[@id='centralNoiseContent1']/span")
ans = []
for el in els:
el_id = el.get_attribute("id")
ans.append(browser.execute_script(f"return window.getComputedStyle(document.querySelector('#{el_id}'), ':before').getPropertyValue('content')"))
ans.append(browser.execute_script(f"return window.getComputedStyle(document.querySelector('#{el_id}'), ':after').getPropertyValue('content')"))
captcha = "".join(map(lambda s: s.replace("\"", ""), ans))
data["response"] = captcha

res = s.post(q2_url, data=data)
print(res.text)

s.close()
time.sleep(5)
browser.close()

PS:有群友说 Ctrl + s 就行了,好像……真的是这样的。

PPS:有群友说 Firefox 可以无视反调试代码开控制台,如果是真的话……唉,当初抛弃你是我的不对。

algo-codegolf (素数判断函数)

因为只要检测500以内的素数,那么一次费马素性测试即可(回顾了一遍原理发现我写的不是费马素性检验,但是费马小定理是用上了)。这里需要爆破检测用的 \(a\),使得当且仅当 \(n\) 为素数时,\(a^{n-1}\equiv 1(\mathrm{mod}\ n)\) 成立。写个代码来爆破 \(a\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from Crypto.Util.number import sieve_base

primes = []
for prime in sieve_base:
if prime > 500:
break
primes.append(prime)

a = 2
flag = False
while not flag:
flag = True
for p in range(2, 500):
i = pow(a, p-1, p)
if i == 1 and p not in primes:
flag = False
break
if i != 1 and p in primes:
flag = False
break
if flag:
print(a)
break
a += 1

得到 \(a = 10103\),然后就是费马素性检验,\(10103^{n-1}\equiv 1(\mathrm{mod}\ n)\),对于素数 \(n\) 成立,对于非素数 \(n\),因为 10103 是素数,所以模小于500的 \(n\),值一定不是0。那么如何将1和大于1的值映射到1和0呢?用1整除就好了。所以最终函数为:1//(10103**(n-1)%n)

algo-codegolf (Pell)

这个想了好久,看了提示才出来的。提示的博客写得非常好,有机会的话恶补一下生成函数。直接开始公式推导吧。

对于 Pell 数列,其递推公式为:

\[ P_{n}=\left\{ \begin{align} &0 &,n=1 \\ &1 &,n=2 \\ &2P_{n-1}+P_{n-2}&,n>2 \end{align} \right. \]

对于第 \(i\) 项等式,两边同乘以 \(x^{i+1}\),即\(P_{i+2}x^{i+1} = 2P_{i+1}x^{i+1} + P_{i}x^{i+1}\),然后累加前 \(n\) 项等式,得到

\[\sum_{i=1}^nP_{i+2}x^{i+1} = 2\sum_{i=1}^nP_{i+1}x^{i+1} + \sum_{i=1}^nP_{i}x^{i+1}\]

\(\mathcal{P}_n(x)=\sum_{i=1}^nP_{i}x^{i-1}\),则上述等式可以写为:

\[\begin{align} \mathcal{P}_{n+2}(x)-P_2x-P_1 &= 2x(\mathcal{P}_{n+1}(x)-P_1) + x^2\mathcal{P}_{n}(x) \\ \mathcal{P}_{n+2}(x)-x &= 2x\mathcal{P}_{n+1}(x) + x^2\mathcal{P}_{n}(x) \end{align} \]

也许这里就是生成函数的特殊之处,认为这里的求和是收敛的,即 \(\lim_{n\to\infty}\mathcal{P}_{n}(x) = \mathcal{P}(x)\)好吧,感觉有点道理,先这么认为了。

因为 \(P_i < 4^i\),所以该函数一定小于一个等比数列求和,那么只要让 \(x < \frac{1}{4}\),即在收敛半径中,这个幂级数就一定收敛

那么通过上述等式,得到:

\[\mathcal{P}(x)=\frac{x}{1-2x-x^2}\]

用Python验证一下:

1
2
3
>>> P = lambda x: x/(1-2*x-x**2)
>>> P(1e-3)
0.0010020050120290703

不难发现,这里小数部分展示了 Pell 数的 2 至 7 项,原因是:

\[\mathcal{P}(10^{-3})=\sum_{i=1}^{\infty}10^{-3(i-1)}P_i\]

那么,\(P_i\equiv \lfloor 10^{3(i-1)}\mathcal{P}(10^{-3})\rfloor (\mathrm{mod}\ 10^3)\),注意到这里模数要比 \(P_i\) 大,不然就只能得到模数了。而 Pell 数的通项公式为:

\[P_n=\frac{(1+\sqrt{2})^n-(1-\sqrt{2})^n}{2\sqrt{2}}\]

所以,\(P_n < 4^{n+1}\)。令 \(x = 4^{-i-1}\),则

\[ \begin{align} P_i &\equiv \lfloor 4^i\mathcal{P}(4^{-i-1})\rfloor (\mathrm{mod}\ 4^{i+1}) \\ P_i &= \lfloor \frac{4^i*4^{-i-1}}{1-2*4^{-i-1}-4^{-2i-2}} \rfloor (\mathrm{mod}\ 4^{i+1}) \\ &= \lfloor \frac{4^i*4^{i+1}}{4^{2i+2} - 2*4^{i+1} - 1} \rfloor (\mathrm{mod}\ 4^{i+1}) \\ &= \lfloor \frac{4^{i^2+i}}{16*16^i - 8*4^i - 1} \rfloor (\mathrm{mod}\ 4*4^i) \end{align} \]

写成 Python 代码为:4**(n*n+n)//(16*16**n-8*4**n-1)%(4*4**n)

不经意传输(🗝简单开个锁️)

想到了一点,但就是一点,后面结合提示瞪公式的时候找到了关键点。

题面

FROM RSA:classic

已知 \(p, q, n=pq, e=65537, d\equiv e^{-1}(\mathrm{mod}\ \varphi(n)), x_0, x_1\),其中 \(p\)\(q\) 为素数,\(\varphi(n)\) 为欧拉函数,\(x_0\)\(x_1\) 为小于 \(n\) 的随机数。对于给定 \(v\),已知

\[\left\{\begin{align} v_0 \equiv ((v-x_0)^d+(p+q)^d+f)(\mathrm{mod}\ n) \\ v_1 \equiv ((v-x_1)^d+(p-q)^d+f)(\mathrm{mod}\ n) \end{align}\right.\]

求解 \(f\) 的值(注意,这里只能获取一次 \(v_0\)\(v_1\)

\(v = x_1\),因为 \(d\) 为奇数,而 \((p\pm q)^d\) 的中间项至少包含一个 \(pq=n\),所以被模除了,只剩下 \(p^d\pm q^d\)

\[ v_0-v_1 \equiv ((x_1-x_0)^d + 2q^d)(\mathrm{mod}\ n) \]

第一阶段就卡在这里了,第二阶段根据提示才找到后面的做法

\[\begin{align} (v_0-v_1)^e &\equiv ((x_1-x_0)^d + 2q^d)^e(\mathrm{mod}\ n) \\ &\equiv ((x_1-x_0) + Kq)(\mathrm{mod}\ n) (K\in \mathbb{Z}) \end{align}\]

那么 \(((v_0-v_1)^e-(x_1-x_0))(\mathrm{mod}\ n)\) 能被 \(q\) 整除,这样只需要求 \(gcd(((v_0-v_1)^e-(x_1-x_0))(\mathrm{mod}\ n), n)\),就能求出 \(q\) 了。然后后面就求出 \(p,d\),求出 \(f\) 了。代码如下:

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
from math import gcd

from Crypto.Util.number import long_to_bytes
from pwn import *

token = "<your_token>"

p = remote("prob07.geekgame.pku.edu.cn", 10007)
p.recvuntil(b"token: ")
p.sendline(token.encode())
p.recvuntil(b"level[1/2]: ")
p.sendline(b'1')
n = int(p.recvline().decode().strip().replace("n = ", ""))
e = int(p.recvline().decode().strip().replace("e = ", ""))
x0 = int(p.recvline().decode().strip().replace("x0 = ", ""))
x1 = int(p.recvline().decode().strip().replace("x1 = ", ""))
p.recvuntil(b"v = ")
p.sendline(str(x1).encode())
p.recvline()
v0 = int(p.recvline().decode().strip().replace("v0 = ", ""))
v1 = int(p.recvline().decode().strip().replace("v1 = ", ""))
p.close()

q = gcd((pow(v0-v1, e, n) - (x1-x0))%n, n)
assert q != 1 and n%q == 0
p = n//q
d = pow(e, -1, (p-1)*(q-1))
f = (v1 - pow(p, d, n) + pow(q, d, n))%n
print(long_to_bytes(f))

速速去写论文!!!😵


GeekGame 2024 WriteUp
https://blog.bcb.pub/2024/10/20/ctf/geekgame2024/
作者
BadCodeBuilder
发布于
2024年10月20日
许可协议