Skip to content

Commit bffc62d

Browse files
committed
update monad's writeups
1 parent 7fd4465 commit bffc62d

21 files changed

+1761
-2
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
| [zxx](players/zxx/README.md) | 总排名第 307 名 | 进制十六 -- 参上, 去吧!追寻自由的电波,FLAG 助力大红包,minecRaft |
7878
| [yyuueexxiinngg](players/yyuueexxiinngg/README.md) | 总排名第 152 名 | 签到、进制十六——参上、去吧!追寻自由的电波、猫咪问答 Pro Max、卖瓜、透明的文件、旅行照片、FLAG 助力大红包、Amnesia{轻度失忆}、图之上的信息、赛博厨房(Level 0、Level 1)、minecRaft |
7979
| [Crabtux](players/Crabtux/) | 总排名第 91 名 | 签到,进制十六——参上,去吧!追寻自由的电波,猫咪问答 Pro Max,卖瓜,透明的文件,旅行照片,FLAG 助力大红包,Amnesia(轻度失忆),图之上的信息,Easy RSA,加密的 U 盘,minecRaft,超 OI 的 WriteUp 模拟器(果然还是逆向比较简单) |
80-
| [monad](players/monad/) | 总排名第 14 名 | 签到、进制十六——参上、去吧!追寻自由的电波、猫咪问答 Pro Max、透明的文件、旅行照片、FLAG 助力大红包、Amnesia-轻度失忆、图之上的信息、Easy RSA、加密的 U 盘、赛博厨房(Lv0-2)、Micro World、助记词、密码生成器、p😭q、(还在更新中) |
80+
| [monad](players/monad/) | 总排名第 14 名 | 签到、进制十六——参上、去吧!追寻自由的电波、猫咪问答 Pro Max、卖瓜、透明的文件、旅行照片、FLAG 助力大红包、Amnesia-轻度失忆、图之上的信息、Easy RSA、加密的 U 盘、赛博厨房(Lv0-2)、Micro World、阵列恢复大师(RAID0+5)、助记词、马赛克、minecRaft、密码生成器、JUST BE FUN、p😭q |
8181
| [Sora](https://www.sorasky.in/hg2021.sorasky) | 总排名第 102 名 | 签到,进制十六——参上,去吧!追寻自由的电波,猫咪问答 Pro Max,卖瓜,透明的文件,旅行照片,FLAG 助力大红包,Amnesia 轻度失忆,图之上的信息,加密的U盘,Micro World,马赛克 |
8282
| [ThinkingNull](https://awsl.blog/2021/ustc-hack-2021) | | 猫咪问答 Pro Max、旅行照片 |
8383
| [風的影子](players/windshadow/) | 总排名第 32 | 签到题、去吧!追寻自由的电波、猫咪问答、卖瓜、透明的文件、旅行照片、FLAG 助力大红包、图之上的信息、Easy RSA、加密的 U 盘、Micro World、马赛克、minecRaft、p😭q|

players/monad/10-Easy RSA.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ $$
6262

6363
然而我不会求,查资料,发现了这个:[Finding the k-th root modulo m](https://math.stackexchange.com/questions/2073284/finding-the-kth-root-modulo-m),简直完美。
6464

65-
按照这篇文章(回答)所说,首先求满足这一式子的 $k$, $v$:
65+
按照这篇文章(回答)所说,首先求满足这一式子的 $u$, $v$:
6666

6767
$$
6868
u \, e - v \, \varphi(n) = 1

players/monad/16-Micro World.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
接着继续尝试反编译,我这里找了 [zrax/pycdc](https://github.com/zrax/pycdc),能够逆向 Python 3.9 的字节码,但不完全能逆向出来(但是至少能看到大致的逻辑,也能看到点的数据)。
88

9+
![](assets/microworld_decompile.png)
10+
911
这是 `2.pyc``Point` 的定义:
1012

1113
```python
+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# 阵列恢复大师
2+
3+
<del>数据恢复软件都是浮云,手撕才是正道。</del>
4+
5+
当时做题的时候,看着 RAID 5 通过的人比 RAID 0 多,所以就先跑去做 RAID 5 了。后来仔细思索一下,可能是因为 RAID 5 有软件能一键恢复吧。
6+
7+
## 1 - RAID 0
8+
9+
额,在恢复之前,先来看一下 RAID 0 的结构。
10+
11+
![](assets/RAID_0.svg)
12+
13+
说白了大概就是按块大小分块,然后按顺序依次存放在各个盘中。所以一个推论就是,一段内容只会出现在一个盘里面。
14+
15+
为了方便起见,先把硬盘名字按照字母序重命名成 1\~8.img。
16+
17+
先随便看看这些盘的内容,然后不难发现,8.img 的 0x00000200 处有一个 `EFI PART`,这是 GPT 分区表的标志。于是就不难推测出 8.img 是第一个盘。
18+
19+
![](assets/raid0_disk_8_head.png)
20+
21+
然后的话,再随便翻翻这些盘的内容,发现似乎空的部分(`0x00`)比较多。要确定盘的顺序的话,因为块与块之间是直接拼起来的,所以可以直接看内容的连续性来判断块的顺序,进而判断盘的顺序。
22+
23+
这样的话,随便翻看几下,发现 `0x008C0000``0x008E0000` 附近的内容有断层,而且丰富性比较高,有文本、乱码、零。并且从这两个“断层”的距离不难推测出块的大小是 128KB。(注意下面第 4 块盘的 `0x008E0000` 前的部分,`n` 后面应该有一个空格。)
24+
25+
| Disk | `0x008C0000`| `0x008C0000`| `0x008E0000`| `0x008E0000`|
26+
| :--: | :----------------: | :------------------: | :----------------: | :----------------: |
27+
| 1 | 0x00000000 | ``2J..`...HD.....k`` | `Subtype /Link /R` | 0x00000000 |
28+
| 2 | 0x00000000 | `ect [118.3625 23` | `g_system_respons` | 0x00000000 |
29+
| 3 | 0x00000000 | `iveness_under_lo` | `...:......b.<.dL` | 0x00000000 |
30+
| 4 | 0x00000000 | `.q,.[.Q..B....Y.` | `0470796 00000 n ` | 0x00000000 |
31+
| 5 | `0 obj.<< /A 489 ` | `..g..b.c.D\...G.` | `F.O...\i...o.G..` | 0x00000000 |
32+
| 6 | 0x00000000 | `000 n .000007636` | `M.t.p...V+.,=.2.` | 0x00000000 |
33+
| 7 | 0x00000000 | `.0000470505 0000` | `4....%......,.iI` | 0x00000000 |
34+
| 8 | 0x00000000 | `0 R /Border [ 0 ` | `n .0000076610 00` | `.0..TL$w.......1` |
35+
36+
根据 RAID 0 的结构,不难推测出 `1 - 2``2 - 3``4 - 7``5 - 8``8 - 6`(能分成三大块:`5 8 6``1 2 3``4 7`)。(如果觉得看表难推测的话,可以再看看这些位置的上下文,找一下文本的规律(常见词等)。)
37+
38+
到这里,可能的盘的顺序就只剩下 `8 6 1 2 3 4 7 5``8 6 4 7 1 2 3 5` 两种,已经很优了。如果懒的话,可以直接两种方法都试一下,看看哪种对的就行了。
39+
40+
实际上,在 `0x001C0000` \~ `0x001E0000` 的地方,可以观测到内容从有(非 0x00)变无(0x00):
41+
42+
| Disk | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
43+
| :-------: |:-:|:-------:|:-:|:-:|:-:|:-:|:-:|:-:|
44+
| Not 0x00? | T | partial | F | F | F | T | F | T |
45+
46+
据此,能推断出第 2 块盘在第 4 个位置。也就是说上面的两个方案,`8 6 1 2 3 4 7 5` 是对的。
47+
48+
既然顺序和块大小(128 KB)都有了,直接恢复出完整镜像(我用了一下 DiskGenius),然后把镜像挂载一下就行了。
49+
50+
## 2 - RAID 5
51+
52+
同理,先来看看 RAID 5 的结构。
53+
54+
![](assets/RAID_5.svg)
55+
56+
与上面 RAID 0 不同的是,它多了一块奇偶校验块。
57+
58+
也像上面一样,先瞅瞅这些盘的头部。
59+
60+
![](assets/raid5_disk_23_head.png)
61+
62+
然后可以发现 2.img 和 3.img 有 GPT 头,故可以确定这两个盘一个是第一个位置,另一个是最后一个位置(首个校验块所在盘)。而且在 2.img 的 `0x0040400` 的地方可以找到 ext2/3/4 的 superblock。
63+
64+
所以现在就只剩下 12 种方案了……这一次我比较懒,就没有继续了(而且也懒得分析比较复杂的 RAID 5 了)。
65+
66+
然后我真的就去莽了,块大小 64 KB 和 256 KB 都莽一下(不要问我为什么不试 128 KB),再把上面 12 种方案都过一遍。实际操作就是在 DiskGenius 上排顺序,看看那个拼好了之后,DiskGenuis 能找到 ext2/3/4 分区。
67+
68+
最后试了几下,试出来了 `2 4 1 5 3`(块大小忘了实际上是多少了),然后用 DiskGenius 导出镜像(不知道为啥我那玩意不支持导出大于 1 MB 的文件),然后瞎搞一下,mount 上了就行了。
69+
70+
<del>为什么我导出镜像之后还要用 testdisk 找一下分区后,导出分区才能挂载。</del>

players/monad/22-马赛克.md

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# 马赛克
2+
3+
这道题的简单题意就是,给你一个打了码的二维码,并且给出打码的程序(算法是模糊部分的一个大的 block 的颜色,是这个区域内颜色的平均值)。<del>简单算一下信息量,感觉还是能复原的。</del>
4+
5+
一些用词(为了避免混乱所以先说明):二维码的块:指模糊前的二维码的一个黑白块;码的块:指打码部分的一个色块(应该没毛病吧)。
6+
7+
于是乎,最朴素的方法就是……枚举原二维码每个块的黑白,然后看看跟模糊后的是否一致,但是这样做时间成本太大了。
8+
9+
然后可以发现,这个码的一个块的影响范围是有限的,大概就 10 个左右的二维码块。于是我们就可以针对每个码的块,枚举其下的二维码的黑白。再把每一个块的有效方案拼起来即可。
10+
11+
<del>原理听懂了吗?原理大概就这个样子。但是……你发现这个确实并不好实现(大概?)</del>
12+
13+
具体实现的话,假设前面处理了若干个块,这些块拼在一起的有效方案(可能有多个)放在一个数组 `p` 里。然后处理一个新的块的时候,就把当前区域的方案,与 `p` 中的匹配(能相容的就结合),把新的方案放到一个新的数组 `q` 供下一轮使用。
14+
15+
具体代码(调试输出十分炫酷,建议运行):
16+
17+
```python
18+
import random
19+
import math
20+
import numpy as np
21+
from PIL import Image
22+
23+
X, Y = 103, 137 # 马赛克左上角位置(单位为像素)
24+
N = 20 # 马赛克块的数量(共N*N块)
25+
BOX_SIZE = 23 # 每个马赛克块的大小(边长,单位为像素)
26+
PIXEL_SIZE = 11 # 二维码每个块的大小(边长,单位为像素)
27+
28+
29+
def calc_block_color(img, x, y):
30+
x1 = X + x * BOX_SIZE
31+
x2 = X + (x + 1) * BOX_SIZE
32+
y1 = Y + y * BOX_SIZE
33+
y2 = Y + (y + 1) * BOX_SIZE
34+
return math.floor(img[x1:x2, y1:y2].mean())
35+
36+
37+
def check_qr_block_in(mx, my, qx, qy):
38+
# 检查 (qx, qy) 这个二维码块 是否影响 (mx, my) 这个打码块
39+
X1 = X + mx * BOX_SIZE
40+
X2 = X + (mx + 1) * BOX_SIZE - 1
41+
Y1 = Y + my * BOX_SIZE
42+
Y2 = Y + (my + 1) * BOX_SIZE - 1
43+
44+
x1 = qx * PIXEL_SIZE
45+
x2 = (qx + 1) * PIXEL_SIZE - 1
46+
y1 = qy * PIXEL_SIZE
47+
y2 = (qy + 1) * PIXEL_SIZE - 1
48+
49+
if (X1 <= x1 <= X2 and Y1 <= y1 <= Y2) or (X1 <= x1 <= X2 and Y1 <= y2 <= Y2) \
50+
or (X1 <= x2 <= X2 and Y1 <= y1 <= Y2) or (X1 <= x2 <= X2 and Y1 <= y2 <= Y2):
51+
return True
52+
return False
53+
54+
55+
def paint_with(img, x, y, color):
56+
# 将 (x, y) 二维码块涂成 color 色
57+
x1 = x * PIXEL_SIZE
58+
x2 = (x + 1) * PIXEL_SIZE
59+
y1 = y * PIXEL_SIZE
60+
y2 = (y + 1) * PIXEL_SIZE
61+
img[x1:x2, y1:y2] = color
62+
63+
64+
class Plan:
65+
def __init__(self, plan = None):
66+
if plan is None:
67+
plan = {}
68+
self.plan = plan
69+
self.min_x = 10000
70+
self.min_y = 10000
71+
72+
def compatible(self, other):
73+
# 检查此 plan 是否与 `other` 兼容
74+
for (x, y), value in self.plan.items():
75+
if (data := other.plan.get((x, y))) is not None and data != value:
76+
return False
77+
return True
78+
79+
def push(self, x, y, data):
80+
self.min_x = min(self.min_x, x)
81+
self.min_y = min(self.min_y, y)
82+
self.plan[(x, y)] = data
83+
84+
def combine(self, other):
85+
# 将此 plan 与 `other` 结合,返回新的 plan
86+
assert self.compatible(other)
87+
new = Plan(self.plan.copy())
88+
for (x, y), value in other.plan.items():
89+
new.push(x, y, value)
90+
return new
91+
92+
93+
def get_initial_plan(img):
94+
# 这是获取初始的 plan,主要包含码周边的一圈二维码块
95+
96+
X1 = X + 0 * BOX_SIZE
97+
X2 = X + N * BOX_SIZE
98+
Y1 = Y + 0 * BOX_SIZE
99+
Y2 = Y + N * BOX_SIZE
100+
101+
def is_in(x, y):
102+
return X1 <= x <= X2 and Y1 <= y <= Y2
103+
104+
plan = Plan()
105+
106+
for x, y in np.ndindex(57, 57):
107+
for i, j in np.ndindex(N, N):
108+
if check_qr_block_in(i, j, x, y):
109+
x1 = x * PIXEL_SIZE
110+
x2 = (x + 1) * PIXEL_SIZE - 1
111+
y1 = y * PIXEL_SIZE
112+
y2 = (y + 1) * PIXEL_SIZE - 1
113+
114+
if not is_in(x1, y1):
115+
color = img.getpixel((y1, x1))
116+
plan.push(x, y, color)
117+
break
118+
elif not is_in(x2, y2):
119+
color = img.getpixel((y2, x2))
120+
plan.push(x, y, color)
121+
break
122+
123+
POSITION_BLOCK = [(50, 50), (50, 28), (28, 50), (28, 28)] # 定位块
124+
for block in POSITION_BLOCK:
125+
for x, y in np.ndindex(5, 5):
126+
d = max(abs(x - 2), abs(y - 2))
127+
color = 0 if (d % 2 == 0) else 255
128+
plan.push(block[0] + x - 2, block[1] + y - 2, color)
129+
130+
return plan
131+
132+
133+
def iter_blocks():
134+
# 枚举顺序从四周到中心,方便先利用已知数据
135+
for d in range(N):
136+
for i, j in np.ndindex(N, N):
137+
dx = min(i, (N - 1) - i)
138+
dy = min(j, (N - 1) - j)
139+
if min(dx, dy) == d:
140+
yield (j, i)
141+
142+
143+
def main():
144+
mosaic = Image.open('pixelated_qrcode.bmp')
145+
temp = np.asarray(mosaic.copy(), dtype='uint8')
146+
147+
plans = [ get_initial_plan(mosaic) ]
148+
for i, j in iter_blocks():
149+
new_plans = []
150+
151+
related_block = []
152+
for x, y in np.ndindex(57, 57):
153+
if check_qr_block_in(i, j, x, y):
154+
related_block.append([x, y])
155+
n = len(related_block)
156+
157+
print(f'Solving ({i:2}, {j:2}), related blocks: {n}')
158+
159+
for b in range(2 ** n):
160+
plan = Plan()
161+
for index, block in enumerate(related_block):
162+
if ((2 ** index) & b) > 0:
163+
paint_with(temp, *block, 0)
164+
plan.push(*block, 0)
165+
else:
166+
paint_with(temp, *block, 255)
167+
plan.push(*block, 255)
168+
169+
color = calc_block_color(temp, i, j)
170+
correct_color = mosaic.getpixel((Y + j * BOX_SIZE, X + i * BOX_SIZE))
171+
if color == correct_color:
172+
for old_plan in plans:
173+
if plan.compatible(old_plan):
174+
new_plans.append(old_plan.combine(plan))
175+
176+
plans = new_plans
177+
178+
if len(plans) > 1000:
179+
ids = [ i for i in range(len(plans)) ]
180+
random.shuffle(ids)
181+
buckets = []
182+
for i in ids[:1000]:
183+
buckets.append(plans[i])
184+
plans = buckets
185+
186+
if len(plans) == 0: # 如果 Failed 了,请洗把脸回来,再跑一遍
187+
raise Exception('Failed')
188+
189+
# 比较炫酷的调试输出
190+
for x in range(9, 52):
191+
for y in range(12, 55):
192+
if (value := plans[0].plan.get((x, y))) is not None:
193+
c = '*' if value == 0 else '_'
194+
else:
195+
c = ' '
196+
print(c, end=' ')
197+
print()
198+
print(f'Current valid plans: {len(plans)}')
199+
200+
# 输出 10 张复原的二维码
201+
for i, plan in enumerate(plans[:10]):
202+
for (x, y), value in plan.plan.items():
203+
paint_with(temp, x, y, value)
204+
image = Image.fromarray(temp, mode='L')
205+
image.save(f'result-{i}.bmp')
206+
207+
208+
if __name__ == '__main__':
209+
main()
210+
```

0 commit comments

Comments
 (0)