문제: DreamHack — Ransom 분류: reversing 난이도: 🥈 Silver 1 FLAG:
DH{Do_y0u_Know_Oni?}
문제 설명은 짧다. "드림이가 FLAG를 가져왔어요. 어라, FLAG가 이상하네요."
받은 건 ransom.exe 와 flag.txt 둘뿐이다. flag.txt 를 열어 보면 글자가 아니라 바이너리 쓰레기라, 저 exe 가 파일을 암호화했고 우리는 그걸 되돌려야 한다는 뜻이다.
문제 개요
| 항목 | 내용 |
|---|---|
| 문제명 | Ransom |
| 난이도 | 🥈 Silver 1 |
| 분류 | reversing |
| 제공 파일 | public.Zip → ransom.exe (PE32 i386, UPX), flag.txt (20바이트) |
| 서버 | 없음 (오프라인) |
| 핵심 | 암호화 함수가 둘이다. 하나만 뒤집으면 안 나온다 |
| 곁가지 | 고정 인덱스 연산 15개 중 14개 상쇄 · 미끼 키 2개 · 키가 파일보다 1바이트 짧음 |
🔬 정찰 — 뭘 받았나
zip 을 열어 보고 파일 종류와 암호문을 먼저 확인한다.
unzip -l extracted/public.Zip; file extracted/ransom.exe; xxd extracted/flag.txt
ransom.exe 는 32비트 PE 인데 UPX compressed 가 붙어 있고 섹션이 3개다. flag.txt 는 정확히 20바이트.
플래그가 DH{...} 형식이고 20바이트라면 중괄호 안이 16글자라는 뜻이다. 이 길이는 나중에 검산할 때 쓴다.
UPX 벗기기
UPX 는 자체 포맷이라 upx -d 한 방이면 끝난다. 손으로 덤프 뜰 일이 없다.
upx -d -o ransom_unpacked.exe ransom_packed.exe; file ransom_unpacked.exe
8,192바이트가 11,776바이트로 풀렸고 섹션이 3개에서 4개로 늘었다. 이제 정상적인 PE 라 objdump 로 읽을 수 있다.
문자열부터
풀린 바이너리의 문자열을 보면 이 문제가 뭘 할지 대충 보인다.
strings -a -n 5 ransom_unpacked.exe | sed -n "10,28p"
세 가지가 눈에 띈다.
[+] Detect it!! 이 여러 번 나온다. 분석 도구를 탐지하면 뱉는 메시지다.
키처럼 생긴 문자열이 셋이다. abc!@#qwe012fgh456, 1@!@#a134234, zBNdlwi102394. 셋 다 쓰는지, 하나만 쓰는지는 코드를 봐야 안다.
그리고 flag.txt 와 [+] Good!. 탐지에 안 걸리면 Good 을 찍고 파일을 건드린다는 얘기다.
🧭 진입점 — 함수가 몇 개나 파일을 건드리나
objdump 로 진입점부터 따라가면 진짜 main 이 나온다. 하는 일이 딱 세 줄이다.
objdump -d --no-show-raw-insn -M intel \
--start-address=0x4019a0 --stop-address=0x4019b6 ransom_unpacked.exe | tail -n +8 | sed -f annot.sed
0x401080 은 프로세스 이름을 훑어 분석 도구를 찾는 함수라 파일과는 무관하다. 문제는 그 뒤다.
0x401610 과 0x4011d0, 둘 다 flag.txt 를 연다. 암호화가 한 번이 아니라 두 번이라는 뜻이고, 이걸 놓치면 아무리 정확히 뒤집어도 결과가 안 나온다. (실제로 놓쳤다 — 아래 삽질 참고.)
🔎 1단계 — 0x401610
먼저 키로 XOR 을 건다.
objdump -d --no-show-raw-insn -M intel \
--start-address=0x4017a4 --stop-address=0x4017d7 ransom_unpacked.exe | tail -n +8 | sed -f annot.sed
mov ecx,0x10 다음 div ecx 라 나머지는 i % 16 이다. 키 문자열은 18글자인데 16개만 쓴다. 뒤의 5, 6 두 글자는 한 번도 안 들어간다. 이런 건 상수를 눈으로 세는 것보다 나눗셈 명령의 제수를 확인하는 게 확실하다.
그 다음 루프가 조금 이상하게 생겼다.
objdump -d --no-show-raw-insn -M intel \
--start-address=0x401804 --stop-address=0x4018c8 ransom_unpacked.exe | tail -n +8 \
| grep -E "add|xor|div|cmp" | sed -f annot.sed
i % 2 로 갈라서 짝수 자리엔 1, 홀수 자리엔 2를 더한다. 여기까진 평범하다.
문제는 그 아래 두 줄이다. buf[0] ^= buf[3] 과 buf[2] ^= buf[7] 인데, 인덱스가 i 가 아니라 고정 상수다. 그런데 위치는 루프 안이라 20번 다 돈다. 게다가 buf[3] 은 i == 3 회차에서 값이 바뀌므로, 회차마다 XOR 되는 값이 같지 않다.
닫힌 식으로 정리하려 들면 골치 아프다. 어차피 각 연산이 전부 가역이니 그대로 시뮬레이션하고 역순으로 되돌리면 된다.
정리하면 1단계는 이렇다.
for i in range(n):
buf[i] ^= key1[i % 16]
for i in range(n):
buf[i] += 1 if i % 2 == 0 else 2
buf[0] ^= buf[3]
buf[2] ^= buf[7]🐛 삽질
▶🐛 삽질 1 — 키가 셋인데 어느 걸 쓰는지 한참 봤다
.rdata 에 키처럼 생긴 문자열이 셋이나 있고, 1단계 함수는 그 셋을 전부 스택에 복사한다. 그래서 처음엔 세 개를 번갈아 쓰거나 이어 붙여 쓰는 줄 알았다.
디스어셈블리를 눈으로 훑는 대신 범위를 잘라 세어 봤다. 스택 오프셋 기준으로 "읽는" 명령이 몇 개인지만 보면 된다.
#!/usr/bin/env python3
"""1단계 함수(0x401610~0x40194e) 안에서 세 키 버퍼가 실제로 '읽히는지' 센다."""
import pathlib
import re
BUFS = {"key1 (abc!@#qwe012fgh456)": "-0x28",
"key2 (1@!@#a134234)": "-0x14",
"key3 (zBNdlwi102394)": "-0x38"}
LO, HI = 0x401610, 0x40194e
lines = pathlib.Path("disasm.txt").read_text().splitlines()
for name, off in BUFS.items():
reads = writes = 0
for ln in lines:
m = re.match(r"\s+([0-9a-f]+):", ln)
if not m or not (LO <= int(m.group(1), 16) <= HI):
continue
if f"{off}]" not in ln:
continue
# 'mov [ebp-off], reg' 는 쓰기, 그 외(movsx reg,[...] 등)는 읽기
if re.search(r"mov\s+(BYTE|WORD|DWORD) PTR \[ebp[^\]]*" + re.escape(off) + r"\],", ln):
writes += 1
else:
reads += 1
verdict = "실제로 쓰인다" if reads else "★ 복사만 하고 한 번도 안 읽는다 = 미끼"
print(f"{name:28} 읽기 {reads:2}회 (시작오프셋 쓰기 {writes}회) {verdict}")
읽기가 있는 건 key1 하나뿐이다. 나머지 둘은 스택에 얹히기만 하고 아무도 안 본다.
"복사되니까 쓰이겠지"라고 넘겨짚으면 안 된다는 얘기다. 쓰기와 읽기를 나눠 세면 5초면 끝난다.
▶🐛 삽질 2 — 1단계만 뒤집고 결과가 안 나와 한참 헤맸다
처음엔 0x401610 을 main 으로 착각했다. 그 안에 fopen("flag.txt") 도 있고 암호화 루프도 있고 [+] Good! 출력도 있어서, 이게 전부라고 봤다.
그래서 1단계만 역산했더니 이런 게 나왔다.
[+] 복호 결과: b'\x108S"\x03\xab1}\xe5\xef;.]2\x04>fpO}'거의 다 쓰레기인데 마지막 바이트만 } 였다. 이게 힌트였다. 마지막 자리에 대해서는 내 모델이 맞다는 뜻이고, 나머지가 틀렸다는 건 어딘가 변환이 더 있다는 뜻이다.
키 인덱스를 % 18 로 바꿔 보고, 덧셈 상수를 뒤바꿔 보고, 고정 XOR 을 빼 보고 — 조합을 다 돌려도 인쇄 가능한 문자가 20개 중 14개를 넘지 않았다.
막힌 김에 처음으로 돌아가 "flag.txt 를 여는 함수가 정말 하나뿐인가"를 확인했더니, flag.txt 문자열이 .rdata 에 두 벌 있었다. 0x4031d4 와 0x40323c. 참조 지점을 따라가니 함수가 둘이었고, 그제서야 진짜 main 인 0x4019a0 이 보였다.
문자열이 중복으로 박혀 있으면 그걸 쓰는 곳도 둘일 수 있다. 처음 찾은 참조에서 멈추면 안 된다.
💣 2단계 — 0x4011d0
2단계는 키를 .rdata 에서 읽어오지 않는다. 스택에 한 바이트씩 직접 박아 넣는다.
objdump -d --no-show-raw-insn -M intel \
--start-address=0x4011e3 --stop-address=0x401288 ransom_unpacked.exe | tail -n +8 | sed -f annot.sed
IsDebuggerPresent 를 먼저 부르고, 통과하면 mov BYTE PTR [ebp-0x420], 0x50 부터 한 바이트씩 채운다. 19개다. 파일은 20바이트인데 키는 19바이트다. 이 어긋남은 뒤에서 다시 나온다.
문자열 상수로 두지 않고 이렇게 박으면 strings 에 안 잡힌다. 아까 문자열 목록에 키가 셋만 보였던 이유이기도 하다.
이어지는 루프 몸통이 이 문제의 볼거리다.
objdump -d --no-show-raw-insn -M intel \
--start-address=0x401362 --stop-address=0x401573 ransom_unpacked.exe | tail -n +8 \
| grep -E "(add|sub|xor) e[a-d]x,0x|xor ecx,edx" | sed -f annot.sed
buf[i] ^= key2[i] 다음에 고정 인덱스 연산이 15개 줄줄이 붙는다. 상수도 제각각이라 처음 보면 복잡한 라운드 함수처럼 보인다.
그런데 짝을 맞춰 보면 이렇다.
| 앞 | 뒤 | 결과 |
|---|---|---|
buf[0] -= 0x15 | buf[0] += 0x15 | 상쇄 |
buf[3] ^= 0x18 | buf[3] ^= 0x18 | 상쇄 |
buf[8] -= 0x33 | buf[8] += 0x33 | 상쇄 |
buf[11] ^= 0x44 | buf[11] ^= 0x44 | 상쇄 |
buf[9] += 0x47 | buf[9] -= 0x47 | 상쇄 |
buf[4] ^= 0x88 | buf[4] ^= 0x88 | 상쇄 |
buf[7] ^= 0x68 | buf[7] ^= 0x68 | 상쇄 |
buf[5] += 0x11 | — | 남는다 |
14개가 서로를 지우고 buf[5] += 0x11 하나만 살아남는다. 전부 즉시값 연산이라 인덱스끼리 얽히지도 않아서, 중간에 다른 연산이 끼어도 상쇄는 그대로 성립한다.
그러니까 눈에 보이는 분량의 대부분이 장식이다. 다만 이걸 믿고 코드에서 지워 버리면 나중에 검산이 안 되니, 역산기에는 15개를 다 적어 두고 시뮬레이션으로 돌렸다.

