# Misc-red_vs_blue
This was a timed game with 66 rounds of trial and error. Within a single connection the answers stayed the same: enter r or b, and after a wrong answer enter y to retry. I kept the correct prefix and replayed it on each restart so I could try the next unknown choice before the time limit.
from pwn import * | |
# sh=process ("./buu--rip") local 端 | |
sh = remote('node4.buuoj.cn', 28699) | |
s = [] | |
i=1 | |
tag=0 | |
q = sh.recvuntil(':') | |
while True: | |
if i==66: | |
break; | |
if tag==0: | |
sh.send('r') | |
q=sh.recvuntil(':') | |
print(q) | |
if b'again' not in q: | |
i+=1;s.append('r') | |
continue | |
else : | |
sh.send('y') | |
for m in s: | |
sh.recvuntil(':') | |
sh.send(m) | |
sh.recvuntil(':') # | |
tag = 1 | |
continue | |
elif tag==1: | |
sh.send('b') | |
q=sh.recvuntil(':') | |
print(q) | |
if b'again' not in q: | |
i += 1;s.append('b') | |
continue | |
else: | |
sh.send('y') | |
for m in s: | |
sh.recvuntil(':') | |
sh.send(m) | |
sh.recvuntil(':')# | |
tag = 0 | |
continue | |
print(i) | |
sh.interactive() |
My loop had a small bug: it stopped on round 66. I still had a guess left, though, and that got me the flag.
# Misc-funny_maze
Similar setup, this time with a conventional maze. Find a path, submit it and repeat for the next maze.
from pwn import * | |
dirs=[(0,1),(1,0),(0,-1),(-1,0)] #当前位置四个方向的偏移量 | |
path=[] #存找到的路径 | |
def mark(maze,pos): #给迷宫 maze 的位置 pos 标 "2" 表示 “到过了” | |
maze[pos[0]][pos[1]]=2 | |
def passable(maze,pos): #检查迷宫 maze 的位置 pos 是否可通行 | |
return maze[pos[0]][pos[1]]==0 | |
def find_path(maze,pos,end): | |
mark(maze,pos) | |
if pos==end: | |
print(pos,end=" ") #已到达出口,输出这个位置。成功结束 | |
path.append(pos) | |
return True | |
for i in range(4): #否则按四个方向顺序检查 | |
nextp=pos[0]+dirs[i][0],pos[1]+dirs[i][1] | |
#考虑下一个可能方向 | |
if passable(maze,nextp): #不可行的相邻位置不管 | |
if find_path(maze,nextp,end):#如果从 nextp 可达出口,输出这个位置,成功结束 | |
print(pos,end=" ") | |
path.append(pos) | |
return True | |
return False | |
def see_path(maze,path,leng): #使寻找到的路径可视化 | |
length = 0 | |
for i,p in enumerate(path): | |
if i==0: | |
maze[p[0]][p[1]] ="E" | |
elif i==len(path)-1: | |
maze[p[0]][p[1]]="S" | |
else: | |
maze[p[0]][p[1]] =3 | |
print("\n") | |
for r in maze: | |
for c in r: | |
if c==3: | |
print('\033[0;31m'+"*"+" "+'\033[0m',end="") | |
length+=1 | |
elif c=="S" or c=="E": | |
print('\033[0;34m'+c+" " + '\033[0m', end="") | |
elif c==2: | |
print('\033[0;32m'+"#"+" "+'\033[0m',end="") | |
elif c==1: | |
print('\033[0;;40m'+" "*2+'\033[0m',end="") | |
else: | |
print(" "*2,end="") | |
print() | |
print(length+1) | |
leng=length+1 | |
return leng | |
def maze_change(num): | |
MAZE=[] | |
row=col=0 | |
M=str(sh.recvuntil("P")) | |
print(M) | |
MAZE.append([]) | |
for m in M: | |
if col == num: | |
MAZE.append([]) | |
row+=1 | |
col=0 | |
if m == '\n': | |
continue | |
elif m == '#': | |
MAZE[row].append(1) | |
col+=1 | |
elif m == ' ': | |
MAZE[row].append(0) | |
col+=1 | |
elif m == 'S': | |
MAZE[row].append(0) | |
start=(row,col) | |
col+=1 | |
elif m =='E': | |
MAZE[row].append(0) | |
end=(row,col) | |
col+=1 | |
elif m =='P': | |
break | |
print(MAZE) | |
return MAZE, start, end | |
def gets(num): | |
maze=[] | |
start=(0,0) | |
end=(0,0) | |
maze,start,end=maze_change(num) | |
find_path(maze,start,end) | |
leng=0 | |
leng=see_path(maze,path,leng) | |
sh.sendline(str(leng+1)) | |
sh=remote("node4.buuoj.cn",27512) | |
print(sh.recvuntil(b'game')) | |
sh.sendline("1") | |
gets(11) | |
print(sh.recvuntil(b'level!')) | |
dirs=[(0,1),(1,0),(0,-1),(-1,0)] #当前位置四个方向的偏移量 | |
path=[] #存找到的路径 | |
gets(21) | |
print(sh.recvuntil(b'level!')) | |
dirs=[(0,1),(1,0),(0,-1),(-1,0)] #当前位置四个方向的偏移量 | |
path=[] #存找到的路径 | |
gets(31) | |
print(sh.recvuntil(b'level!')) | |
dirs=[(0,1),(1,0),(0,-1),(-1,0)] #当前位置四个方向的偏移量 | |
path=[] #存找到的路径 | |
gets(101) | |
sh.interactive() |
I copied part of the maze code from this implementation. What took me ages to debug was resetting the variables between rounds. I kept looking for a mistake in the order of the moves ( ̄_ ̄|||).
# Misc-ezSteganography
During the contest I only got the first half. The second clue pointed to the green channel, but we fixated on the first clue’s mention of QIM quantisation and went down a rabbit hole trying to work out what it meant (。﹏。).
The missing step was to XOR the Green 0 and Green 1 bit planes. StegSolve’s Image Combiner can do it directly.



