본문 바로가기

2025 - 2

2주차 워게임 [Hidden] 문제풀이

문제 설명

The image looks mediocre but there seems to be something hidden

이미지는 평범해 보이지만 숨겨진 무언가가 있는 것 같습니다

 


 

스테가노그라피 같아서 stegOnline 을 사용했다. flag로 보이는 무언가가 뜨긴했지만. 해상도가 너무 낮았다.

 

문제파일에 py파일도 같이 있길래 python 으로 열어보았다. 내용은 다음과 같았다.

import zlib as z
import base64 as b
from PIL import Image as I
d=b'DH{Fake_Flags?}';k=0x55
e=bytes([x^k for x in d]);q=b.b64encode(e);c=z.compress(q)
s=(100,100);im=I.new("RGB",s);px=im.load()
bt=''.join(f"{i:08b}"for i in c);i=0
for y in range(s[1]):
 for x in range(s[0]):
  if i>=len(bt):break
  r,g,b=px[x,y]
  r=(r&0xFE)|int(bt[i]);i+=1
  g=(g&0xFE)|int(bt[i])if i<len(bt)else g;i+=1
  b=(b&0xFE)|int(bt[i])if i<len(bt)else b;i+=1
  px[x,y]=(r,g,b)
 if i>=len(bt):break
im.save("flag.png")
with open("testfile.png","rb")as f:a=f.read()
with open("flag.png","rb")as f:h=f.read()
j=a.find(b'IEND')+12
open("base.png","wb").write(a[:j]+h)

 

역시나 모르는 명령어 잔뜩이길래 지티피한테 분석 부탁했다...

 

import zlib as z           # zlib 모듈을 z로 줄여 import (데이터 압축/해제용)
import base64 as b        # base64 모듈을 b로 줄여 import (주의: 아래에서 동일 이름 b를 변수로도 사용함)
from PIL import Image as I  # Pillow의 Image를 I로 줄여 import

# --- 원본 데이터 및 키 설정 ---
d = b'DH{Fake_Flags?}'    # 숨기려는 원본 바이트(예: 플래그). b'...'는 바이트 리터럴
k = 0x55                  # XOR에 사용할 1바이트 키

# --- 간단한 '암호화'와 인코딩 / 압축 ---
e = bytes([x ^ k for x in d])  # 각 바이트에 키를 XOR -> 결과를 bytes로 만듦
q = b.b64encode(e)              # XOR 결과를 base64로 인코딩 (바이트 반환)
c = z.compress(q)               # base64 결과를 zlib로 압축

# --- 100x100 RGB 이미지 생성 및 픽셀 접근자 얻기 ---
s = (100, 100)               # 이미지 크기 (width, height)
im = I.new("RGB", s)         # 새 RGB 이미지 생성 (기본 색은 검정 (0,0,0))
px = im.load()               # 픽셀 읽기/쓰기용 매퍼

# --- 압축된 바이트를 비트열(string)로 변환 ---
bt = ''.join(f"{byte:08b}" for byte in c)  # 각 바이트를 8비트 이진 문자열로 바꿔 이어붙임
i = 0                                     # 현재 읽고 있는 비트 인덱스(전체 비트 문자열에서의 위치)

# --- LSB에 비트들을 차례대로 심기 (R -> G -> B 순) ---
for y in range(s[1]):            # 세로(높이) 루프
    for x in range(s[0]):        # 가로(너비) 루프
        if i >= len(bt):         # 모든 비트를 다 심었으면 내부 루프 탈출
            break
        r, g, b = px[x, y]       # 현재 픽의 R, G, B 값 읽음
        # 각 색 채널의 최하위 비트(LSB)를 bt에서 하나씩 꺼내어 덮어씀
        r = (r & 0xFE) | int(bt[i]); i += 1
        g = (g & 0xFE) | int(bt[i]) if i < len(bt) else g; i += 1
        b = (b & 0xFE) | int(bt[i]) if i < len(bt) else b; i += 1
        px[x, y] = (r, g, b)    # 변경한 RGB를 픽셀에 다시 저장
    if i >= len(bt):            # 내부에서 빠져나온 뒤에도 끝났으면 외부 루프 탈출
        break

im.save("flag.png")            # 스테가노 이미지를 파일로 저장

# --- 원래의 PNG(testfile.png)와 새로 만든 flag.png를 읽어서 붙이는 과정 ---
with open("testfile.png", "rb") as f:
    a = f.read()               # 원본 PNG 바이트 전부 읽기

with open("flag.png", "rb") as f:
    h = f.read()               # 방금 만든 스테가노 PNG 바이트 전부 읽기

# PNG의 IEND 청크(파일 종료 청크)를 찾아 그 청크의 끝 위치(= IEND 타입 + CRC까지) 바로 뒤로 이동
j = a.find(b'IEND') + 12      # b'IEND'는 바이트 리터럴. +12는 length(4)+type(4)+CRC(4)를 건너뛰기 위함

