NahamCon2022

NahamCon CTF 2022 中 XORROX、Unimod、Baby RSA Quiz 和 Ostrich 的题解。

今天玩了玩NahamCon CTF 2022,凌晨三点开始比赛。当然,我肯定是睡觉起床了再打。

起床之后队友把热身题全扫了,我也没啥题可以做了(菜鸡一个。后面找了以下难度不大的crypto写。以下是writeup。后面我还会写一下别人的writeup,因为有些题没写出来。

XORROX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3

import random

with open("flag.txt", "rb") as filp:
flag = filp.read().strip()

key = [random.randint(1, 256) for _ in range(len(flag))]

xorrox = []
enc = []
for i, v in enumerate(key):
k = 1
for j in range(i, 0, -1):
k ^= key[j]
xorrox.append(k)
enc.append(flag[i] ^ v)

with open("output.txt", "w") as filp:
filp.write(f"{xorrox=}\n")
filp.write(f"{enc=}\n")

可以看到是一个简单的多次异或。异或是可逆的,而且我们的key元素中的第一个也就是key[0]是不会被修改的。所以我们只需要再跑一下这个循环,把key跑出来就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# solve.py
xorrox=[1, 209, 108, 239, 4, 55, 34, 174, 79, 117, 8, 222, 123, 99, 184, 202, 95, 255, 175, 138, 150, 28, 183, 6, 168, 43, 205, 105, 92, 250, 28, 80, 31, 201, 46, 20, 50, 56]
enc=[26, 188, 220, 228, 144, 1, 36, 185, 214, 11, 25, 178, 145, 47, 237, 70, 244, 149, 98, 20, 46, 187, 207, 136, 154, 231, 131, 193, 84, 148, 212, 126, 126, 226, 211, 10, 20, 119]
key = []
for i, v in enumerate(xorrox):
k = 1
for j in range(i, 0, -1):
k ^= xorrox[j]
xorrox[i] = k
print(chr(enc[i]^k), end="")
key.append(k)
print(key, len(key))


xorrox = []
for i, v in enumerate(key):
k = 1
for j in range(i, 0, -1):
k ^= key[j]
xorrox.append(k)
print(xorrox)

Unimod

这题有点蛋疼。因为直接看数据文件格式有点不对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import random
import libnum

flag = open('flag.txt', 'r').read()
ct = ''
k = random.randrange(0,0xFFFD)
# k = 15449
for c in flag:

ct += chr((ord(c) + k) % 0xFFFD)

# a = chr((ord(c) + k) % 0xFFFD)
# print((ord(c) + k) % 0xFFFD)
# print(libnum.s2n(chr((ord(c) + k) % 0xFFFD)))
# print(ord(a))

open('out', 'wb').write(ct.encode())

注释部分是我测试的部分。原题目是没有的。

以下是原题目的十六进制数据。可以看到很明显的三个字节一个分组。但是从代码上看,我们的到的数据应该是两个字节一个分组。

1
E9 A5 87 E9 A5 8D E9 A5 82 E9 A5 88 E9 A5 9C E9 A4 95 E9 A5 86 E9 A4 97 E9 A4 99 E9 A5 85 E9 A4 92 E9 A4 97 E9 A5 82 E9 A4 97 E9 A4 92 E9 A5 83 E9 A5 84 E9 A4 93 E9 A5 86 E9 A5 82 E9 A4 98 E9 A4 93 E9 A5 85 E9 A4 96 E9 A5 87 E9 A4 9A E9 A4 98 E9 A4 92 E9 A4 94 E9 A4 95 E9 A4 95 E9 A5 86 E9 A4 99 E9 A4 95 E9 A5 87 E9 A4 92 E9 A4 92 E9 A5 9E E9 A3 AB

所以有了注释中的三条输出语句:

1
2
3
15565
14922637
15565

可以看到,这个数据经过编码成了三个字节。所以我们直接读文件,然后用三个字节分组,使用ord函数来解码,才能得到正确的值。

这里其实有个k,但是影响不大。这里0xFFFD其实比我们ASCII码的范围大了太多。基本上不会出现取的k刚好在0xFFFD的边界上。所以直接解就行了,如果不行那也就再加个0xFFFD进行处理就行。

1
2
3
4
5
6
7
8
9
10
# solve.py
data = open(r"out", "rb").read()

# 反正挺坑的,一个2字节的数据二进制写入成了3字节,这里直接三字节分片然后用ord取他的数字就行

for i in range(0, len(data), 3):
a = data[i:i+3].decode()
print(chr(ord(a) - 39137), end="")

# 通过猜测第一个字符是'f', 来计算k = 39137

Baby RSA Quiz

这题没有给文件而是以ssh的方式进行的。

一共三小题,第一小题给的是个小n,直接分解就行了。

1
2
3
4
5
6
7
8
 ---------
| Part 1: |
---------
n = 115398298544369
e = 65537
ct = 37386859625793

What is the plaintext (in integer form)?

这个直接上网站一分解就行。得到p,q求解d,然后算m。这里答案是整型的,但其实也是能转成字符串的。

1
2
3
4
5
6
7
 ---------
| Part 2: |
---------
n = 21984385600649967782331863593924102436585066484191623195882222547673570244475780629687822167989708574426866726594728874805115343269798282457109572103808168717297643786879687407125835510708369369452066036081265495727817566573832554802942178842361272009775406486896598963082180982538712496148845771649987025233485619836564348850399478693602977813231635752371346486494990471592789061740477387129729970592609102941311866809610802313761260294058835607292101793124755896831042571976602987089357127373715869012159472398776658325559448411402063905089012465509274881162806001450685859679444595914638449991047584895744421268419
e = 3
ct = 26480272848384180570411447917437668635135597564435407928130220812155801611065536704781892656033726277516148813916446180796750368332515779970289682282804676030149428215146347671350240386440440048832713595112882403831539777582778645411270433913301224819057222081543727263602678819745693540865806160910293144052079393615890645460901858988883318691997438568705602949652125
What is the plaintext (in integer form)?

这里可以看到加密指数e很小,所以我们只需要开方求解就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# solve.py
from gmpy2 import iroot
import libnum

e = 0x3
n = 21984385600649967782331863593924102436585066484191623195882222547673570244475780629687822167989708574426866726594728874805115343269798282457109572103808168717297643786879687407125835510708369369452066036081265495727817566573832554802942178842361272009775406486896598963082180982538712496148845771649987025233485619836564348850399478693602977813231635752371346486494990471592789061740477387129729970592609102941311866809610802313761260294058835607292101793124755896831042571976602987089357127373715869012159472398776658325559448411402063905089012465509274881162806001450685859679444595914638449991047584895744421268419
c = 26480272848384180570411447917437668635135597564435407928130220812155801611065536704781892656033726277516148813916446180796750368332515779970289682282804676030149428215146347671350240386440440048832713595112882403831539777582778645411270433913301224819057222081543727263602678819745693540865806160910293144052079393615890645460901858988883318691997438568705602949652125

k = 0
while 1:
res = iroot(c + k * n, e) # c+k*n 开3次方根 能开3次方即可
if (res[1] == True):
print(int(res[0]))
print(libnum.n2s(int(res[0]))) # 转为字符串
break
k = k + 1

这里我打印了数字也打印了字符串,是从我之前的代码上改的。

1
2
3
4
5
6
7
8
9
10
11
 ---------
| Part 3: |
---------
q = p + 2
while !(isPrime(q)):
q += 2
n = p*q
n = 88881615737488301225985044439881905195398292505026841973979114989583620386889966196936383863573165224625895278832099461610638739494998960540693208136536764698022077924393297727577553457866191347931512435919295847354039191586621873435195696477610004925566087150861042148930517495281441139741545346918379251933
e = 65537
ct = 20631059905657990621121472678172311137355898932087354262172309893885321498925560019065033573185005618575156342764326342253251212781095718707971251014407982342185739678572043698218123534431914415817339019215948120246829763194761668345794978860628612447418680047622373133912651452245133307912003452486642093319
What is the plaintext (in integer form)?

这里给出了两个素数生成的方法。可以看到这两个素数应该是比较接近的。

所以直接用yafu尝试进行分解。yafu比较适合这种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fac: factoring 88881615737488301225985044439881905195398292505026841973979114989583620386889966196936383863573165224625895278832099461610638739494998960540693208136536764698022077924393297727577553457866191347931512435919295847354039191586621873435195696477610004925566087150861042148930517495281441139741545346918379251933
fac: using pretesting plan: normal
fac: no tune info: using qs/gnfs crossover of 95 digits
div: primes less than 10000
fmt: 1000000 iterations
Total factoring time = 0.6750 seconds


***factors found***

P154 = 9427704690829486372323834254401690991535897190805758825455941539744129948703411885292076952568876153202122275131143524310245302189466786096848123870588379
P154 = 9427704690829486372323834254401690991535897190805758825455941539744129948703411885292076952568876153202122275131143524310245302189466786096848123870588327

ans = 1

可以看到很快就出答案了。这样p和q就分解出来了。后面就是单纯的rsa运算了。

后面还做了一道隐写

Ostrich

一共给了三个文件。

一张图片,一个算法,一个结果。

应该对算法进行逆向就行了。

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
import imageio
from PIL import Image, GifImagePlugin
from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l
import random
from apng import APNG

filenames = []
flag = "REDACTED"

orig_filename = "ostrich.jpg"
orig_image = Image.open(orig_filename)
pixels = orig_image.load()
width, height = orig_image.size
images = []

for i in range(len(flag)):
new_filename = f'./images/ostrich{i}.png'
new_image = Image.new(orig_image.mode, orig_image.size)
new_pixels = new_image.load()
for x in range(width):
for y in range(height):
new_pixels[x,y] = orig_image.getpixel((x, y))

x = random.randrange(0,width)
y = random.randrange(0,height)
pixel = list(orig_image.getpixel((x, y)))
while(pixel[2] == 0):
x = random.randrange(0,width)
y = random.randrange(0,height)
pixel = list(orig_image.getpixel((random.randrange(0,width), random.randrange(0,height))))

new_val = l2b(pixel[2]*ord(flag[i]))
pixel[0] = new_val[0]
if len(new_val) > 1:
pixel[1] = new_val[1]
pixel[2] = 0

new_pixels[x, y] = (pixel[0], pixel[1], pixel[2])
new_image.save(new_filename)
filenames.append(new_filename)
images.append(new_image)

APNG.from_files(filenames, delay=0).save("result.apng")

最后保存的是一个APNG文件,这是个动图文件,但这里不会动就是了。

大致流程先看了一遍。我们先把文件提出来,这里的APNG动图也是多幅图片在一起构成的。每张图片里面嵌入了一个字节。

1
2
3
4
5
6
7
import PIL
from apng import APNG

img = APNG.open(r"result.apng")

for i, (png, control) in enumerate(img.frames):
png.save("./extract/{i}.png".format(i=i))

一共提取了38张图片。

来讲一下嵌入的流程。

1
2
3
4
5
6
7
x = random.randrange(0, width)
y = random.randrange(0, height)
pixel = list(orig_image.getpixel((x, y)))
while (pixel[2] == 0):
x = random.randrange(0, width)
y = random.randrange(0, height)
pixel = list(orig_image.getpixel((random.randrange(0, width), random.randrange(0, height))))

最主要的部分在这里。

这里要找一个B通道值不为0的点。原始图像中找一个B通道不为0的点,基本上所有的点都不可能B通道为0,所以其实就是随机的在里面找一个点。

1
2
3
4
5
new_val = l2b(pixel[2] * ord(flag[i]))
pixel[0] = new_val[0]
if len(new_val) > 1:
pixel[1] = new_val[1]
pixel[2] = 0

在这里面,又把我们的值改成了0。这就让这个点很好找了。。

所以我们只需要找到这个点,读取这个点的像素值,然后和原始图像的这个点做运算就可以得到flag。

只要和原图比较多了哪一个b通道值为0 的点就行。

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
# solve.py
from PIL import Image, GifImagePlugin
from apng import APNG
from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l

origin = Image.open(r"ostrich.jpg")
zero_dot = []
width, height = origin.size

for x in range(width):
for y in range(height):
if origin.getpixel((x,y))[2] == 0:
zero_dot.append((x,y))

for i in range(38):
current_img = Image.open(f"./extract/{i}.png")
width, height = current_img.size
image = Image.new(current_img.mode, current_img.size)
pixels = image.load()
change_dot = None
for x in range(width):
if change_dot is not None:
break
for y in range(height):
pixels[x, y] = current_img.getpixel((x, y))
if pixels[x,y][2] == 0:
if (x,y) not in zero_dot:
# print(f"{i} :",(x,y), "is the change point")
change_dot = (x,y)

origin_blue = origin.getpixel(change_dot)[2]

# 基本上都会出现两个字节长度
value = bytes([current_img.getpixel(change_dot)[0],current_img.getpixel(change_dot)[1]])
value = b2l(value)

print(chr(value//origin_blue), end="")

以下是后面添加的。(2022/05/03)

Steam Locomotive

这题很好玩。

用ssh链接目标服务器之后。执行的是SL这个命令,而且此时我们会被阻塞无法操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21


( ) (@@) ( ) (@) () @@ O @ O @ O
(@@@)
( )
(@@@@)

( )
==== ________ ___________
_D _| |_______/ \__I_I_____===__|_________|
|(_)--- | H\________/ | | =|___ ___| _________________
/ | | H | | | | ||_| |_|| _| \_____A
| | | H |__--------------------| [___] | =| |
| ________|___H__/__|_____/[][]~\_______| | -| |
|/ | |-----------I_____I [][] [] D |=======|____|________________________|_
__/ =| o |=-~~\ /~~\ /~~\ /~~\ ____Y___________|__|__________________________|_
|/-=|___|=O=====O=====O=====O |_____/~\___/ |_D__D__D_| |_D__D__D_|
\_/ \__/ \__/ \__/ \__/ \_/ \_/ \_/ \_/ \_/



火车跑完,连接也断开了。这是一道简单题,但是我真的不会做。后面看别人的writeup才知道。原来ssh连接可以直接带命令。

1
2
3
C:\Users\lanpesk>ssh -p 31404 user@challenge.nahamcon.com ls
user@challenge.nahamcon.com's password:
flag.txt

这里我们连接的时候直接带上一个ls命令。结果我们可以看到有个flag文件。

1
2
3
C:\Users\lanpesk>ssh -p 31404 user@challenge.nahamcon.com cat flag*
user@challenge.nahamcon.com's password:
flag{4f9b10a81141c7a07a494c28bd91d05b}

这样我们就能在ssh阻塞的情况下执行其他命令。算是学到了一些小知识。我觉得可以出在我们学校的新生赛里面嘿嘿。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!