๋ฌธ์ : DreamHack โ dreamthereum ๋ถ๋ฅ: reversing (์ค๋งํธ์ปจํธ๋ํธ) ๋์ด๋: ๐ Platinum 4 FLAG:
DH{ABCB69B964FA423A7BCB69B964FA423}
๋ฌธ์ ์ค๋ช
์ ์งง๋ค. "๋๋ฆผ๋๋ฆฌ์์ ์ธ์์์๋ ์ค๋งํธ์ปจํธ๋ํธ ๋ฐฐํฌ ์ฌ์ด์ฆ ์ ํ๋ ์คํํ gas limit๋ ์๋ค"๋ฉด์, ๋ฐฐํฌ๋ ์ปจํธ๋ํธ์ flagChecker ๋ฉ์๋๊ฐ true๋ฅผ ๋๋ ค์ฃผ๋ ์
๋ ฅ์ ์ฐพ์ผ๋ผ๊ณ ํ๋ค. ์ฐธ๊ณ ๋งํฌ๋ก 4byte.directory๋ฅผ ๊ฑธ์ด ๋จ๋ค. ํจ์ ์
๋ ํฐ ์ด์ผ๊ธฐ๋ผ๋ ๋ป์ด๋ค.
๋ฐ์ ํ์ผ์ challenge.sol ํ๋. 1099์ค์ด๋ค.
file challenge.sol; wc -l challenge.sol; grep -c "function dummy" challenge.sol; grep -oE "dummy[0-9A-F]\(" challenge.sol | sort -u | tr "\n" " "; echo
function dummy๊ฐ 256๊ฐ. ์ด๋ฆ์ dummy0~dummyF 16๊ฐ๋ฟ์ธ๋ฐ ์ ์ธ์ด 256๊ฐ๋ผ๋ ๊ฑด, ์ด๋ฆ ํ๋๋น 16๊ฐ์ฉ ์ค๋ฒ๋ก๋๋ผ ์๋ค๋ ๋ป์ด๋ค. ๋ฐฐํฌ ์ฌ์ด์ฆ ์ ํ์ด ์๋ค๋ ์ค๋ช
์ด ์ฌ๊ธฐ์ ๊ฑธ๋ฆฐ๋ค โ ์ค์ ์ด๋๋ฆฌ์์ด๋ผ๋ฉด EIP-170์ 24KB ์ฝ๋ ์ ํ์ ๊ฑธ๋ ค ๋ฐฐํฌ ์์ฒด๊ฐ ์ ๋๋ค.
๋ฌธ์ ๊ฐ์
| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ๋ฌธ์ ๋ช | dreamthereum |
| ๋์ด๋ | ๐ Platinum 4 |
| ๋ถ๋ฅ | reversing (EVM / Solidity) |
| ์ ๊ณต ํ์ผ | challenge.sol (1099์ค, ์๋ฒ ์์) |
| ์คํ | Solidity >=0.7.0 <0.9.0, EVM |
| ํต์ฌ ๊ธฐ๋ฒ | ํจ์ ์ ๋ ํฐ(keccak256 ์ 4๋ฐ์ดํธ)๋ฅผ ๊ฐ์ผ๋ก ์ฐ๋ ์ํ ์ ์ด + ์ฌ์ฌ ์ ์ฝ์ผ๋ก ํ์ ๊ณต๊ฐ์ 256์ผ๋ก ์ถ์ |
ํ์ด ํ๋ฆ์ ์ด๋ ๋ค. 256๊ฐ ๋๋ฏธ ํจ์๋ ์ ๋ถ TABLE2[uint32(msg.sig) % TABLE2.length]๋ง ๋ฐํํ๋ ๊ป๋ฐ๊ธฐ๋ค. flagChecker๋ ์
๋ ฅ 31๋ฐ์ดํธ๋ฅผ ํ์ผ๋ฉด์ internalCall(flag[i], flag[i+1])์ ๊ฒฐ๊ณผ๊ฐ flag[i+2]์ ๊ฐ์์ง ๊ณ์ ํ์ธํ๋ค. ์ฆ ์ ๋ ๊ธ์๋ฅผ ๊ณ ๋ฅด๋ฉด ๋๋จธ์ง 29๊ธ์๋ ๊ณ์ฐ์ผ๋ก ์ ํด์ง๋ค. ํ๋ณด๋ 16ร16 = 256๊ฐ๋ฟ์ด๊ณ , ๋ง์ง๋ง์ ๊ฑธ๋ฆฐ keccak256 ๋น๊ต๊ฐ ๊ทธ์ค ํ๋๋ง ๋จ๊ธด๋ค.
๐งฉ ๋ฐฐ๊ฒฝ โ EVM ํจ์ ์ ๋ ํฐ
์ด๋๋ฆฌ์์์ ์ปจํธ๋ํธ๋ฅผ ํธ์ถํ ๋ calldata ๋งจ ์ 4๋ฐ์ดํธ๊ฐ ํจ์ ์ ๋ ํฐ๋ค. ๊ฐ์ ํจ์ ์๊ทธ๋์ฒ ๋ฌธ์์ด์ keccak256 ํด์ ์ 4๋ฐ์ดํธ๋ค.
selector = keccak256("transfer(address,uint256)")[0:4] = 0xa9059cbb์๊ทธ๋์ฒ์ ๋ค์ด๊ฐ๋ ๊ฑด ํจ์ ์ด๋ฆ๊ณผ ์ธ์ ํ์
๋ฟ์ด๋ค. ์ธ์ ์ด๋ฆ๋, ๋ฐํ ํ์
๋, view ๊ฐ์ ์์์๋ ๋ค์ด๊ฐ์ง ์๋๋ค. ๊ทธ๋์ ๊ฐ์ ์ด๋ฆ์ ํ์
๋ง ๋ฐ๊ฟ ์ค๋ฒ๋ก๋ํ๋ฉด ์
๋ ํฐ๊ฐ ์ ๋ถ ๋ฌ๋ผ์ง๋ค. 4byte.directory๋ ์ด 4๋ฐ์ดํธ๋ฅผ ์๋ ์๊ทธ๋์ฒ๋ก ๋๋๋ ค ๋ณด๋ ๊ณต๊ฐ DB๋ค.
์ด ๋ฌธ์ ๋ ๊ทธ ์ ๋ ํฐ๋ฅผ "์ฃผ์"๊ฐ ์๋๋ผ ๊ฐ์ผ๋ก ์ด๋ค. ๋๋ฏธ ํจ์๊ฐ ํ๋ ์ผ์ ์๊ธฐ๋ฅผ ๋ถ๋ฅธ ์ ๋ ํฐ๋ฅผ 32๋นํธ ์ ์๋ก ์ฝ์ด 16์ผ๋ก ๋๋ ๋๋จธ์ง๋ฅผ ํ ์ด๋ธ ์ธ๋ฑ์ค๋ก ์ฐ๋ ๊ฒ๋ฟ์ด๋ค.

