문제 설명
누군가 만든 매우 쉬운 wasm 문제
Files
ubuntu@WSL2:~/CTF/dreamhack.io$ tree very-easy-wasm very-easy-wasm ├── index.html └── simple.wasm 0 directories, 2 files ubuntu@WSL2:~/CTF/dreamhack.io$ file very-easy-wasm/index.html very-easy-wasm/index.html: HTML document, ASCII text ubuntu@WSL2:~/CTF/dreamhack.io$ file very-easy-wasm/simple.wasm very-easy-wasm/simple.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
simple.wasm 파일 하나와 index.html 파일 하나가 보인다.
Decompile
https://cluberwsoh9812.tistory.com/17
https://www.hahwul.com/2018/10/06/hacking-security-analysis-web-assembly/
https://github.com/WebAssembly/wabt
위 링크를 참고해서 wabt 프로젝트를 이용해서 simple.wasm 파일을 디컴파일 할 수 있다.
ubuntu@WSL2:~/CTF/dreamhack.io/very-easy-wasm$ wasm-decompile ./simple.wasm -o simple.dcmp ubuntu@WSL2:~/CTF/dreamhack.io/very-easy-wasm$ file simple.dcmp simple.dcmp: Java source, ASCII text
import memory imports_memory; table T_a:funcref(min: 3, max: 0); function f_a(a:int):int { return (a ^ 192) + 175 & 255 } function f_b(a:int):int { return (a ^ 191) + 236 & 255 } function f_c(a:int):int { return (a ^ 196) + 171 & 255 } export function something(a:int) { var d:int = 0; var b:int = d; loop L_a { var c:int = b[0]:ubyte; b[0]:byte = call_indirect(c, d % 3); d = c; b = b + 1; if (b < a) continue L_a; } }
분석
그냥 html 파일을 열면, CORS 문제로 wasm 파일이 로드되지 않는다.
따라서 vscode로 열어서 Live Server 플러그인을 이용하면 된다.
다음은 index.html 파일에서 암호화할 문자열을 입력하고 encrypt 버튼을 눌렀을때 작동하는 일부 코드이다.
<script> const btn = document.getElementById("btn") const input = document.getElementById("input") const out = document.getElementById("out") const memory = new WebAssembly.Memory({ initial: 1 }); const u8Arr = new Uint8Array(memory.buffer) WebAssembly.instantiateStreaming(fetch('simple.wasm'), { "imports": { memory } }).then(obj => { btn.addEventListener("click", function() { const flag = input.value const res = new TextEncoder().encodeInto(flag, u8Arr) const len = res.written for (let i = 0; i < 9; i++) { obj.instance.exports.something(len) for (let j = 0; j < len / 2 | 0; j++) { let t = u8Arr[len - j - 1] u8Arr[len - j - 1] = u8Arr[j] u8Arr[j] = t } } out.innerText = Array.from(u8Arr.slice(0, len), function(byte) { return ('0' + (byte & 0xFF).toString(16)).slice(-2); }).join('') }) }) </script>
Chrome 브라우저로 브레이크포인트를 걸어 하나씩 확인해봤을때
다음과 같이 작동한다는 것을 알 수 있었다.
- u8Arr에는 암호화할 문자열의 문자 하나씩 배열에 아스키 코드값으로 하나의 요소에 들어간다.
- 문자열 길이인 len 매개변수와 함께 simple.wasm 파일로부터 로드된 something 함수를 호출해
u8Arr에 있는 각 요소의 값을 변형시킨다. - 각 요소의 값들이 있는 u8Arr 배열을 역순으로 뒤집는다.
- 이런 짓거리(2~3번 과정)를 9번 반복한다.
- 마지막으로 u8Arr 배열에 저장된 바이트 데이터를 16진수 문자열로 변환시켜 결과값을 나타나게 한다.
something 함수는 다음과 같이 리스트에 값들을 변형시킨다는 것을 알 수 있었다.
이를 테면, CDE 문자열을 넣어 something 함수를 살펴봤을때
각 배열 요소들은 다음과 같은 변화가 있었다.
CDE = [67, 68, 69] (67 ^ 192) + 175 & 255 = 50 //w2c__f0 -> 0%3=0 (68 ^ 191) + 236 & 255 = 231 //w2c__f1 -> 67%3=1 (69 ^ 196) + 171 & 255 = 44 //w2c__f2 -> 68%3=2 [44, 231, 50] (44 ^ 192) + 175 & 255 = 155 //w2c__f0 -> 0%3=0 (231 ^ 196) + 171 & 255 = 206 //w2c__f2 -> 44%3=2 (50 ^ 192) + 175 & 255 = 161 //w2c__f0 -> 231%3=0 [161, 206, 155] (161 ^ 192) + 175 & 255 = 16 //w2c__f0 -> 0%3=0 (206 ^ 196) + 171 & 255 = 181 //w2c__f2 -> 161%3=2 (155 ^ 196) + 171 & 255 = 10 //w2c__f2 -> 206%3=2 [10, 181, 16] (10 ^ 192) + 175 & 255 = 121 //w2c__f0 -> 0%3=0 (181 ^ 191) + 236 & 255 = 246 //w2c__f1 -> 10%3=1 (16 ^ 191) + 236 & 255 = 155 //w2c_f1 -> 181%3=1 [155, 246, 121] ~(생략) (195 ^ 192) + 175 & 255 = 178 //w2c__f0 -> 0%3=0 (78 ^ 192) + 175 & 255 = 61 //w2c__f0 -> 195 % 3 = 0 (195 ^ 192) + 175 & 255 = 178 //w2c__f0 -> 78%3=0 [178, 61, 178]
call_indirect (param i32) (result i32)
에서 호출될 수 있는 f0, f1, f2(각각 f_a, f_b, f_c와 같음)가 각각 존재하는데,
호출되는 함수는 각 배열에 있는 요소의 값을 3을 1로 나눈 나머지에 따라 달라진다.
처음에는 무조건 0으로 시작해서 f0을 호출하다가,
0번째 요소가 67이면 67 % 3 = 1이므로 f1 호출
1번째 요소가 68이면 68 % 3 = 2이므로 f2 호출이 된다.
만약 n(단, 0은 아님)번째 요소가 100이면,
100 % 3 = 1이므로 f1을 호출해서 n+1 번째 요소의 값을 변형시킨다.
따라서 Python3 코드로 암호화시키는 코드를 작성하면 다음과 같다.
def convert_val(val, choice): if(choice == 0): return (val ^ 192) + 175 & 255 if(choice == 1): return (val ^ 191) + 236 & 255 if(choice == 2): return (val ^ 196) + 171 & 255 flag = "DH{THIS_IS_FAKE_FLAG}" #암호화시킬 문자열 flag = [ord(char) for char in flag] saved_flag = flag.copy() for i in range(9): choice = 0 for j in range(len(flag)): # print("choice: " + str(choice), end=", ") flag[j] = convert_val(flag[j], choice) choice = saved_flag[j] % 3 flag = flag[::-1] saved_flag = flag.copy() # print(flag) enc_flag_list = [hex(item)[2:].zfill(2) for item in flag] enc_flag = ''.join(enc_flag_list) # print(enc_flag_list) print(enc_flag)
PS Microsoft.PowerShell.Core\FileSystem::\\wsl.localhost\Ubuntu\home\ubuntu\CTF\dreamhack.io\very-easy-wasm> python3 simple.py 4cd86ad5eda212f25c1558d858148eb6598f0ca579
Solution
간단한 XOR 방식으로 암호화가 이루어졌기에
역으로 계산하면 풀린다. (나에게 역산은 항상 어렵다…)
def convert_val(val, choice): if(choice == 0): return ((val - 175) ^ 192) & 255 if(choice == 1): return ((val - 236) ^ 191) & 255 if(choice == 2): return ((val - 171) ^ 196) & 255 enc_flag = "aea9ee71d339c19b0e9c504213b105ebd8c78b9205d46ef347fe70ce4df8dcf4cd7c9f79" enc_flag_list = [int(enc_flag[i:i+2], 16) for i in range(0, len(enc_flag), 2)] saved_enc_flag_list = enc_flag_list.copy() for i in range(9): choice = 0 # print(enc_flag_list) # enc_flag_list = enc_flag_list[::-1] saved_enc_flag_list = enc_flag_list.copy() for j in range(len(enc_flag_list)-1, -1, -1): # print("choice: " + str(choice), end=", ") enc_flag_list[j] = convert_val(enc_flag_list[j], choice) choice = enc_flag_list[j] % 3 enc_flag_list= enc_flag_list[::-1] # print(enc_flag_list[::-1]) for i in range(len(enc_flag_list)): print(chr(enc_flag_list[i]), end='')
FLAG
DH{279E4EE960ECB3D0968BDA91C40662EF}