StegLab解答与心得记录以及问题反思

前言

本题最初的想法是希望在当今隐写藏b横行的ctf隐写环境来一场真真正正的隐写算法的挑战,选手通过使用python脚本自行编写隐写和提取脚本,通过算法来加强自己隐写算法的鲁棒性,在图像攻击下仍然能够正常提取出。因此编写了一个oj平台希望选手们能够发挥纯粹的隐写算法精神。虽然如此,但是平台仍有不足,此处在比赛期间发现的问题我们后面讲解

平台架构设计

图片[1]-StegLab解答与心得记录以及问题反思-魔法少女雪殇

整体架构分为前端与后端两部分,前端使用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脚本即可通过。

选手速度排行

按照加密和解密共用时间计算排名:

用户名加密时间解密时间时间总和
x0rc3r3rs11088214013.228
SU-Team11057224413.301
LaoGong11133218613.319
Polaris11145219613.341
天枢Dubhe11056228813.344
L3H_Sec11192216213.354
Nepnep11105228013.385
Doub1e-S11218218413.402
EDISEC11311221313.524
L11309222313.532
0psu311266228213.548
0RAYS11336224913.585
Onehalf11427226213.689
zhanyi11665227413.939
r4kapig11727230714.034
A1natas12253222914.482
LSP12102240214.504
S1uM4i11257360814.865
Vidar-Team12114307615.19
擅长可持久化线段树签到不好意思走错片场了12933233515.268
n03tAck13910250916.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)
擅长可持久化线段树签到不好意思走错片场了26075136512819231.203328
L31604736311705634.721792
SU-Team34832384158515236.417536
0psu334484224324403237.728256
EDISEC33353728713523240.48896
天枢Dubhe34918400565657640.574976
Doub1e-S34385920687718441.263104
Onehalf37953536332185641.275392
Polaris34680832759808042.278912
A1natas35782656669696042.479616
LaoGong34652160837632043.02848
0RAYS329154561121894444.1344
x0rc3r3rs34791424989593644.68736
L3H_Sec345743361021132844.785664
Vidar-Team355041281263206448.136192
zhanyi41676800659865648.275456
S1uM4i345333761739980851.933184
Nepnep349880321719500852.18304
n03tAck276316162763161655.263232
LSP365035522498150461.485056
r4kapig374743042666905664.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版本,都会在下次与大家再次相遇!

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情