๐ฌ ์ฝ๋ ์ ์ฐฐ โ 1099์ค ์ค 1024์ค์ด ๋ฏธ๋ผ
๋๋ฏธ ์ ์ธ์ ๋นผ๊ณ ๋๋ฉด ๋จ๋ ๊ฒ ๋ช ์ค ์ ๋๋ค. function ์ ์ธ๊ณผ ๋ ํ
์ด๋ธ, ๊ทธ๋ฆฌ๊ณ ๋ง์ง๋ง ๋น๊ต์๋ง ๋ฝ์ ๋ดค๋ค.
grep -n "function \|TABLE1 =\|TABLE2 =\|keccak256" challenge.sol | grep -v dummy
์ง์ง ์ฝ๋๋ 40์ค ๋จ์ง์ด๋ค. ๋ ํ ์ด๋ธ๋ถํฐ ๋ณด์.
string[] TABLE1 = ["uint256", "bytes", "bytes4", "bytes1", "bytes32", "uint8",
"uint16", "uint248", "string", "uint240", "uint224", "uint192",
"uint24", "uint32", "uint56", "uint72"];
uint8[] TABLE2 = [0x41,0x42,0x43,0x44,0x45,0x46,0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39];TABLE1์ 16๊ฐ ํ์
์ด๋ฆ, TABLE2๋ 'A''F' ๋ค์์ '0''9'๊ฐ ์ค๋ 16๊ฐ ๋ฌธ์๋ค. ๋ ๋ค 16์ง ๋ฌธ์ ํ๋๋ก ๊ท๊ฒฐ๋๋ค๋ ๊ฒ ํฌ์ธํธ๋ค. ์์์ ์ฃผ์ํด์ผ ํ๋ค. TABLE2[0]์ '0'์ด ์๋๋ผ 'A'๋ค.
๋๋ฏธ ํจ์ 256๊ฐ๋ ์ ๋ถ ๊ฐ์ ๋ชธํต์ ๊ฐ๋๋ค.
function dummyA(uint192) external view returns (uint8) {
return TABLE2[uint32(msg.sig) % TABLE2.length];
}์ธ์๋ ์ด๋ฆ์กฐ์ฐจ ์๋ค. ๊ฐ์ ์ฐ์ง ์๊ธฐ ๋๋ฌธ์ด๋ค. ์ด ํจ์๊ฐ ๋ฐํํ๋ ๊ฐ์ ์ด๋ค ์ ๋ ํฐ๋ก ๋ถ๋ ธ๋๊ฐ์๋ง ์์กดํ๋ค.
ํต์ฌ ์ธ ํจ์
function getArgsFromB(uint8 x) internal view returns (string memory) {
require(x >= 0x41 && x < 0x41+6 || x >= 0x30 && x <= 0x39, "BOOOOM!");
if (x>=0x41)
return TABLE1[x-0x41 + 0xa];
return TABLE1[x-0x30];
}'0'9๋ก, '9'๋ 0'A'15๋ก ์ ํ๋ค. 16์ง ๋ฌธ์๋ฅผ ๊ทธ ๊ฐ์ผ๋ก ์ฝ๋ ๊ฒ๊ณผ ์ ํํ ๊ฐ๋ค. ๋ฒ์๋ฅผ ๋ฒ์ด๋๋ฉด 'F'๋ 10BOOOOM!์ผ๋ก revertํ๋ค.
function internalCall(uint8 A, uint8 B) internal onlyOwner returns (uint8) {
string memory callMethod = "dummy";
callMethod = concatenate(callMethod, A); // ์ด๋ฆ ํ ๊ธ์
callMethod = concatenate(callMethod, "(");
callMethod = concatenate(callMethod, getArgsFromB(B)); // ์ธ์ ํ์
callMethod = concatenate(callMethod, ")");
(bool success, bytes memory returnData) = address(this).call(abi.encodeWithSignature(callMethod, 0x0));
require(success, "Bamb!");
return uint8(sliceUint(returnData, 0));
}๋ฌธ์์ด๋ก ์๊ทธ๋์ฒ๋ฅผ ์กฐ๋ฆฝํด abi.encodeWithSignature๋ก ๋๊ธด๋ค. ์ด๋ ๊ณ์ฐ๋๋ ์
๋ ํฐ๊ฐ ๊ณง ๊ทธ ๋๋ฏธ ํจ์์ msg.sig๊ฐ ๋๊ณ , ๋๋ฏธ๋ ๊ทธ๊ฑธ 16์ผ๋ก ๋๋ ๋๋จธ์ง๋ก TABLE2๋ฅผ ์ฐ์ด ๋๋ ค์ค๋ค. ๊ทธ๋ฌ๋๊น internalCall(A, B)๋ ๋ฌธ์ ๋ ๊ฐ๋ฅผ ๋ฐ์ ๋ฌธ์ ํ๋๋ฅผ ๋ฑ๋ ํจ์๋ค. ์ปจํธ๋ํธ ์์์ ๋ฒ์ด์ง๋ ์๊ธฐ ํธ์ถ์ด์ง๋ง, ๊ฒฐ๊ณผ๋ฅผ ๊ฒฐ์ ํ๋ ๊ฑด keccak256๋ฟ์ด๋ผ ์คํ๋ผ์ธ์์ ๊ทธ๋๋ก ๊ณ์ฐํ ์ ์๋ค.
function flagChecker(bytes calldata flag) external returns (bool) {
uint8 ret;
bytes memory output = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; // output.length == 31
for(uint i=0; i<output.length - 2; i++) {
ret = internalCall(uint8(flag[i]), uint8(flag[i+1]));
output[i] = flag[i];
output[i+1] = flag[i+1];
output[i+2] = bytes1(ret);
require(ret == uint8(flag[i+2]), "BOBOBOBO!!!");
}
// https://emn178.github.io/online-tools/keccak_256.html
return keccak256(abi.encodePacked("DH{", output, "}")) == bytes32(0xe6a9e5bcd806b56f7ab007dbb1e881f5b5746f00cb940b8083be861a257f7806);
}output์ 31๋ฐ์ดํธ๊ณ ๋ฃจํ๋ i = 0..28์ ๋๋ค. ๋งค ํ flag[i+2]๋ฅผ ์ฝ์ผ๋ฏ๋ก ์
๋ ฅ์ ์ต์ 31๋ฐ์ดํธ์ฌ์ผ ํ๊ณ , ๋ฃจํ๊ฐ ๋๋๋ฉด output์ flag์ ์ 31๋ฐ์ดํธ ๊ทธ ์์ฒด๊ฐ ๋๋ค.
๐ฃ ํต์ฌ โ ์ ๋ ํฐ๊ฐ ๊ณง ์ ์ดํจ์๋ค
internalCall์ ํ์ด์ฌ์ผ๋ก ์ฎ๊ธฐ๋ฉด ์ด๋ ๊ฒ ๋๋ค.
f(A, B) = TABLE2[ uint32(keccak256("dummy" + A + "(" + TABLE1[hex(B)] + ")")[:4]) % 16 ]์ฌ๊ธฐ์ ๋ด ๊ณ์ฐ์ด ๋ง๋์ง๋ถํฐ ํ์ธํด์ผ ํ๋ค. ์
๋ ํฐ๋ ์ง์ keccak์ ๋๋ ค๋ ๋์ง๋ง, foundry์ cast๊ฐ ๊ฐ์ ์ผ์ ํด ์ฃผ๋ ๊ต์ฐจ๊ฒ์ฆ์ผ๋ก ์ฐ๊ธฐ ์ข๋ค. cast keccak์ผ๋ก ์ต์ข
๋น๊ต ๋์๊น์ง ํ ๋ฒ์ ํ์ธํ๋ค.
for s in "dummyA(uint192)" "dummy0(uint72)" "dummyD(bytes1)"; do printf "%-18s -> " "$s"; docker run --rm -e FOUNDRY_DISABLE_NIGHTLY_WARNING=1 --entrypoint cast ghcr.io/foundry-rs/foundry:latest sig "$s"; done; docker run --rm -e FOUNDRY_DISABLE_NIGHTLY_WARNING=1 --entrypoint cast ghcr.io/foundry-rs/foundry:latest keccak "DH{ABCB69B964FA423A7BCB69B964FA423}"
์
๋ค ํ์ด์ฌ์ผ๋ก ๊ณ์ฐํ ๊ฐ๊ณผ ๊ฐ๊ณ , cast keccak์ด ๋ฑ์ ํด์๋ ์์ค 1083ํ์ ์์์ ์ ํํ ์ผ์นํ๋ค.
์ปดํ์ผ๋ฌ๊ฐ ์ค์ ๋ก ๋ง๋ ABI์์๋ ๊ฐ์ ๊ฐ์ด ๋์ค๋์ง ํ์ธํ๋ค. forge inspect๋ก ๋ฉ์๋ ํ๋ฅผ ๋ฝ์ผ๋ฉด ์ปจํธ๋ํธ๊ฐ ๊ฐ์ง ์
๋ ํฐ๊ฐ ์ ๋ถ ๋์จ๋ค.
docker run --rm -e FOUNDRY_DISABLE_NIGHTLY_WARNING=1 -v "$PWD":/w -w /w --entrypoint forge ghcr.io/foundry-rs/foundry:latest inspect src/Challenge.sol:Challenge methods | tee methods.txt | grep -E "dummyA\(uint192\)|dummy0\(uint72\)|dummyD\(bytes1\)|flagChecker"; grep -c "| dummy" methods.txt
dummy ๋ฉ์๋๊ฐ 256๊ฐ, ์
๋ ํฐ๋ ๊ทธ๋๋ก๋ค. ์ฌ๊ธฐ๊น์ง ์ค๋ฉด f๋ ์์ํ ํ ํ๋๋ก ํ์ ๋๋ค.
๐ฏ ์ฌ์ฌ ๊ตฌ์กฐ โ ์ ๋ ๊ธ์๊ฐ ์ ๋ถ๋ค
flagChecker์ require๋ฅผ ๋ค์ ๋ณด์.
require(ret == uint8(flag[i+2]), "BOBOBOBO!!!");ret์ f(flag[i], flag[i+1])์ด๋ค. ๊ทธ๋ฌ๋ ์ด ์กฐ๊ฑด์ ๊ณง flag[i+2] = f(flag[i], flag[i+1])์ด๋ผ๋ ์ ํ์์ด๋ค. i๊ฐ 0๋ถํฐ 28๊น์ง ๋๋๊น flag[2]๋ถํฐ flag[30]๊น์ง 29๊ธ์๊ฐ ์ ๋ ๊ธ์์์ ์ฐ์์ ์ผ๋ก ๊ฒฐ์ ๋๋ค.
![ํด๋ฆญํ์ฌ ํ๋ flagChecker์ ์ฌ์ฌ ๊ตฌ์กฐ ๋์ โ ํญ 2์ ์ฐฝ์ด ํ ์นธ์ฉ ๋ฏธ๋๋ฌ์ง๋ฉฐ f(flag[i], flag[i+1])์ด flag[i+2]๋ฅผ ํ์ ํ๊ณ , ์์ ๋ก์ด ๊ฑด ๋งจ ์ ๋ ๊ธ์๋ฟ์ด๋ผ ํ๋ณด๊ฐ 16ร16 = 256๊ฐ๋ก ์ค์ด๋ ๋ค](/_next/image?url=%2Fimages%2Fblog%2Fdreamhack-dreamthereum-writeup%2F06_diag_chain.png&w=3840&q=75)
๊ฒ๋ค๊ฐ ๋ ๊ธ์ ๋ชจ๋ 16์ง ๋ฌธ์์ฌ์ผ ํ๋ค. B ์๋ฆฌ๋ getArgsFromB๊ฐ BOOOOM!์ผ๋ก ๋ง๊ณ , A ์๋ฆฌ๋ ์กด์ฌํ์ง ์๋ ์ด๋ฆ์ด๋ฉด call์ด ์คํจํด Bamb!๋ก ๋งํ๋ค. ๊ทธ๋ฆฌ๊ณ f์ ์ถ๋ ฅ์ TABLE2 ์์๋ผ ์ญ์ 16์ง ๋ฌธ์๋ค. ๊ฒฐ๊ตญ 31๊ธ์ ์ ๋ถ๊ฐ 0-9A-F๋ค.
์ ์ดํ๋ฅผ ์ค์ ๋ก ๋ฝ์ ๋ดค๋ค. ํ์ด ํจ์ ์ด๋ฆ A, ์ด์ด ์ธ์ ํ์
B๋ค.
#!/usr/bin/env python3
"""internalCall(A,B) ์ ์ดํ๋ฅผ 16x16 ์ผ๋ก ์ถ๋ ฅํ๋ค. solve.py ์ ๊ฐ์ ๋ก์ง."""
from solve import ALPHABET, TABLE1, TABLE2, hexval, selector
print(" ์์ ์๊ทธ๋์ฒ โ ์
๋ ํฐ โ %16 โ ๋ฐํ๋ฌธ์")
for a, b in (("A", "B"), ("0", "F"), ("D", "3")):
sig = "dummy%s(%s)" % (a, TABLE1[hexval(b)])
sel = selector(sig)
print(" %-22s 0x%08x %%16=%2d -> '%s'"
% (sig, sel, sel % 16, chr(TABLE2[sel % 16])))
print()
print(" f(A,B) ์ ์ดํ (ํ=A: ํจ์์ด๋ฆ, ์ด=B: ์ธ์ํ์
)")
print(" " + " ".join(ALPHABET))
for a in ALPHABET:
row = []
for b in ALPHABET:
sig = "dummy%s(%s)" % (a, TABLE1[hexval(b)])
row.append(chr(TABLE2[selector(sig) % 16]))
print(" %s %s" % (a, " ".join(row)))python3 table.py![ํด๋ฆญํ์ฌ ํ๋ table.py ์คํ ๊ฒฐ๊ณผ โ dummyA(uint192)๊ฐ 0xab128832๋ผ์ %16=2์ด๊ณ TABLE2[2]='C'๊ฐ ๋๋ ์์ ์ธ ์ค๊ณผ, 16ร16 ์ ์ดํ ์ ์ฒด๊ฐ ์ถ๋ ฅ๋๋ค. ํ์ ๋ชจ๋ ์นธ์ด 0-9A-F ๋ฌธ์ ํ๋๋ค](/_next/image?url=%2Fimages%2Fblog%2Fdreamhack-dreamthereum-writeup%2F07_table.png&w=3840&q=75)
(A, B) = ('A', 'B')๋ฉด 'C'. ์ค์ flag๊ฐ ABC๋ก ์์ํ๋ ์ด์ ๊ฐ ์ด ํ ์นธ์ ๋ค์ด ์๋ค.
โถ๐ ํจ์ โ ์ฝ๊ธฐ ์ฌ์ด ์ค๋ ์ธ ๊ฐ์ง, ์ฆ์์ ์ ๋ถ 'ํ๋ณด 0๊ฐ'
์ด ๋ฌธ์ ๋ ์ค๊ฐ ํผ๋๋ฐฑ์ด ์๋ค. require๊ฐ and ๋์ฐ๋ ์๋๊ณ ๋ถ๋ถ ์ ์๋ ์์ด์, ๋ชจ๋ธ์ ํ๋ฆฌ๊ฒ ์ธ์ฐ๋ฉด 256๊ฐ๋ฅผ ๋ค ๋๋ ค๋ ์๋ฌด๊ฒ๋ ์ ๊ฑธ๋ฆฐ๋ค. ๋ฐ๋๋ก ๋งํ๋ฉด ํ๋ณด๊ฐ ์ ํํ 1๊ฐ ๋์ค๋์ง๊ฐ ๋ชจ๋ธ ๊ฒ์ฆ์ด๋ค. ํท๊ฐ๋ฆฌ๊ธฐ ์ฌ์ด ์ธ ์ง์ ์ ์ผ๋ถ๋ฌ ํ๋ฆฌ๊ฒ ๊ตฌํํด ๋๋ ค ๋ดค๋ค.
#!/usr/bin/env python3
"""์ฝ๊ธฐ ์ฌ์ด ์ค๋
์ธ ๊ฐ์ง๋ฅผ ์ค์ ๋ก ๋๋ ค ๋ณธ๋ค. ์ ๋ถ ํ๋ณด 0๊ฐ๊ฐ ๋์จ๋ค.
A) TABLE2 ๋ฅผ '์์ฐ์ค๋ฌ์ด' 16์ง ์์ '0'..'9','A'..'F' ๋ก ์ฝ๋ ๊ฒฝ์ฐ
B) ์
๋ ํฐ๋ฅผ ๋ฆฌํ์๋์ธ uint32 ๋ก ์ฝ๋ ๊ฒฝ์ฐ
C) ๊ฒ์ฌ ๋์ ๋ฌธ์์ด ๊ธธ์ด๋ฅผ 31 ์ด ์๋๋ผ 32 ๋ก ์ก๋ ๊ฒฝ์ฐ
"""
from solve import ALPHABET, TABLE1, TABLE2, TARGET, hexval, k256, selector
NATURAL = [ord(c) for c in "0123456789ABCDEF"] # ์ค๋
A ์ฉ
def count_hits(table2, endian="big", length=31):
tbl = {}
for a in ALPHABET:
for b in ALPHABET:
sig = "dummy%s(%s)" % (a, TABLE1[hexval(b)])
raw = k256(sig.encode())[:4]
sel = int.from_bytes(raw, endian)
tbl[(a, b)] = chr(table2[sel % 16])
hits = []
for c0 in ALPHABET:
for c1 in ALPHABET:
s = [c0, c1]
while len(s) < length:
s.append(tbl[(s[-2], s[-1])])
out = "".join(s)
if int.from_bytes(k256(b"DH{" + out.encode() + b"}"), "big") == TARGET:
hits.append(out)
return hits
for label, kw in (
("A) TABLE2 ๋ฅผ '0'..'9','A'..'F' ์์๋ก ์ค๋
", dict(table2=NATURAL)),
("B) ์
๋ ํฐ๋ฅผ ๋ฆฌํ์๋์ธ์ผ๋ก ์ค๋
", dict(table2=TABLE2, endian="little")),
("C) ๊ธธ์ด๋ฅผ 32 ๋ก ์ค๋
", dict(table2=TABLE2, length=32)),
("์ ๋ต ๋ชจ๋ธ ", dict(table2=TABLE2)),
):
hits = count_hits(**kw)
print(" %s -> ํ๋ณด %d๊ฐ %s" % (label, len(hits), ("DH{%s}" % hits[0]) if hits else ""))python3 pitfall.py
TABLE2๋ 'A'๋ถํฐ ์์ํ๋ '0'..'9','A'..'F' ์์๋ก ์ฝ์ผ๋ฉด ์ด๊ธ๋๋ค. bytes4๋ฅผ uint32๋ก ๋ฐ๊พธ๋ Solidity ๋ณํ์ ๋นํธ ํํ์ ๊ทธ๋๋ก ์ ์งํ๋ฏ๋ก ๋น
์๋์ธ์ด๊ณ , ๋ฆฌํ์๋์ธ์ผ๋ก ์ฝ์ผ๋ฉด ๋๋จธ์ง๊ฐ ๋ฌ๋ผ์ง๋ค. ๊ฒ์ฌ ๋์์ ์
๋ ฅ ์ ์ฒด๊ฐ ์๋๋ผ 31๋ฐ์ดํธ์ง๋ฆฌ output์ด๋ค. ์
๋ค ์ฆ์์ด ๋๊ฐ์ด "์๋ฌด๊ฒ๋ ์ ๋์ด"์ด๋ผ, ์ด๋๊ฐ ํ๋ ธ๋์ง ์๋ ค ์ฃผ๋ ์ ํธ๊ฐ ์๋ค.
๐ Full Exploit
์ ์๋ผ๊ณ ํด ๋ด์ผ 256๊ฐ๋ค. ๊ฐ ์์์์ผ๋ก 31๊ธ์๋ฅผ ์ ๊ฐํ๊ณ keccak256("DH{" + s + "}")๋ฅผ ๋ชฉํ ์์์ ๋น๊ตํ๋ค.
#!/usr/bin/env python3
"""dreamthereum (Dreamhack, Platinum 4 / reversing) โ solver
challenge.sol ์ flagChecker ๋ flag ๋ฅผ 31๋ฐ์ดํธ๋ก ์ฝ์ผ๋ฉด์
flag[i+2] == internalCall(flag[i], flag[i+1]) (i = 0 .. 28)
์ ๊ฐ์ ํ๋ค. internalCall ์ "dummy<A>(<TABLE1[hex(B)]>)" ๋ผ๋ ์๊ทธ๋์ฒ๋ฅผ ๋ฌธ์์ด๋ก
์กฐ๋ฆฝํด ์๊ธฐ ์์ ์ call ํ๊ณ , ๋ถ๋ฆฐ ๋๋ฏธ ํจ์๋ TABLE2[uint32(msg.sig) % 16] ์ ๋๋ ค์ค๋ค.
์ฆ f(A,B) = TABLE2[ uint32(keccak256("dummy"+A+"("+TABLE1[hex(B)]+")")[:4]) % 16 ] ์ด๊ณ ,
31๊ธ์ ์ ์ฒด๊ฐ ์ ๋ ๊ธ์๋ง์ผ๋ก ๊ฒฐ์ ๋๋ค โ ํ๋ณด๋ 16*16 = 256๊ฐ๋ฟ.
๋ง์ง๋ง ๊ด๋ฌธ keccak256("DH{" + output + "}") == 0xe6a9...7806 ๋ก ํ๋๋ฅผ ๊ณ ๋ฅธ๋ค.
"""
from Crypto.Hash import keccak
TABLE1 = ["uint256", "bytes", "bytes4", "bytes1", "bytes32", "uint8",
"uint16", "uint248", "string", "uint240", "uint224", "uint192",
"uint24", "uint32", "uint56", "uint72"]
# 0x41..0x46 = 'A'..'F', 0x30..0x39 = '0'..'9'
TABLE2 = [0x41, 0x42, 0x43, 0x44, 0x45, 0x46,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39]
ALPHABET = "0123456789ABCDEF" # getArgsFromB ๊ฐ ํ์ฉํ๋ ๋ฌธ์ ์งํฉ
TARGET = 0xe6a9e5bcd806b56f7ab007dbb1e881f5b5746f00cb940b8083be861a257f7806
def k256(data: bytes) -> bytes:
h = keccak.new(digest_bits=256)
h.update(data)
return h.digest()
def hexval(ch: str) -> int:
"""getArgsFromB: '0'-'9' -> 0..9, 'A'-'F' -> 10..15"""
o = ord(ch)
if 0x41 <= o < 0x41 + 6:
return o - 0x41 + 0xa
if 0x30 <= o <= 0x39:
return o - 0x30
raise ValueError("BOOOOM!")
def selector(sig: str) -> int:
"""abi.encodeWithSignature ์ ์ 4๋ฐ์ดํธ = uint32(msg.sig)"""
return int.from_bytes(k256(sig.encode())[:4], "big")
def build_table():
"""f[A][B] = internalCall(A, B) ์ ๋ฐํ ๋ฌธ์"""
table = {}
for a in ALPHABET:
for b in ALPHABET:
sig = "dummy%s(%s)" % (a, TABLE1[hexval(b)])
table[(a, b)] = chr(TABLE2[selector(sig) % len(TABLE2)])
return table
def expand(c0: str, c1: str, table, length: int = 31) -> str:
s = [c0, c1]
while len(s) < length:
s.append(table[(s[-2], s[-1])])
return "".join(s)
def main():
table = build_table()
# 256๊ฐ ์์์ ์ ์
hits = []
for c0 in ALPHABET:
for c1 in ALPHABET:
out = expand(c0, c1, table)
digest = k256(b"DH{" + out.encode() + b"}")
if int.from_bytes(digest, "big") == TARGET:
hits.append(out)
for out in hits:
flag = "DH{%s}" % out
print("[+] output =", out, "(len %d)" % len(out))
print("[+] keccak =", k256(flag.encode()).hex())
print("[+] FLAG =", flag)
if not hits:
print("[-] ํ๋ณด ์์")
return
# ์์ฒด๊ฒ์ฐ: ์ฐพ์ flag ๋ฅผ flagChecker ๋ก์ง ๊ทธ๋๋ก ๋ค์ ๋๋ ค ๋ณธ๋ค
out = hits[0]
for i in range(len(out) - 2):
assert table[(out[i], out[i + 1])] == out[i + 2], i
print("[+] self-check: flag[i+2] == f(flag[i], flag[i+1]) for all i โ OK")
if __name__ == "__main__":
main()python3 solve.py
DH{ABCB69B964FA423A7BCB69B964FA423}๋ณต์๋ ๋ฌธ์์ด์ ๋ณด๋ฉด A7 ์ดํ BCB69B964FA423์ด ๊ทธ๋๋ก ๋ฐ๋ณต๋๋ค. ์ ์ดํจ์๊ฐ ์ ํํ ํ๋ผ์ ์ฐฝ์ด ์ด๋ฏธ ์ง๋์จ ์์ ๋ค์ ๋ง๋๋ฉด ๊ทธ๋๋ถํฐ ์ฃผ๊ธฐ์ ๊ฐํ๋ค. ์ฌ๊ธฐ์๋ ์ฃผ๊ธฐ 14๋ก ์ํํ๋ค.
โ ์ค์ EVM ์์์ ํ์ธ
๊ณ์ฐ์ผ๋ก ๋์จ ๊ฐ์ด๋ ์ปจํธ๋ํธ๊ฐ ์ง์ง true๋ฅผ ์ฃผ๋์ง๋ ๋ด์ผ ํ๋ค. ๋ฌธ์ ์๋ฒ๊ฐ ์๋ ์ ํ์ด๋ผ ๋ก์ปฌ์์ EVM์ ๋์ ๋ค. foundry ์ด๋ฏธ์ง๋ฅผ ๊ทธ๋๋ก ์ฐ๋ฉด solc ์ค์น๊น์ง ์์์ ํ๋ค.
์ฌ๊ธฐ์ ๊ฑธ๋ฆฌ๋ ๊ฒ internalCall์ onlyOwner๋ค. owner๋ ์์ฑ์์์ msg.sender๋ก ๋ฐํ๋ฏ๋ก, ํ
์คํธ ์ปจํธ๋ํธ๊ฐ ์ง์ Challenge๋ฅผ ๋ฐฐํฌํ๋ฉด ๊ทธ ํ
์คํธ ์ปจํธ๋ํธ๊ฐ owner๊ฐ ๋๋ค. forge-std ์์ด ์์ Solidity๋ง์ผ๋ก ์งฐ๋ค.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "../src/Challenge.sol";
// forge-std ์์ด ๋๋ ํ
์คํธ. ์ด ์ปจํธ๋ํธ๊ฐ Challenge ๋ฅผ ๋ฐฐํฌํ๋ฏ๋ก owner == ์ด ์ปจํธ๋ํธ๊ฐ ๋์ด
// internalCall ์ onlyOwner ๋ฅผ ํต๊ณผํ๋ค.
contract FlagTest {
Challenge c;
function setUp() public {
c = new Challenge();
}
// ์ ๋ต flag โ flagChecker ๊ฐ true
function test_correct_flag_returns_true() public {
bool ok = c.flagChecker(bytes("ABCB69B964FA423A7BCB69B964FA423"));
require(ok, "flagChecker returned false");
}
// ๋ง์ง๋ง ํ ๊ธ์๋ง ๋ฐ๊ฟ๋ i=28 ์ require ์์ BOBOBOBO!!! ๋ก revert
function test_last_byte_flipped_reverts_BOBOBOBO() public {
try c.flagChecker(bytes("ABCB69B964FA423A7BCB69B964FA424")) returns (bool ok) {
require(!ok, "tampered flag was accepted");
} catch Error(string memory reason) {
require(
keccak256(bytes(reason)) == keccak256(bytes("BOBOBOBO!!!")),
"unexpected revert reason"
);
}
}
// 16์ง ๋ฌธ์๊ฐ ์๋ ๋ฐ์ดํธ๊ฐ ์ธ์ํ์
์๋ฆฌ์ ์ค๋ฉด getArgsFromB ๊ฐ BOOOOM!
function test_non_hex_byte_reverts_BOOOOM() public {
try c.flagChecker(bytes("AZCB69B964FA423A7BCB69B964FA423")) returns (bool ok) {
require(!ok, "non-hex flag was accepted");
} catch Error(string memory reason) {
require(
keccak256(bytes(reason)) == keccak256(bytes("BOOOOM!")),
"unexpected revert reason"
);
}
}
}docker run --rm -e FOUNDRY_DISABLE_NIGHTLY_WARNING=1 -v "$PWD":/w -w /w --entrypoint forge ghcr.io/foundry-rs/foundry:latest test -vv
3๊ฐ ์ ๋ถ ํต๊ณผ. ๋ฐํ๊ฐ๊ณผ ์คํจ ๊ฒฝ๋ก๊น์ง ์ค์ EVM์์ ํ์ธ๋ ์ ์ด๋ค.
ํธ์ถ ํธ๋ ์ด์ค๋ฅผ ๋ณด๋ฉด ์ปจํธ๋ํธ ์์์ ๋ฌด์จ ์ผ์ด ๋ฒ์ด์ง๋์ง ๊ทธ๋๋ก ๋์จ๋ค. -vvvv๋ก ์ผ๊ณ , ๊ฐ์ด๋ฐ ๋ฐ๋ณต ๊ตฌ๊ฐ์ ์๋ผ์ ๋ดค๋ค.
docker run --rm -e FOUNDRY_DISABLE_NIGHTLY_WARNING=1 -v "$PWD":/w -w /w --entrypoint forge ghcr.io/foundry-rs/foundry:latest test --match-test test_correct_flag_returns_true -vvvv > trace.txt 2>&1; sed -n "/^Traces:/,+13p" trace.txt; echo " ...(์ค๋ต: dummy ํธ์ถ์ ๋ชจ๋ 29ํ)..."; tail -9 trace.txt
์ฒซ ํธ์ถ์ด Challenge::dummyA(0) โ Return 67์ด๋ค. 67์ 0x43, ๊ณง 'C'๋ค. ์์์ ํ๋ก ๋ฝ์ f('A','B') = 'C'๊ฐ EVM ์์์ ๊ทธ๋๋ก ์ฌํ๋๋ค. ํธ๋ ์ด์ค ๋์ โ [Return] true๊ฐ flagChecker์ ๋ฐํ๊ฐ์ด๋ค.
๋ง์ง๋ง ๊ด๋ฌธ์ธ keccak256๋ ์ปจํธ๋ํธ ์ฃผ์์ ์ ํ ๊ทธ ์จ๋ผ์ธ ๋๊ตฌ๋ก ํ ๋ฒ ๋ ํ์ธํ๋ค. ์ถ์ ์๊ฐ challenge.sol 1082ํ์ URL์ ๋จ๊ฒจ ๋๋ค.

โป๏ธ ์ฌํ
๋ฌธ์ ํด๋๋ง ์์ผ๋ฉด flag๊น์ง ํ ๋ฒ์ ๋์ค๊ฒ ์คํฌ๋ฆฝํธ๋ฅผ ๋จ๊ฒผ๋ค. solve.py๋ keccak๋ง ์์ผ๋ฉด ๋๊ณ (ํ์ด์ฌ pycryptodome), docker์ foundry ์ด๋ฏธ์ง๊ฐ ์์ผ๋ฉด EVM ๊ฒ์ฆ๊น์ง ์ด์ด์ ๋๋ค.
#!/usr/bin/env bash
# dreamthereum (Dreamhack, Platinum 4 / reversing) โ ํ ๋ฐฉ ์ฌํ.
# solve.py ๋ก flag ๋ฅผ ๋ณต์ํ๊ณ , docker foundry ๊ฐ ์์ผ๋ฉด ์ค์ EVM ์ผ๋ก ํ ๋ฒ ๋ ๊ฒ์ฆํ๋ค.
set -eu
cd "$(dirname "$(readlink -f "$0")")"
EXPECT=$(python3 -c "import json;print(json.load(open('๋ฌธ์ .json'))['flag'])" 2>/dev/null || echo '')
# 1) ์คํ๋ผ์ธ ๋ณต์ โ keccak256 ๋ง ์์ผ๋ฉด ๋๋ค (pycryptodome)
OUT=$(timeout 300 python3 solve.py 2>&1) || true
echo "$OUT"
FLAG=$(printf '%s' "$OUT" | grep -aoE 'DH\{[^}]+\}' | head -1)
# 2) (์ ํ) ์ค์ EVM ๊ฒ์ฆ โ foundry ์ด๋ฏธ์ง๊ฐ ์ด๋ฏธ ๋ฐ์์ ธ ์์ ๋๋ง ๋๋ค
if command -v docker >/dev/null 2>&1 && docker image inspect ghcr.io/foundry-rs/foundry:latest >/dev/null 2>&1; then
echo
echo "--- forge test (์ค์ EVM) ---"
( cd evm && timeout 600 docker run --rm -v "$PWD":/w -w /w \
--entrypoint forge ghcr.io/foundry-rs/foundry:latest test -vv ) || true
docker run --rm -v "$PWD/evm":/w alpine:latest rm -rf /w/out /w/cache >/dev/null 2>&1 || true
else
echo
echo "(docker/foundry ์์ โ EVM ๊ฒ์ฆ์ ๊ฑด๋๋ด๋ค. solve.py ๋ง์ผ๋ก flag ๋ ๋์จ๋ค)"
fi
echo
if [ -n "$FLAG" ] && { [ -z "$EXPECT" ] || [ "$FLAG" = "$EXPECT" ]; }; then
echo "โ
PASS $FLAG"; exit 0
fi
echo "โ FAIL (์ป์ ๊ฐ: '${FLAG:-์์}' / ๊ธฐ๋: '$EXPECT')"; exit 1./reproduce.sh
๋ก์ปฌ ํ๊ฒฝ: Ubuntu 6.17 ์ปค๋ / Python 3.13 + pycryptodome / Docker 29.7.2 + ghcr.io/foundry-rs/foundry:latest(solc 0.8.36 ์๋ ์ค์น). challenge.sol์ evm/src/Challenge.sol๋ก ๊ทธ๋๋ก ๋ณต์ฌํด์ ์ด๋ค.
๐ ๊ฒฐ๋ก
์ ๋ ํฐ๋ ์ฃผ์๊ฐ ์๋๋ผ ๊ฐ์ผ๋ก๋ ์ธ ์ ์๋ค
์ด ๋ฌธ์ ์ ์ฌ๋ฏธ๋ keccak256 ํด์์ ์ 4๋ฐ์ดํธ๋ฅผ ๋์ํ์ฒ๋ผ ๊ตด๋ฆฐ ๋ฐ ์๋ค. ํจ์ ์ด๋ฆ๊ณผ ์ธ์ ํ์ ์ด ํ ๊ธ์์ฉ ๋์๋๋ "๋ฌธ์ ๋ ๊ฐ โ ๋ฌธ์ ํ๋"๋ผ๋ ์ ์ด๊ฐ ๋ง๋ค์ด์ง๊ณ , ๊ทธ ์ ์ด๊ฐ ์ฌ์ฌ๋ก ์ฎ์ด๋ฉด์ 31๊ธ์์ง๋ฆฌ ๋ต์ด ์ ๋ ๊ธ์๋ก ์์ถ๋๋ค. ๋ฏธ๋ผ 1024์ค์ ์ด ๊ตฌ์กฐ๋ฅผ ๋ฎ๊ธฐ ์ํ ๊ป๋ฐ๊ธฐ๋ค.
ํ์ ๊ณต๊ฐ์ ์ค์ด๋ ๊ฑด ์ ์ฝ ์์ฒด๋ค
require๊ฐ ๋งค ํ ๋ค์ ๊ธ์๋ฅผ ๋ชป ๋ฐ๋ ์๊ฐ ์์ ๋๋ ๋ ๊ธ์๋ก ๋จ์ด์ง๋ค. ์ด๋ฐ ๋ฌธ์ ์์ ๋จผ์ ๋ณผ ๊ฒ์ ์๊ณ ๋ฆฌ์ฆ์ ๋ณต์ก๋๊ฐ ์๋๋ผ ์
๋ ฅ์ ์ด๋ ๋ถ๋ถ์ด ์ ๋ง ์์ ๋ก์ด๊ฐ๋ค. ๋ฐ๋๋ก ๋ถ๋ถ ์ ๋ต ์ ํธ๊ฐ ์ ํ ์๋ค๋ ์ ๋๋ฌธ์, ๋ชจ๋ธ์ด ํ๋ฆฌ๋ฉด ํ๋ณด 0๊ฐ๋ผ๋ ๊ฒฐ๊ณผ๋ง ๋จ๋๋ค. ๊ทธ๋์ "ํ๋ณด๊ฐ ์ ํํ 1๊ฐ"๋ผ๋ ์ฌ์ค์ด ๊ณง ๋ชจ๋ธ ๊ฒ์ฆ์ด ๋๋ค.
์จ์ฒด์ธ์ ์ฌ๋ผ๊ฐ ๊ฒ์ ๋น๋ฐ์ ์๋ค
flagChecker๋ onlyOwner๋ก ๊ฐ์ธ์ฌ ์์ด ์๋ฌด๋ ๋ถ๋ฅผ ์ ์์ง๋ง, ๊ฒ์ฆ์ ์ฐ์ด๋ ์์์ ๋ก์ง์ ์ ๋ถ ์ฝ๋ ์์ ์๋ค. ์ปจํธ๋ํธ ๋ฐ์ดํธ์ฝ๋๋ ๋๊ตฌ๋ ์ฝ์ ์ ์์ผ๋ ์ ๊ทผ ์ ์ด๋ ์คํ ๊ถํ์ ๋ง์ ๋ฟ ์ ๋ณด๋ฅผ ๊ฐ์ถ์ง ๋ชปํ๋ค. ๋ต์ ์ปจํธ๋ํธ ์์์ ๊ณ์ฐํ ์ ์๊ฒ ๋ง๋ค์ด ๋๋ฉด ๊ทธ ๊ณ์ฐ์ ์คํ๋ผ์ธ์์๋ ๋๊ฐ์ด ๋์๊ฐ๋ค.
์ค๋ฒ๋ก๋ฉ๊ณผ ์ ๋ ํฐ ์ถฉ๋
๊ฐ์ ์ด๋ฆ์ 16๊ฐ์ง ํ์
์ผ๋ก ์ค๋ฒ๋ก๋ํด๋ ์
๋ ํฐ๊ฐ ๊ฒน์น์ง ์์๊ธฐ์ ์ด ๊ตฌ์กฐ๊ฐ ์ฑ๋ฆฝํ๋ค. ์ค์ ์๋น์ค ์ปจํธ๋ํธ๋ผ๋ฉด ๋ฐ๋๋ก ์ํ ์ ํธ๋ค. 4๋ฐ์ดํธ๋ ์ถฉ๋์ด ์ผ๋ง๋ ์ง ๋ง๋ค์ด์ง ์ ์๋ ๊ณต๊ฐ์ด๊ณ , ํ๋ก์ ํจํด์์ ์
๋ ํฐ ์ถฉ๋์ ์ค์ ์ฌ๊ณ ๋ก ์ด์ด์ง ์ ์ด ์๋ค. ๋ฐฐํฌ ์ ์ forge inspect <์ปจํธ๋ํธ> methods๋ก ์
๋ ํฐ ํ๋ฅผ ๋ฝ์ ๋๋ฉด ์ต์ํ ๊ฒน์นจ์ ๋์ผ๋ก ํ์ธํ ์ ์๋ค.
Comments
๋๊ธ
๋๊ธ์ ๋จ๊ธฐ๋ ค๋ฉด ๋ก๊ทธ์ธ์ด ํ์ํด์. (๋ค์ด๋ฒ ยท ๊ตฌ๊ธ ๊ณ์ )
๋๊ธ ๋ถ๋ฌ์ค๋ ์คโฆ