https://maysokuli.tistory.com/41
[Dreamhack] Hidden (Level 1) forensics
문제 설명The image looks mediocre but there seems to be something hidden( 이미지는 평범해 보이지만 숨겨진 무언가가 있는 것 같습니다)flag format DH{...} 압축 해제하면 이렇게 2개의 파일이 있음.file.png는 그냥
maysokuli.tistory.com
해당 문제를 풀면서 LSB 변조된 이미지 파일을 다시 디코딩 하는 코드를 gemini가 짜줬었다.
그런데 LSB란 무엇이고, 그걸 어떻게 디코드 했는지 자세히 알아보자.
LSB(Least Significant Bit)란?
LSB(Least Significant Bit, 최하위 비트)는 이진수 표현에서 가중치가 가장 낮은, 가장 오른쪽에 위치한 비트를 말합니다. 0 또는 1의 값을 가지며, 해당 숫자가 짝수인지 홀수인지 결정하는 단위가 됩니다. 전체 데이터 수치에 미치는 영향이 가장 적은 비트입니다.

이진수에서 LSB는 값의 홀수/짝수만 결정할 정도로 영향력이 아주 작다!
스테가노그래피(Steganography)는 "감추어져 있다"라는 뜻의 그리스어에서 유래한 기술로, 메시지를 암호화하는 것을 넘어 메시지의 존재 자체를 숨기는 기술입니다.
그중 LSB 변조 방식은 이미지 파일의 미세한 색상 값을 아주 조금 바꿔서 데이터를 숨깁니다.
컴퓨터 이미지는 R, G, B 세가지 색상의 조합으로 표현된다.
각 색상은 0부터 255까지의 숫자로 이루어져 있음!
- 색상 값 확인: 예를 들어 어떤 픽셀의 빨간색 값이 254라고 가정. (이진수로 11111110)
- 데이터 숨기기: 내가 숨기고 싶은 비밀 비트가 '1'이라면, 이진수 끝자리(LSB)를 1로 바꿈.
- 결과: 값은 255(11111111)가 된다
사람의 눈은 색상 수치가 1 정도 차이 나는 것을 구별할 수 없다.
- 원본: RGB(254, 100, 50)
- 변조: RGB(255, 101, 50)
위 두 색상을 나란히 놓아도 인간은 똑같은 색으로 인식.
하지만 컴퓨터는 이 끝자리를 하나하나 읽어서 숨겨진 비밀 메시지(플래그)를 조립해낼 수 있는 것!!
그럼 이제 저 문제에서 Gemini가 짜준 코드를 다시 해석해봅시다.
전체 코드
import zlib as z
import base64 as b
from PIL import Image as I
try:
# 1. 이미지 로드
im = I.open("flag.png")
px = im.load()
width, height = im.size
# 2. LSB 비트 추출
bit_list = []
for y in range(height):
for x in range(width):
r, g, b_val = px[x, y]
# 각 채널의 LSB를 리스트에 추가
bit_list.append(str(r & 1))
bit_list.append(str(g & 1))
bit_list.append(str(b_val & 1))
bits = "".join(bit_list)
# 3. 비트를 바이트 객체로 변환
byte_arr = bytearray()
for i in range(0, len(bits), 8):
byte_str = bits[i:i+8]
if len(byte_str) < 8: break
byte_arr.append(int(byte_str, 2))
extracted_bytes = bytes(byte_arr)
# 4. zlib 데이터 찾기 (PNG 내부에 숨겨진 압축 데이터 시작점)
# zlib 헤더는 보통 0x78 0x9C로 시작합니다.
idx = extracted_bytes.find(b'\x78\x9c')
if idx != -1:
# 압축 해제
decompressed = z.decompress(extracted_bytes[idx:])
# Base64 디코딩
decoded_b64 = b.b64decode(decompressed)
# 5. XOR 복호화 (Key: 0x55)
k = 0x55
flag = "".join([chr(x ^ k) for x in decoded_b64])
print("-" * 30)
print("찾은 플래그: " + flag)
print("-" * 30)
else:
print("이미지에서 유효한 zlib 데이터를 찾을 수 없습니다.")
except Exception as e:
print(f"오류 발생: {e}")
1) 이미지 열기
im = I.open("flag.png")
px = im.load()
flag.png 파일을 불러온다!
2) 숨겨진 비트 긁어모으기
for y in range(height):
for x in range(width):
r, g, b_val = px[x, y]
bit_list.append(str(r & 1)) # 끝자리 비트만 추출
이미지의 모든 픽셀을 하나씩 확인해서, 가장 마지막 비트만 추출해낸다.
& 1 연산 -> 숫자가 홀수면 1, 짝수면 0을 남긴다(0과 1의 리스트를 만든다)
3) 비트를 모아 '데이터 뭉치' 만들기
for i in range(0, len(bits), 8):
byte_str = bits[i:i+8]
byte_arr.append(int(byte_str, 2))
낱개로 흩어진 0과 1 비트들을 8개씩 모아서 1바이트의 하나의 데이터 단위로 만든다.
4) 진짜 압축 데이터 찾아내기
idx = extracted_bytes.find(b'\x78\x9c')
decompressed = z.decompress(extracted_bytes[idx:])
추출한 데이터에는 우리가 원하는 정보 외에도 쓰레기 데이터가 섞여있을 수 있음.
고로 zlib 압축의 시작인 0x78 0x9C를 찾아서 그 부분부터 압축을 푼다.
5) 암호 해독(Base64, XOR)
decoded_b64 = b.b64decode(decompressed)
flag = "".join([chr(x ^ k) for x in decoded_b64])
압축을 풀고 나온 데이터를 Base64 형식으로 한 번 더 변환함.
그 변환값에 0x55를 XOR 연산한다!
그렇게 하면 최종 결과값인 DH{Supe3r_Dupe3r_H0td0g} 가 도출된다~~!
'디지털 포렌식 > 기타 추가 공부' 카테고리의 다른 글
| [인코그니토] 컨퍼런스 참가! (0) | 2026.05.11 |
|---|---|
| S3에 대한 AWS CLI 명령어를 공부해보자! (0) | 2026.05.05 |
| [AWS] Amazon ECR의 개념 및 구성 요소 (0) | 2026.04.28 |
| AES-256 암호와 그 모드들! (0) | 2026.03.30 |
| 2023 DFC 102번 풀어보자 (0) | 2026.02.07 |