# Misc-Just a GIF
I revisited this after the contest. It’s much the same idea as running_pixel. Splitting the GIF gave 451 frames: 41 groups of 11. Compare frame i of each group with frame i of the first group, marking the different pixels black:
from PIL import Image | |
path = "Just_a_GIF/" #填自己的路径 | |
for i in range(11): | |
picn = Image.new("RGB",(119,83),(255,255,255)) | |
for j in range(1,41): | |
p1=Image.open(path+str(i)+'.png') | |
p2=Image.open(path+str(i+j*11)+'.png') | |
for w in range(119): | |
for h in range(83): | |
if p1.getpixel((w,h)) != p2.getpixel((w,h)): | |
picn.putpixel((w,h),(0,0,0)) | |
picn.save(str(i)+'.png') |
That leaves eleven difference images:

I pieced them together by hand:

It looked like a Data Matrix code. Scanning it gave DASCTF{6bb73086aeb764b5727529d82b084cce} .
# Misc-Nuclear wastewater
Another one I revisited afterwards. The QR code was coloured. A teammate suggested looking at the RGB values: every block had two zero channels and a value in the third. That looked like a good place to hide the message. I extracted the numbers:
from PIL import Image | |
img = Image.open("Nuclear wastewater.png") | |
for i in range(3): | |
for j in range(0,230,10): | |
for t in range(0,230,10): | |
rgb=list(img.getpixel((t,j))) | |
if rgb[i]>32 and rgb[i]<128: | |
print(chr(rgb[i]),end='') |
Ys>UEJht#?ppeEFtstR#~:hi~tR:@s@YRteK#e@KsR&E&:eR:Eht/#iKtteYKhYKYhhhihhKtC2tt:HVEesY&#@Rj!seRi:eitEtKsetKtEE:hh#h#eYKYihhYK(Kt@iSY$KY/@pRsEetsip:~h@eeEs!E&&::EsEEei#/iYe#/ieKKt//iKYhh
The resulting character string contained a lot of repetitions, so I checked the frequencies:
from collections import Counter | |
f = 'Ys>UEJht#?ppeEFtstR#~:hi~tR:@s@YRteK#e@KsR&E&:eR:Eht/#iKtteYKhYKYhhhihhKtC2tt:HVEesY&#@Rj!seRi:eitEtKsetKtEE:hh#h#eYKYihhYK(Kt@iSY$KY/@pRsEetsip:~h@eeEs!E&&::EsEEei#/iYe#/ieKKt//iKYhh' | |
print(Counter(f)) |

I initially read the result as theKEYis:#R@/&p~!>UJ?FC2HVj(S$ , but the password didn’t work. Taoshen’s write-up pointed out that every character in the tail occurred just once. Their relative order wasn’t determined by frequency. Dropping that tail left the puzzle password #R@/&p~! .
Inside the archive was flag.txt. I’d been a bit optimistic about finding the flag in plain sight (T_T). Looking at its bytes revealed something else:

Zero-width characters again: U+200C, U+200D and U+200E. A Unicode steganography decoder recovered the clue: “On April 13, 2021, nuclear wastewater was especially severe in CTX1 City, Citrix County.”
Citrix CTX1 meant nothing to me at first. A search suggested it was some kind of encryption. Clearly I still had a lot to learn (;′⌒`). Looking it up led me to the corresponding operation in CyberChef.


Two passes produced flag{98047de9ce5aaa4c0031fb55e9dfac70} . And that was the end of Misc! (o゚v゚) ノ

