第一次参加国赛,难哭了😭😭
WriteUp
babygame
godot逆向,AES算法
附件程序是个小游戏。拖入x64dbg分析,一路F9,在入口点附近可以看到Godot Engine字样

上网查找一下可以知道godot是一种游戏引擎,类似unity,可以解包
使用GDRETools/gdsdecomp解析exe可以得到一个babygame文件夹,文件夹下的scripts目录打开如下

010editor可以打开gd文件直接查看,如下是flag.gd,出现了主要的检验逻辑,加密使用了AES_ECB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| extends CenterContainer
@onready var flagTextEdit: Node = $PanelContainer / VBoxContainer / FlagTextEdit @onready var label2: Node = $PanelContainer / VBoxContainer / Label2
static var key = "FanAglFanAglOoO!" var data = ""
func _on_ready() -> void : Flag.hide()
func get_key() -> String: return key
func submit() -> void : data = flagTextEdit.text
var aes = AESContext.new() aes.start(AESContext.MODE_ECB_ENCRYPT, key.to_utf8_buffer()) var encrypted = aes.update(data.to_utf8_buffer()) aes.finish()
if encrypted.hex_encode() == "d458af702a680ae4d089ce32fc39945d": label2.show() else: label2.hide()
func back() -> void : get_tree().change_scene_to_file("res://scenes/menu.tscn")
extends Node
|
题目提示需要收集金币(得分),在game_manager.gd里可以发现得分会修改key,A会被替换成B
1 2 3 4 5 6 7 8 9
| @onready var fan = $"../Fan"
var score = 0
func add_point(): score += 1 if score == 1: Flag.key = Flag.key.replace("A", "B") fan.visible = true
|
所以把key改一下,然后使用Crypto库解密即可
Exp:
1 2 3 4 5 6 7 8 9
| from Crypto.Cipher import AES import binascii enc="d458af702a680ae4d089ce32fc39945d" enc_bytes=binascii.unhexlify(enc) key=b"FanBglFanBglOoO!" cipher = AES.new(key, AES.MODE_ECB) decrypted = cipher.decrypt(enc_bytes) print(decrypted)
|
复现
wasm-login
js+wasm逆向(没学过jsweb逆向╥﹏╥…
附件是个网页登录校验项目

题目描述:

在项目根目录下执行python -m http.server 4000可以把项目跑起来(当然直接看代码也行,但是当时没思路,于是就找了个办法把项目跑起来看看)

index.html的script部分如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
| <script src="crypto-js.js"></script> <script src="build/release.js"></script> <script type="module"> import { authenticate } from "./build/release.js"; async function initWasm() { const wasmStatus = document.getElementById('wasm-status'); const loginForm = document.getElementById('login-form'); const loginBtn = document.getElementById('login-btn'); const loginSpinner = document.getElementById('login-spinner'); const statusMessage = document.getElementById('status-message'); const errorMessage = document.getElementById('error-message'); const passwordInput = document.getElementById('password'); const togglePasswordBtn = document.getElementById('toggle-password'); try {
wasmStatus.textContent = 'WASM 已加载'; wasmStatus.classList.add('text-success'); togglePasswordBtn.addEventListener('click', function() { const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password'; passwordInput.setAttribute('type', type); const icon = this.querySelector('i'); const text = this.querySelector('span'); if (type === 'text') { icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); text.textContent = '隐藏'; } else { icon.classList.remove('fa-eye'); icon.classList.add('fa-eye-slash'); text.textContent = '显示'; } }); loginForm.addEventListener('submit', async function(e) { e.preventDefault(); loginBtn.disabled = true; loginSpinner.classList.remove('hidden'); statusMessage.classList.add('hidden'); try { const username = document.getElementById('username').value; const password = document.getElementById('password').value; const authResult = authenticate(username, password); const authData = JSON.parse(authResult); console.log('发送到服务器的数据:', authData); simulateServerRequest(authData) .then(response => { if (response.success) { alert('登录成功!'); } else { showError(response.message || '登录失败,请重试'); } }) .catch(error => { console.error('登录错误:', error); showError('网络错误,请稍后重试'); }) .finally(() => { loginBtn.disabled = false; loginSpinner.classList.add('hidden'); }); } catch (error) { console.error('WASM 处理错误:', error); showError('内部错误,请联系管理员'); loginBtn.disabled = false; loginSpinner.classList.remove('hidden'); } }); function showError(message) { errorMessage.textContent = message; statusMessage.classList.remove('hidden'); const errorBox = statusMessage.querySelector('div'); errorBox.classList.add('animate-shake'); setTimeout(() => { errorBox.classList.remove('animate-shake'); }, 500); } function simulateServerRequest(data) { return new Promise(resolve => { setTimeout(() => { const check = CryptoJS.MD5(JSON.stringify(data)).toString(CryptoJS.enc.Hex); if (check.startsWith("ccaf33e3512e31f3")){ resolve({ success: true }); }else{ resolve({ success: false }); } }, 1000); }); } } catch (error) { console.error('WASM 加载失败:', error); wasmStatus.textContent = 'WASM 加载失败'; wasmStatus.classList.add('text-danger'); loginBtn.disabled = true; loginBtn.classList.add('bg-neutral-400'); loginBtn.classList.remove('bg-primary', 'hover:bg-primary/90'); } } window.addEventListener('load', initWasm); </script> </body> </html>
<!-- 测试账号 admin 测试密码 admin-->
|
审阅代码可知,校验逻辑在simulateServerRequest()里,主要是如下几行
1 2 3 4 5 6
| const check = CryptoJS.MD5(JSON.stringify(data)).toString(CryptoJS.enc.Hex); if (check.startsWith("ccaf33e3512e31f3")){ resolve({ success: true }); }else{ resolve({ success: false }); }
|
data是传入的参数,实际参数应该是authData:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const authResult = authenticate(username, password); const authData = JSON.parse(authResult);
simulateServerRequest(authData).then(response => { if (response.success) { alert('登录成功!'); } else { showError(response.message || '登录失败,请重试'); } })
|
所以校验逻辑为:调用wasm的authenticate函数,返回值求md5后与给定的哈希值头”ccaf33e3512e31f3”进行比较。结合题目描述,authenticate函数除了用到index.html末尾的测试账密,还可能用到了时间戳,意味着需要求出这个时间戳进而求出完整哈希值。可以选择爆破时间戳,但是在此之前还得了解一下时间戳怎么来的
release.js用来连接wasm和js,在release.js里可以看到一些wasm的环境配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const adaptedImports = { env: Object.setPrototypeOf({ abort(message, fileName, lineNumber, columnNumber) { message = __liftString(message >>> 0); fileName = __liftString(fileName >>> 0); lineNumber = lineNumber >>> 0; columnNumber = columnNumber >>> 0; (() => { throw Error(`${message} in ${fileName}:${lineNumber}:${columnNumber}`); })(); }, "console.log"(text) { text = __liftString(text >>> 0); console.log(text); }, "Date.now"() { return Date.now(); }, }, Object.assign(Object.create(globalThis), imports.env || {})), };
|
此处设置了环境量(环境函数?)Date.now(),通过工具wabt下的wasm2c转化出wasm的c代码,也可以发现相关内容

接下来,要想爆破时间戳,需要能够设置authenticate函数使用的时间戳。在wasm里难以修改相关逻辑,只好在release.js中修改Date.now()的定义,使其在被authenticate函数调用的时候返回我们想要的数。这里可以创建一个全局变量globalThis.mytime,然后修改Date.now()的返回值为这个全局变量,这样就可以设置不同的时间戳来让authenticate函数使用了。

最后抄一下index.html里检验逻辑的js代码就可以写出hook脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { authenticate } from "./build/release.js"; import CryptoJS from "crypto-js";
function hook(){ var startTime=new Date("2025-12-21T22:40:00+08:00").getTime(); var endTime=new Date("2025-12-22T01:10:00+08:00").getTime(); const username = "admin"; const password = "admin";
console.log("Start brute forcing..."); for(let i=startTime;i<=endTime;i+=1){ globalThis.mytime = i; const authResult = authenticate(username, password); const authData = JSON.parse(authResult); var check = CryptoJS.MD5(JSON.stringify(authData)).toString(CryptoJS.enc.Hex); if(check.startsWith("ccaf33e3512e31f3")){ console.log("found hash:",check); } } console.log("Brute forcing completed."); }
hook();
|
eternum
go逆向,待更新…
vvvmmm
vm逆向,待更新…