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 timefrom selenium import webdriverfrom 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:
看一眼发生什么事了。看到
最后加载了一个 page2.max.js
,点开一看全是
_0xffffffff
这样的变量,大概是混淆过了。
有一个 id
为 root
的节点,看起来内容像是加密过的,搜索了一下好像叫 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); } }; 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); } }; 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 ); } [0x9 *-0x40e +0x5ec +-0x1 *-0x1e93 ,0x3 *0x45 +0xc16 +0xce3 *-0x1 ,-0x11eb *0x1 +0x53 *0x2c +0x3aa ][_0x4bbdbc (0xd45 ,a0_0x523a18._0x5e86c5 )](()=> a0_0x504e28 (_0xab6759)), _0x4eb293[_0xe7c12d (0x137f ,a0_0x523a18._0x4b8b9f )](a0_0x204ab6,_0xab6759,_0x17cb53); 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()
来解决计算后的值罢。
综上
步骤如下:
修改 js 文件(可以在这里下载 修改后的),并放在
./static/js
目录下
在当前目录下启动一个 http 服务器,例如
python -m http.server
执行 Python 脚本,更加具体地:
获取带有密文的 index.html
,并存储在当前目录下
操作 Selenium 读取页面
定位到元素,并执行
getComputedStyle().getPropertyValue('content')
直接获得解码后的结果
拼接结果并获得 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 reimport timeimport requestsfrom selenium import webdriverfrom 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 gcdfrom Crypto.Util.number import long_to_bytesfrom 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))%nprint (long_to_bytes(f))
速速去写论文!!!😵