前言
本题最初的想法是希望在当今隐写藏b横行的ctf隐写环境来一场真真正正的隐写算法的挑战,选手通过使用python脚本自行编写隐写和提取脚本,通过算法来加强自己隐写算法的鲁棒性,在图像攻击下仍然能够正常提取出。因此编写了一个oj平台希望选手们能够发挥纯粹的隐写算法精神。虽然如此,但是平台仍有不足,此处在比赛期间发现的问题我们后面讲解
平台架构设计
整体架构分为前端与后端两部分,前端使用element+vite进行开发,后端采用golang+gin结合docker进行开发
整体流程如下:
选手编写encrypt.py,将水印字符写入系统后台的八张图像内,随后生成新的加密后的图像
随后执行psnr.py 判断加密后与加密前图像的相似程度,当相似度==100时则说明没有加密,返回错误
当相似度<85时,判断为对图像破坏过大,返回错误
只有相似度在100到85之间则进行加密通过,很多选手反馈在运行默认模板代码显示[0/1]就是这个原因
psnr.py代码:
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))
当然,后期为了降低难度,将psnr的范围扩大到了100-70之间
当通过psnr后则代表加密通过,后台会自动运行attack.py,对加密后的图像进行了攻击,随后保存为新的图像作为待解密图像。
此时便解锁了可以编写解密脚本的权限。
最后选手在编写解密流程,系统将会监控docker的日志输出,根据输出内容对比进行判断是否成功解密,最后返回结果。[0/8][6/8]或者解密成功
题目详解
本章将放出选手优秀脚本和内存和耗时最优排行和点评
StegLab1-PointAttack
本题共21个选手正确解出,算是一个比较基础的LSB算法的进阶使用,选手只需要熟练掌握lsb通过加key进行增强鲁棒性以及对lsb的理解就可以轻松斩杀这道题目。
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
可以看出,attack在图像的原本样貌基础上进行大规模的随机乱序的点和五条横线散布在图像上,选手只需加固自己的lsb脚本即可通过。
选手速度排行
按照加密和解密共用时间计算排名:
用户名 | 加密时间 | 解密时间 | 时间总和 |
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
x0rc3r3rs队伍的师傅算法时间复杂度最低,在此公布其编码代码
补充:该队伍师傅提交时间在psnr难度降低后,所以速度较快且代码能过,经测试,该代码在原始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
在难度降低前提交队伍中运行速度最快,交的最快的选手
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
循环,做的冗余,北邮研究生的含金量
选手内存占用排行
用户名 | 加密内存 | 解密内存 | 内存总和(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
较为预期的一种情况,通过对lsb内容进行特殊处理以加强鲁棒性,具有较大的参考价值
一血队伍:A1natas
最快的队伍师傅还是比较牛的
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 ''
该队伍师傅把代码写在一起了,影响不大,整体来讲也是针对lsb进行操作优化,运气不错,八次攻击都没污染到
StegLab2-ToJPGAttack
本题由于出题人脑袋抽风,导致提出想要开放报错信息以降低难度,结果被各位黑客各种骚操作把平台裤子都扒下来了,所以经过沟通和确认以及验证,故当前题目共五次提交,一血二血和第四血均为通过攻击平台或者使用漏洞进行获取flag,故取消这三支队伍该题目的成绩,且公布当前两解选手的题解。问题初衷是希望选手编写fft进行隐写
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")
攻击比较言简意赅,直接将文件转为jpg格式。
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
跟上一个队伍方式比较像
总结
其实本次还准备了一道题目给大家,但是由于本人在19日晚进行的操作,导致平台出现问题,在此向所有选手抱歉,给大家带来了不好的解题体验。所以导致第三题没有如期上线。
但是从反馈问卷来看,steglab1的好评率还是比较不错的。能做到这点,这题就没白出。
在此向所有选手道歉。
平台将会在修复问题以及优化体验后再次向所有选手见面,期待我们的下次相遇。
如对平台有什么建议或者想法欢迎评论区留言反馈,包括呼声较大的koh版本,都会在下次与大家再次相遇!
暂无评论内容