Preface
The initial idea of this topic was to introduce a genuine challenge of steganography algorithm in the current CTF steganography environment where creative ideas are rampant. Contestants are expected to strengthen the robustness of their steganography algorithms through the use of Python scripts for steganography and extraction. The algorithms should still be able to extract normally under image attacks. Therefore, an OJ platform was created in the hope that contestants could showcase the pure spirit of steganography algorithms. Despite this, the platform still has its shortcomings and the problems found during the competition will be discussed later.
Platform Architecture Design
The overall architecture is divided into two parts: the front end and the back end. The front end was developed using Element+Vite, while the back end was developed using Golang+Gin in conjunction with Docker.
The overall process is as follows:
Contestants write encrypt.py, which writes the watermark characters into the eight images in the system backend, and then generates new images encrypted.
Then execute psnr.py to judge the similarity between the image before and after encryption. If the similarity is equal to 100, it means there is no encryption and an error is returned.
When the similarity is less than 85, it is judged that the image is damaged too much, and an error is returned.
Only when the similarity is between 100 and 85 is the encryption passed. Many contestants gave feedback that the reason for displaying [0/1] when running the default template code is this.
psnr.py code:
import math
import numpy as np
import cv2
import sys
def calculate_psnr(img1, img2):
if img1.shape != img2.shape:
raise ValueError('输入图像的大小必须相同')
mse = np.mean((img1 - img2) ** 2)
max_pixel = 255.0
if mse == 0:
return 'NOP'
else:
psnr = 20 * math.log10(max_pixel / math.sqrt(mse))
if psnr > 85:
return 'Success'
else:
return 'NOP'
if __name__ == '__main__':
if len(sys.argv)!=3:
print("Error: Invalid number of arguments")
exit(0)
img1 = sys.argv[1]
img2 = sys.argv[2]
img1 = cv2.imread(img1)
img2 = cv2.imread(img2)
print(calculate_psnr(img1, img2))
Of course, in order to lower the difficulty, the range of PSNR was later expanded to between 100-70.
When the encryption is passed through PSNR, the backend will automatically run attack.py, attacking the encrypted image and then saving it as a new image for decryption.
At this point, the permission to write decryption scripts is unlocked.
Finally, contestants write the decryption process. The system will monitor the output of Docker logs, and judge whether the decryption is successful based on the output content, and finally return the result. [0/8], [6/8] or decryption successful.
Contestant speed ranking.
This chapter will release the contestants’ outstanding scripts and the rankings for best memory and time consumption, along with comments.
StegLab1-PointAttack
A total of 21 contestants correctly solved the problem, which is an advanced application of the rather basic LSB algorithm. Contestants only need to proficiently master the enhancement of robustness by adding a key to LSB, as well as understanding LSB, to easily conquer this problem.
attack.py
def attack(img):
img = Image.open(img)
draw = ImageDraw.Draw(img)
a,b = img.size
x1 = random.randint(0,a//2)
y1 = 0
x2 = random.randint(a//2,a)
y2 = b
draw.line((x1,y1,x2,y2), fill=(random.randint(0,255),random.randint(0,255),random.randint(0,255)))
for _ in range(5):
x1 = random.randint(0,a)
y1 = random.randint(0,a)
x2 = random.randint(0,a)
y2 = random.randint(0,a)
draw.line((x1,y1,x2,y2), fill=(random.randint(0,255),random.randint(0,255),random.randint(0,255)))
for _ in range(1000):
x = random.randint(0,a)
y = random.randint(0,a)
draw.point((x,y), fill=(random.randint(0,255),random.randint(0,255),random.randint(0,255)))
return img
It can be seen that the attack randomly scatters points and five horizontal lines on the image based on its original appearance. Contestants only need to reinforce their LSB scripts to pass.
Contestant speed ranking.
Ranking based on the total time of encryption and decryption:
username | encry time | decry time | time |
x0rc3r3rs | 11088 | 2140 | 13.228 |
SU-Team | 11057 | 2244 | 13.301 |
LaoGong | 11133 | 2186 | 13.319 |
Polaris | 11145 | 2196 | 13.341 |
天枢Dubhe | 11056 | 2288 | 13.344 |
L3H_Sec | 11192 | 2162 | 13.354 |
Nepnep | 11105 | 2280 | 13.385 |
Doub1e-S | 11218 | 2184 | 13.402 |
EDISEC | 11311 | 2213 | 13.524 |
L | 11309 | 2223 | 13.532 |
0psu3 | 11266 | 2282 | 13.548 |
0RAYS | 11336 | 2249 | 13.585 |
Onehalf | 11427 | 2262 | 13.689 |
zhanyi | 11665 | 2274 | 13.939 |
r4kapig | 11727 | 2307 | 14.034 |
A1natas | 12253 | 2229 | 14.482 |
LSP | 12102 | 2402 | 14.504 |
S1uM4i | 11257 | 3608 | 14.865 |
Vidar-Team | 12114 | 3076 | 15.19 |
擅长可持久化线段树签到不好意思走错片场了 | 12933 | 2335 | 15.268 |
n03tAck | 13910 | 2509 | 16.419 |
x0rc3r3rs
The algorithm time complexity of the x0rc3r3rs team’s master is the lowest, and their coding code is announced here.
Supplement: The master of this team submitted the time after the difficulty of PSNR was reduced, so the speed was faster and the code could pass. After testing, this code could not pass normally before the original PSNR.
class Solution:
def Encrypt(self, img, key) :
img = Image.open(img)
width, height = img.size
pixel_map = img.load()
key += '$' # Add a delimiter to the key
key_index = 0
for y in range(height):
for x in range(width):
pixel = list(pixel_map[x, y])
if key_index < len(key):
new_pixel = [ord(key[key_index])] * len(pixel)
pixel_map[x, y] = tuple(new_pixel)
key_index += 1
else:
break
return img
class Solution:
def Decrypt(self,img)-> str:
img = Image.open(img)
width, height = img.size
pixel_map = img.load()
key = ''
for y in range(height):
for x in range(width):
pixel = list(pixel_map[x, y])
char_value = np.bincount(pixel).argmax()
if char_value == 36: # '$'
return key
key += chr(char_value)
return key
天枢Dubhe
The fastest running team among those who submitted before the difficulty was reduced, the player who submitted the fastest.
class Solution:
def Encrypt(self, img, key) :
image = Image.open(img)
width, height = image.size
# Convert the encrypted message to binary
binary_encrypted_message = ''
for i in key:
if isinstance(i,int):
binary_encrypted_message += (bin(i)).replace('0b','').zfill(8)
else:
binary_encrypted_message += (bin(ord(i)).replace('0b','')).zfill(8)
max_message_length = width * height * 3 // 8
image_array = np.array(image)
# Flatten the image array to a 1D array
flat_image_array = image_array.ravel()
# Encode the encrypted message in the LSBs of the image pixels
for i in range(len(binary_encrypted_message)):
bit = int(binary_encrypted_message[i])
pixel_value = flat_image_array[i]
new_pixel_value = (pixel_value & 0xFE) | bit
flat_image_array[i] = new_pixel_value
# Reshape the 1D array back to the original image shape
encoded_image_array = flat_image_array.reshape(image_array.shape)
# Save the encoded image
encoded_image = Image.fromarray(np.uint8(encoded_image_array))
return encoded_image
class Solution:
def Decrypt(self,img)-> str:
encoded_image = Image.open(img)
encoded_image_array = np.array(encoded_image)
binary_message = ""
for i in range(0,8*10):
# Extract the LSB of each pixel value
bit = encoded_image_array.ravel()[i] & 1
binary_message += str(bit)
# Convert binary message to characters
message = ""
for i in range(0, len(binary_message), 8):
byte = binary_message[i:i+8]
char_code = int(byte, 2)
message += chr(char_code)
return message
选手内存占用排行
teamname | encrypt memory | decrypt memory | memory(mb) |
擅长可持久化线段树签到不好意思走错片场了 | 26075136 | 5128192 | 31.203328 |
L | 31604736 | 3117056 | 34.721792 |
SU-Team | 34832384 | 1585152 | 36.417536 |
0psu3 | 34484224 | 3244032 | 37.728256 |
EDISEC | 33353728 | 7135232 | 40.48896 |
天枢Dubhe | 34918400 | 5656576 | 40.574976 |
Doub1e-S | 34385920 | 6877184 | 41.263104 |
Onehalf | 37953536 | 3321856 | 41.275392 |
Polaris | 34680832 | 7598080 | 42.278912 |
A1natas | 35782656 | 6696960 | 42.479616 |
LaoGong | 34652160 | 8376320 | 43.02848 |
0RAYS | 32915456 | 11218944 | 44.1344 |
x0rc3r3rs | 34791424 | 9895936 | 44.68736 |
L3H_Sec | 34574336 | 10211328 | 44.785664 |
Vidar-Team | 35504128 | 12632064 | 48.136192 |
zhanyi | 41676800 | 6598656 | 48.275456 |
S1uM4i | 34533376 | 17399808 | 51.933184 |
Nepnep | 34988032 | 17195008 | 52.18304 |
n03tAck | 27631616 | 27631616 | 55.263232 |
LSP | 36503552 | 24981504 | 61.485056 |
r4kapig | 37474304 | 26669056 | 64.14336 |
L
class Solution:
def Encrypt(self, img_path, key):
img = Image.open(img_path)
if img.mode != 'RGB':
img = img.convert('RGB')
pixels = np.array(img)
bin_key = ''.join(format(ord(i), '08b') for i in key)
bin_key += '11111111' # 添加一个分隔符,表示密钥的结束
key_index = 0
# 只处理前n个像素,其中n为二进制密钥的长度
for j in range(len(bin_key)):
i, color_channel = divmod(j, 3)
pixel_value = pixels[i // img.width, i % img.width][color_channel]
pixel_bin = format(pixel_value, '08b')
new_pixel_bin = pixel_bin[:-1] + bin_key[key_index]
pixels[i // img.width, i % img.width][color_channel] = int(new_pixel_bin, 2)
key_index += 1
encrypted_img = Image.fromarray(pixels)
return encrypted_img
class Solution:
def Decrypt(self, img_path) -> str:
img = Image.open(img_path)
pixels = np.array(img)
bin_key = ""
max_length = 8 * 10 + 8 # 最长的密钥长度 + 分隔符
for j in range(max_length):
i, color_channel = divmod(j, 3)
pixel_value = pixels[i // img.width, i % img.width][color_channel]
bin_key += format(pixel_value, '08b')[-1]
if bin_key[-8:] == '11111111': # 如果遇到分隔符
bin_key = bin_key[:-8]
chars = [chr(int(bin_key[i:i+8], 2)) for i in range(0, len(bin_key), 8)]
key = ''.join(chars)
return key
A more expected situation, by specially handling the LSB contents to enhance robustness, has great reference value.
First Blood Team: A1natas
The master of the fastest team is quite impressive.
from PIL import Image
import numpy as np
class Solution:
def Encrypt(self, img_path, key):
img = Image.open(img_path)
img_array = np.array(img)
key_data = key.encode() # 将 key 转换为字节数据
key_data += b"\x14"
# 获取 key 的二进制表示
binary_key = ''.join(format(byte, '08b') for byte in key_data)
key_index = 0
for i in range(img_array.shape[0]):
for j in range(img_array.shape[1]):
if key_index < len(binary_key):
pixel = img_array[i, j]
pixel &= 0xFE # 将最低有效位设置为0
pixel |= int(binary_key[key_index]) # 将 key 的对应位写入最低有效位
img_array[i, j] = pixel
key_index += 1
encrypted_img = Image.fromarray(img_array)
#encrypted_img.save(f"{img_path}.enc.png")
return encrypted_img
def Decrypt(self, img_path):
img = Image.open(f"{img_path}.enc.png")
img_array = np.array(img)
extracted_key = ""
l = 0
flag = ""
for i in range(img_array.shape[0]):
for j in range(img_array.shape[1]):
extracted_key += str(img_array[i, j][-1] & 1)
# print(extracted_key)
l += 1
if l == 8:
l = 0
data = bytes([int(extracted_key, 2)])
extracted_key = ""
if data == b"\x14":
return flag
else:
flag += data.decode()
return ''
StegLab2-ToJPGAttack
The issue was brought up by the organizer’s thoughtlessness, resulting in the proposal to open up error messages to reduce the difficulty. As a result, hackers took various saucy actions to strip the platform of its ‘pants’. Therefore, after communication, confirmation and verification, there have been a total of five submissions for this question. The first, second and fourth blood were obtained by attacking the platform or using vulnerabilities to get the flag, so the scores of these three teams for this question are cancelled, and the solutions of the two current solvers are announced. The original intention of the question was to hope that the contestants would write an FFT for steganography.
attack.py
import sys
from PIL import Image,ImageDraw
import numpy as np
import builtins
import random
import os
def attack(img):
img = Image.open(img)
return img
if __name__ == "__main__":
if len(sys.argv)!=2:
print("Error: Invalid number of arguments")
exit(0)
img = sys.argv[1]
new = attack(img)
new.save(img[:-4]+"_attacked.jpg")
os.rename(img[:-4]+"_attacked.jpg",img[:-4]+"_attacked.png")
LaoGong
class Solution:
def str2bits(self,key):
dic={'!': 0, '@': 1, 'a': 2, 'b': 3, 'c': 4, 'd': 5, 'e': 6, 'f': 7, 'g': 8, 'h': 9, 'i': 10, 'j': 11, 'k': 12, 'l': 13, 'm': 14, 'n': 15, 'o': 16, 'p': 17, 'q': 18, 'r': 19, 's': 20, 't': 21, 'u': 22, 'v': 23, 'w': 24, 'x': 25, 'y': 26, 'z': 27, 'A': 28, 'B': 29, 'C': 30, 'D': 31, 'E': 32, 'F': 33, 'G': 34, 'H': 35, 'I': 36, 'J': 37, 'K': 38, 'L': 39, 'M': 40, 'N': 41, 'O': 42, 'P': 43, 'Q': 44, 'R': 45, 'S': 46, 'T': 47, 'U': 48, 'V': 49, 'W': 50, 'X': 51, 'Y': 52, 'Z': 53, '0': 54, '1': 55, '2': 56, '3': 57, '4': 58, '5': 59, '6': 60, '7': 61, '8': 62, '9': 63}
res=[]
for i in key:
t=dic[i]
for j in range(6):
res.append(t&1)
t>>=1
return res
def channel_avg(self,n,x,y):
vec=[(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
r=0
g=0
b=0
for i in vec:
xx=x+i[0]
yy=y+i[1]
r+=n[xx][yy][0]
g+=n[xx][yy][1]
b+=n[xx][yy][2]
ret=(r//8,g//8,b//8)
dr=0
dg=0
db=0
for i in vec:
xx=x+i[0]
yy=y+i[1]
dr+=(n[xx][yy][0]-ret[0])**2
dg+=(n[xx][yy][1]-ret[1])**2
db+=(n[xx][yy][2]-ret[2])**2
dr+=(n[x][y][0]-ret[0])**2
dg+=(n[x][y][1]-ret[1])**2
db+=(n[x][y][2]-ret[2])**2
from math import sqrt
dr=sqrt(dr/9)
dg=sqrt(dg/9)
db=sqrt(db/9)
return ret,(dr,dg,db)
def Encrypt(self, img, key) :
key=key.ljust(10,"!")
img = Image.open(img)
m=np.asarray(img)
import random
r = random.Random(678)
stream=self.str2bits(key)
#print(stream)
#print(len(stream))
n=np.copy(m)
self.s=stream
for t in range(1):
for i in stream:
x=r.randint(0,1023)
y=r.randint(0,1023)
avg,dx=self.channel_avg(n,x,y)
#print(n[x][y][0]-avg[0],n[x][y][1]-avg[1],n[x][y][2]-avg[2])
if(i):
n[x][y][0]=255
n[x][y][1]=255
n[x][y][2]=255
else:
n[x][y][0]=0
n[x][y][1]=0
n[x][y][2]=0
img=Image.fromarray(n)
return img
def bits2str(self,stream):
rdic='!@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
res=0
#print(stream)
for i in stream:
res*=2
res+=i
return rdic[res]
class Solution:
def str2bits(self,key):
dic={'!': 0, '@': 1, 'a': 2, 'b': 3, 'c': 4, 'd': 5, 'e': 6, 'f': 7, 'g': 8, 'h': 9, 'i': 10, 'j': 11, 'k': 12, 'l': 13, 'm': 14, 'n': 15, 'o': 16, 'p': 17, 'q': 18, 'r': 19, 's': 20, 't': 21, 'u': 22, 'v': 23, 'w': 24, 'x': 25, 'y': 26, 'z': 27, 'A': 28, 'B': 29, 'C': 30, 'D': 31, 'E': 32, 'F': 33, 'G': 34, 'H': 35, 'I': 36, 'J': 37, 'K': 38, 'L': 39, 'M': 40, 'N': 41, 'O': 42, 'P': 43, 'Q': 44, 'R': 45, 'S': 46, 'T': 47, 'U': 48, 'V': 49, 'W': 50, 'X': 51, 'Y': 52, 'Z': 53, '0': 54, '1': 55, '2': 56, '3': 57, '4': 58, '5': 59, '6': 60, '7': 61, '8': 62, '9': 63}
res=[]
for i in key:
t=dic[i]
for j in range(6):
res.append(t&1)
t>>=1
return res
def channel_avg(self,n,x,y):
vec=[(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
r=0
g=0
b=0
for i in vec:
xx=x+i[0]
yy=y+i[1]
r+=n[xx][yy][0]
g+=n[xx][yy][1]
b+=n[xx][yy][2]
ret=(r//8,g//8,b//8)
dr=0
dg=0
db=0
for i in vec:
xx=x+i[0]
yy=y+i[1]
dr+=(n[xx][yy][0]-ret[0])**2
dg+=(n[xx][yy][1]-ret[1])**2
db+=(n[xx][yy][2]-ret[2])**2
dr+=(n[x][y][0]-ret[0])**2
dg+=(n[x][y][1]-ret[1])**2
db+=(n[x][y][2]-ret[2])**2
from math import sqrt
dr=sqrt(dr/9)
dg=sqrt(dg/9)
db=sqrt(db/9)
return ret,(dr,dg,db)
def Encrypt(self, img, key) :
key=key.ljust(10,"!")
img = Image.open(img)
m=np.asarray(img)
import random
r = random.Random(678)
stream=self.str2bits(key)
#print(stream)
#print(len(stream))
n=np.copy(m)
self.s=stream
for t in range(1):
for i in stream:
x=r.randint(0,1023)
y=r.randint(0,1023)
avg,dx=self.channel_avg(n,x,y)
#print(n[x][y][0]-avg[0],n[x][y][1]-avg[1],n[x][y][2]-avg[2])
if(i):
n[x][y][0]=255
n[x][y][1]=255
n[x][y][2]=255
else:
n[x][y][0]=0
n[x][y][1]=0
n[x][y][2]=0
img=Image.fromarray(n)
return img
def bits2str(self,stream):
rdic='!@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
res=0
#print(stream)
for i in stream:
res*=2
res+=i
return rdic[res]
def Decrypt(self,img):
img = Image.open(img)
n=np.asarray(img)
import random
r = random.Random(678)
res=[]
prob=[0]*60
for i in range(60):
x=r.randint(0,1023)
y=r.randint(0,1023)
avg,dx=self.channel_avg(n,x,y)
#print("%.2f,%.2f,%.2f,%d"%(n[x][y][0]/0x80,n[x][y][1]/0x80,n[x][y][2]/0x80,self.s[i%60]))#,dx)
res.append(int((n[x][y][0]/128+n[x][y][1]/128+n[x][y][2]/128)//3))
#print([res[i]==self.s[i] for i in range(60)])
key=''
for i in range(0,60,6):
key+=self.bits2str(res[i:i+6][::-1])
return key
Nepnep
class Solution:
def Encrypt(self, img, key):
image = Image.open(img)
pixels = image.load()
width, heidht = image.size
t = min(width, heidht)
key_bin = bin(int(key.encode().hex(), 16))[2:].zfill(len(key)*8)
print(key_bin)
for i in range(len(key_bin)):
pixel1 = list(pixels[i, 0])
if key_bin[i] == '1':
for j in range(3):
pixel1[j] = 255
else:
for j in range(3):
pixel1[j] = 0
pixels[i, 0] = tuple(pixel1)
image.convert('RGB')
return image
class Solution:
def Decrypt(self, img) -> str:
image = Image.open(img)
pixels = image.load()
width, heidht = image.size
# 解密消息从像素的最低有效位中提取
decrypted_message = ""
for i in range(10*8):
pixel1 = list(pixels[i, 0])
if sum(pixel1) > 255:
decrypted_message += '1'
else:
decrypted_message += '0'
msg = ''
for i in range(0, 80, 8):
msg += chr(int(decrypted_message[i:i+8], 2))
return msg
Summary
Actually, I had prepared another question for everyone, but due to my operation on the night of the 19th, problems arose with the platform. I apologize to all participants for this and for the poor problem-solving experience it caused. As a result, the third question did not go online as scheduled.
However, looking at the feedback forms, steglab1 received a relatively good rating. Achieving this means the problem was not in vain.
I apologize to all participants.
The platform will meet with all participants again after fixing the problems and improving the experience. We look forward to our next meeting.
If you have any suggestions or ideas for the platform, please feel free to leave a comment in the comment section, including the widely requested koh version. All these will be considered when we meet again!
暂无评论内容