키가 파일보다 한 바이트 짧다
키는 19바이트인데 루프는 i 가 0부터 19까지 돈다. 20번째에서 읽는 key2[19] 는 초기화한 적이 없는 스택이다.
값이 얼마인지는 코드만 봐서는 알 수 없다. 그래서 일단 0으로 두고 풀되, 나중에 0~255 를 전부 훑어 검산했다.
🚀 복호
두 단계를 그대로 구현하고, 순서를 뒤집어 되돌린다. 2단계 → 1단계 순이다.
#!/usr/bin/env python3
"""Ransom (DreamHack, Silver 1 / reversing) — flag.txt 복호
UPX 를 벗기면 진짜 main 은 0x4019a0 이고, 파일을 두 번 건드린다.
main: call 0x401080 # 프로세스 이름으로 분석도구 탐지 (파일과 무관)
call 0x401610 # 1단계
call 0x4011d0 # 2단계 (IsDebuggerPresent 먼저)
1단계 (0x401610) — key1 은 .rdata 0x4031e0 의 "abc!@#qwe012fgh456"
for i in range(n): buf[i] ^= key1[i % 16] # ★ %16 이라 뒤 두 글자 '5','6' 은 안 쓰인다
for i in range(n):
buf[i] += 1 if i % 2 == 0 else 2
buf[0] ^= buf[3] # 고정 인덱스, 매 회차 실행
buf[2] ^= buf[7]
2단계 (0x4011d0) — key2 는 스택에 한 바이트씩 박아 넣은 19바이트
for i in range(n):
buf[i] ^= key2[i] # 나머지 연산 없이 인덱스 그대로
<고정 인덱스 연산 15개> # 아래 OPS
OPS 는 15개인데 14개가 서로 역연산 짝이라 상쇄되고, 실질은 buf[5] += 0x11 하나뿐이다.
그래도 짝이 맞는지 눈으로 세는 것보다 그대로 시뮬레이션하는 쪽이 안전해서 전부 적어 뒀다.
.rdata 의 "1@!@#a134234" / "zBNdlwi102394" 는 0x401610 스택에 복사만 되고 읽히지 않는다(미끼).
key2 는 19바이트만 초기화되는데 파일은 20바이트라, 마지막 한 바이트는 초기화 안 된 스택을
읽는다. 실행 환경에서 0 이었다는 가정이 맞는지는 --scan 으로 확인한다.
python3 solve.py # extracted/flag.txt 복호
python3 solve.py --selftest # 복호본을 다시 암호화해 원본과 대조
python3 solve.py --scan # key2 의 미초기화 마지막 바이트를 0~255 전수
"""
import argparse
import pathlib
import string
import sys
KEY1 = b"abc!@#qwe012fgh456"
KEY2 = bytes([0x50, 0x70, 0x20, 0x62, 0x74, 0x58, 0x48, 0x45, 0x90, 0x90,
0x70, 0x40, 0x36, 0x45, 0x55, 0x71, 0x18, 0x19, 0x70])
# 2단계 루프 몸통의 고정 인덱스 연산 (0x401371 ~ 0x40156c 순서 그대로)
OPS = [(0, '-', 0x15), (3, '^', 0x18), (8, '-', 0x33), (11, '^', 0x44),
(9, '+', 0x47), (4, '^', 0x88), (7, '^', 0x68), (0, '+', 0x15),
(11, '^', 0x44), (9, '-', 0x47), (8, '+', 0x33), (5, '+', 0x11),
(3, '^', 0x18), (4, '^', 0x88), (7, '^', 0x68)]
HERE = pathlib.Path(__file__).resolve().parent
def _apply(b, idx, op, v, inverse=False):
if op == '^':
b[idx] ^= v # XOR 은 자기 자신이 역연산
elif (op == '+') != inverse:
b[idx] = (b[idx] + v) & 0xFF
else:
b[idx] = (b[idx] - v) & 0xFF
def stage1(b):
n = len(b)
for i in range(n):
b[i] ^= KEY1[i % 16]
for i in range(n):
b[i] = (b[i] + (1 if i % 2 == 0 else 2)) & 0xFF
b[0] ^= b[3]
b[2] ^= b[7]
def stage1_inv(b):
n = len(b)
for i in reversed(range(n)):
b[2] ^= b[7]
b[0] ^= b[3]
b[i] = (b[i] - (1 if i % 2 == 0 else 2)) & 0xFF
for i in range(n):
b[i] ^= KEY1[i % 16]
def stage2(b, tail=0x00):
key = KEY2 + bytes([tail]) * (len(b) - len(KEY2))
for i in range(len(b)):
b[i] ^= key[i]
for idx, op, v in OPS:
_apply(b, idx, op, v)
def stage2_inv(b, tail=0x00):
key = KEY2 + bytes([tail]) * (len(b) - len(KEY2))
for i in reversed(range(len(b))):
for idx, op, v in reversed(OPS):
_apply(b, idx, op, v, inverse=True)
b[i] ^= key[i]
def encrypt(data: bytes, tail=0x00) -> bytes:
b = bytearray(data)
stage1(b)
stage2(b, tail)
return bytes(b)
def decrypt(data: bytes, tail=0x00) -> bytes:
b = bytearray(data)
stage2_inv(b, tail)
stage1_inv(b)
return bytes(b)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("path", nargs="?", default=str(HERE / "extracted" / "flag.txt"))
ap.add_argument("--selftest", action="store_true")
ap.add_argument("--scan", action="store_true",
help="key2 의 미초기화 마지막 바이트를 0~255 전수해 후보를 본다")
a = ap.parse_args()
ct = pathlib.Path(a.path).read_bytes()
print(f"[*] 암호문 {len(ct)}바이트: {ct.hex()}")
if a.scan:
ok = set(string.printable[:-5].encode())
hits = [(t, decrypt(ct, t)) for t in range(256)]
hits = [(t, p) for t, p in hits if all(c in ok for c in p)]
for t, pt in hits[:4]:
print(f" tail=0x{t:02x} → {pt.decode()}")
closed = [t for t, p in hits if p.endswith(b"}")]
print(f" ... 인쇄가능 후보 {len(hits)}개")
print(f" 그중 '}}' 로 끝나는 것: {['0x%02x' % t for t in closed]}")
return
pt = decrypt(ct)
print(f"[+] 복호 결과: {pt!r}")
try:
print(f"[+] FLAG: {pt.decode()}")
except UnicodeDecodeError:
print("[-] ASCII 로 안 떨어진다 — --scan 으로 마지막 키 바이트를 훑어 볼 것")
sys.exit(1)
if a.selftest:
again = encrypt(pt)
ok = again == ct
print(f"[{'+' if ok else '-'}] 재암호화 대조: {again.hex()} {'일치' if ok else '불일치'}")
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()
--selftest 는 복호한 평문을 다시 암호화해서 배포본 20바이트와 대조한다. 복호만 하고 "글자로 읽히니 맞겠지" 하고 넘어가면, 자리 하나가 우연히 맞은 건지 알 수가 없다.
[+] FLAG: DH{Do_y0u_Know_Oni?}
[+] 재암호화 대조: 745c3705448a410c81e10b1e3c576d0c08142d5e 일치🧪 바이너리로 확인 — wine 실행
PE 라 gdb 로 런타임을 찍기가 마땅치 않다. 대신 바이너리를 실제로 돌려서 확인할 수 있다. 복호로 얻은 평문을 flag.txt 에 넣고 ransom.exe 를 wine 으로 실행하면, 배포본과 같은 암호문이 나와야 한다.
#!/usr/bin/env bash
# 복호 결과가 맞는지 바이너리로 직접 확인한다.
# 평문(복호 결과)을 flag.txt 에 넣고 ransom.exe 를 실제로 돌려서,
# 배포본 flag.txt 와 바이트가 같아지는지 본다.
set -eu
cd "$(dirname "$(readlink -f "$0")")"
PLAIN=${1:-'DH{Do_y0u_Know_Oni?}'}
WORK=wine_run
rm -rf "$WORK"; mkdir -p "$WORK"
cp extracted/ransom.exe "$WORK/"
printf '%s' "$PLAIN" > "$WORK/flag.txt"
echo "[*] 실행 전 flag.txt (복호로 얻은 평문)"
xxd "$WORK/flag.txt"
export WINEPREFIX="$HOME/.wine" WINEDEBUG=-all
( cd "$WORK" && wine ransom.exe ) 2>&1 | grep -vE '^wine:|XDG_RUNTIME_DIR' || true
echo
echo "[*] 실행 후 flag.txt (ransom.exe 가 다시 암호화한 결과)"
xxd "$WORK/flag.txt"
echo "[*] 배포본 flag.txt"
xxd extracted/flag.txt
python3 - "$WORK/flag.txt" extracted/flag.txt <<'PY'
import sys, pathlib
got = pathlib.Path(sys.argv[1]).read_bytes()
want = pathlib.Path(sys.argv[2]).read_bytes()
head_ok = got[:19] == want[:19]
print()
print(f"앞 19바이트 : {'일치' if head_ok else '불일치'}")
print(f"20번째 : 이번 실행 0x{got[19]:02x} / 배포본 0x{want[19]:02x}"
f" → 미초기화 키 바이트 차이 0x{got[19] ^ want[19]:02x}")
if not head_ok:
sys.exit(1)
print()
print("✅ 이 평문이 배포본 암호문을 만든다 (마지막 1바이트는 환경 의존이라 제외)")
PY
[+] Good! 이 찍히고, 나온 암호문이 배포본과 19바이트까지 똑같다. 마지막 한 바이트만 다르다.
이게 바로 아까 그 초기화 안 된 키 바이트다. 같은 바이너리라도 돌리는 환경에 따라 그 자리에 들어오는 스택 쓰레기가 달라지니, 마지막 바이트도 따라 달라진다. 출제자 PC 에서는 0 이었고, 이 wine 환경에서는 0x7b 였다(두 결과의 XOR 차이가 그대로 그 값이다).
실패가 아니라 오히려 분석이 맞다는 증거다. 실제로 solve.py 의 encrypt 에 tail=0x7b 를 넣으면 wine 이 만든 암호문이 그대로 재현된다.
마지막 바이트를 전수로 확인
값을 0 이라고 가정한 게 우연히 맞은 건 아닌지, 0~255 를 전부 넣어 봤다.

