StegLab QA and EXP

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

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

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:

usernameencry timedecry timetime
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

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

选手内存占用排行

teamnameencrypt memorydecrypt memorymemory(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

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!

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

昵称

取消
昵称表情