# 원본 PNG의 IEND 끝(=j)까지 자른 것과 flag.png 전체를 이어써서 base.png로 저장
open("base.png", "wb").write(a[:j] + h)

 

지피티가 분석해준걸 읽어보니까 숨기려는 바이트를 base64로 인코딩 후에 압축하고 그 자체를 이미지의 LSB에 스테가노로 남긴 것 같다. stegOnline 에도 half LSB 라는게 있던데, 그걸로는 안나와서 다른 사이트를 찾았다.

 

https://stylesuxx.github.io/steganography/

 

Steganography Online

Encode message To encode a message into an image, choose the image you want to use, enter your text and hit the Encode button. Save the last image, it will contain your hidden message. Remember, the more text you want to hide, the larger the image has to b

stylesuxx.github.io

 

여기에 flag.png를 넣고 나온 문자열은 다음과 같았다.

 

xœs
2(u­
ÔöuÌñ ¬ôÊÍÏC:Ë

 

이걸 base64로 디코딩 후에 제출란에 넣었더니, 정답이 아니라고 했다. 뭐가 잘못된 거지 모르겠어서 지피티꼐 물어봤다.

 

네 풀이 과정 (추정)

  1. flag.png를 온라인 스테가노그래피 사이트에 넣음
  2. “히든 메시지 추출” 기능으로 LSB 데이터를 뽑음
  3. 나온 결과를 보고 “아 이게 숨겨둔 Base64구나” → Base64 디코딩 시도
  4. 근데 이상한 값만 나오고, 정답(플래그)이 아님

뭐가 잘못된 건가?

  1. flag.png LSB 안에 들어있는 게 “그냥 Base64 문자열”이 아님
    • 실제로는: 원본 → XOR → Base64 인코딩 → zlib 압축
    • 즉, LSB에 들어간 건 압축된 바이트 스트림이야.
    • 온라인 툴은 이걸 단순히 텍스트로 가정하고 출력하기 때문에 깨져보이는 거지.
  2. 복원 순서를 지켜야 함
    • 올바른 복원 절차는:
      • LSB 추출 → 바이트 복원
      • zlib.decompress 로 압축 해제
      • base64.b64decode
      • ^ 0x55 XOR
    • 이 과정을 거쳐야만 원래 플래그(DH{...})가 나와.
  3. 온라인 툴은 단순 메시지 삽입/추출까지만 해줌
    • 추가로 적용된 압축, Base64, XOR 같은 처리는 안 해줘.
    • 그래서 단순 추출만 하면 “중간 단계 데이터”만 보이는 거야.

✅ 정리하면:
네 풀이에서 잘못된 건 “추출된 값이 바로 Base64라고 착각한 것” 이야.
실제로는 압축된 바이트라서 그냥 Base64처럼 해석하면 당연히 틀린 결과가 나와.

 

 

아하 그러니까 내가 사이트에서 뽑은 값은 zlib 압축된 데이터고, 저번주에 풀었던 EZ Des 처럼 압축해제, base64 디코딩, XOR 적용을 파이썬 코드로 작성해야하는 상황인 것 같다. 그래서 여러 코드들을 사용해 보았지만 하나 같이 다 더미 플래그만 나온다. 코드 작동은 정상적으로 되는데, 문제파일 안에 있는 flag.py 의 base.png 파일, png원본( testfile.png ) 자체가 없어서 더이상 풀이가 불가능하다고 한다..

 

gpt::

 
  • 네가 처음 보내준 것에는 flag.png 하나만 있었고, 원본 testfile.png나 base.png는 없었어.
  • 문제 코드(flag.py)의 핵심은 flag.png를 원본 PNG 끝에 붙여서 base.png를 만드는 것인데, 너는 그 과정에서 필요한 **원본 PNG(testfile.png)**가 없었기 때문에 실제 문제의 base.png를 재현할 수 없었어.
  • 그래서 내가 “문제를 풀 수 없다”라고 한 거야. 이유: 문제에서 최종적으로 숨겨진 데이터를 추출하려면, 제작자가 제공한 base.png 또는 원본 PNG + flag.png가 필요함.
  • 지금 네가 가진 것은 단순히 flag.png 예시일 뿐이고, 이 파일만으로는 문제에서 요구하는 최종 플래그를 얻을 수 없음.

즉, 정리하면:

 
문제 코드: testfile.png + flag.py → base.png 생성 → base.png에서 LSB 추출 → 플래그 복원 지금 네 상태: flag.png만 있음 → base.png 재현 불가 → 최종 플래그 추출 불가

정확히 이해하면, 지금 네가 시도했던 LSB 추출/복원 과정에서 나온 DH{Fake_Flags?}는 예시용 더미 플래그였던 거야.

 


 

WOW... 분명 드림핵에선 69명이 풀었다고 했는데 지피티는 불가능하다고 하고, 지피티가 잘못알고 있을 확률이 높지만 난 아직까진 지피티없이 완벽하게 문제를 풀 능력은 없어서... 더 풀고싶은데 제출기한은 다가오고 일단 제출하고 계속 공부하면서 더 붙잡아야겠다...