인쇄 가능한 문자열이 나오는 후보가 95개나 된다. 마지막 한 글자만 바뀌니 당연하다.
하지만 플래그가 } 로 닫혀야 한다는 조건을 걸면 0x00 하나만 남는다. 형식이 검산 도구가 되는 흔한 경우다.
재현
배포본만 있으면 바로 돌려볼 수 있게 한 방 스크립트를 남겼다. 서버가 필요 없는 문제라 이것만으로 진짜 플래그가 나온다.
#!/usr/bin/env bash
# Ransom (DreamHack, Silver 1) — 한 방 재현
# ./reproduce.sh 복호 → flag 대조 (인자 없이 그냥 돌리면 된다)
# ./reproduce.sh --wine 추가로 ransom.exe 를 wine 으로 실제 실행해 암호문 재현까지
set -eu
cd "$(dirname "$(readlink -f "$0")")"
EXPECT='DH{Do_y0u_Know_Oni?}'
# 배포본이 zip 안에 있으면 먼저 푼다
[ -f extracted/flag.txt ] || ( cd extracted && unzip -o -q public.Zip )
OUT=$(timeout 120 python3 solve.py --selftest 2>&1) || true
echo "$OUT" | tail -6
FLAG=$(printf '%s' "$OUT" | grep -aoE 'DH\{[^}]+\}' | head -1)
if [ "$FLAG" != "$EXPECT" ]; then
echo; echo "❌ FAIL (얻은 값: '${FLAG:-없음}' / 기대: '$EXPECT')"; exit 1
fi
if ! printf '%s' "$OUT" | grep -q '재암호화 대조.*일치'; then
echo; echo "❌ FAIL — 복호는 됐는데 재암호화가 원본과 안 맞는다"; exit 1
fi
if [ "${1:-}" = "--wine" ]; then
echo; echo "=== wine 실행 검증 ==="
./run_wine.sh "$EXPECT" | tail -6
fi
echo; echo "✅ PASS $FLAG"
📝 결론
암호화 함수가 하나라고 가정하지 않는다
이 문제에서 제일 오래 잡아먹은 건 알고리즘이 아니라 "함수가 둘"이라는 사실이었다. 파일을 여는 코드를 찾았다고 거기서 멈추면, 그 뒤에 붙은 두 번째 변환을 통째로 놓친다.
같은 문자열이 .rdata 에 두 벌 박혀 있으면 그걸 쓰는 곳도 둘일 수 있다. 참조를 전부 훑고 진입점부터 호출 순서를 확인하는 게 결국 빠르다.
분량과 실질은 다르다
2단계의 고정 연산 15개는 화면을 꽉 채우지만 실제로 남는 건 하나다. 상수가 제각각이라 복잡해 보일 뿐, 인덱스별로 묶으면 대부분이 자기를 지운다.
반대로 1단계의 두 줄짜리 buf[0] ^= buf[3] 은 짧은데 훨씬 성가시다. 루프 안에 있어서 20번 돌고, 참조하는 값이 도중에 바뀐다. 길이로 난이도를 재면 반대로 짚는다.
복호가 읽힌다고 끝내지 않는다
DH{ 로 시작하는 문자열이 나오면 거기서 손을 놓기 쉽다. 하지만 그건 앞자리 몇 개가 맞았다는 뜻일 뿐이다. 복호본을 다시 암호화해 원본 바이트와 대조하면 전 자리가 한꺼번에 검산된다.
이번엔 그 검산 덕분에 마지막 바이트가 환경 의존이라는 것까지 드러났다.
초기화 안 된 메모리를 읽는 코드는 결과가 기계마다 다르다
키 배열을 19개만 채워 놓고 20번 읽는 건 명백한 버그다. 문제 파일이 만들어진 PC 에서는 그 자리가 0이었지만, wine 에서 돌리면 다른 값이 나온다.
암호화 도구가 이러면 복호가 원본 기계에서만 되는 일이 생긴다. 랜섬웨어를 흉내 낸 문제에 이 버그가 들어 있는 게 공교롭다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…