首页
社区
课程
招聘
2025 全国大学生信息安全竞赛初赛 Reverse WriteUp
发表于: 1天前 223

2025 全国大学生信息安全竞赛初赛 Reverse WriteUp

1天前
223

wasm-login

前端逻辑分析

页面中有一个「登录」按钮,点击后会调用 WASM 中的authenticate函数。

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
import { authenticate } from "./build/release.js";
    // 初始化 WASM 模块
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;
             
            // 调用 WASM 中的 authenticate 函数
            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(() => {
              // 实际应用中这里应该是真实的 API 请求
              // 这里仅作演示,使用本地判断
              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');
      }
    }
     
    // 页面加载完成后初始化 WASM
 window.addEventListener('load', initWasm);
  

前端会计算一个 check 值:

1
2
check = MD5(JSON.stringify(authData));
// 要求:check 必须以 "ccaf33e3512e31f3" 开头

因此要 枚举时间戳,找到一个能满足该前缀要求的 check

最终提交格式:

1
flag{正确的check值}

WASM 后端分析(release.wasm.map)

使用 PowerShellrelease.wasm.map 中提取 TypeScript 源码,这些源码原本就是有注释的:

1
2
3
4
5
6
7
$map = Get-Content -Raw -Path .\[release.wasm.map](9f4K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4u0W2L8r3g2S2M7$3g2Q4x3X3g2%4j5i4y4E0i4K6u0W2L8h3q4H3i4K6t1&6 | ConvertFrom-Json
$idx = $map.sources.IndexOf('assembly/index.ts')
$map.sourcesContent[$idx] | Out-File -Encoding ascii .\assembly_index.ts
 
$map = Get-Content -Raw -Path .\[release.wasm.map](721K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4u0W2L8r3g2S2M7$3g2Q4x3X3g2%4j5i4y4E0i4K6u0W2L8h3q4H3i4K6t1&6 | ConvertFrom-Json
$idx = $map.sources.IndexOf('assembly/base64.ts')
$map.sourcesContent[$idx] | Out-File -Encoding ascii .\assembly_base64.ts

assembly_index.ts 中可以看到核心认证逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function authenticate(username: string, password: string): string {
  // 1. Base64编码密码
  const encodedPassword = encode(stringToUint8Array(password));
  //console.log(encodedPassword);
  // 2. 获取当前时间戳(毫秒)
  const timestamp = Date.now().toString();
  //console.log(timestamp);
  // 3. 构建原始JSON消息
  const message = `{"username":"${username}","password":"${encodedPassword}"}`;
  //console.log(message);
  // 4. 使用HMAC-SHA256签名
  const signature = signMessage(message, timestamp);
  //console.log(signature);
  // 5. 构建最终JSON消息
  const finalMessage = `          {"username":"${username}","password":"${encodedPassword}","signature":"${signature}"}`;
 
  return finalMessage;
  //return "ok";
}

签名函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function signMessage(message: string, secret: string): string {
  const messageBytes = String.UTF8.encode(message);
  const secretBytes = String.UTF8.encode(secret);
  /**
  const messageBytesPtr = changetype<usize>(messageBytes);
  const secretBytesPtr = changetype<usize>(secretBytes);
   
  const hashInput = new ArrayBuffer(messageBytes.byteLength + secretBytes.byteLength);
  const hashInputPtr = changetype<usize>(hashInput);
  memory.copy(hashInputPtr, messageBytesPtr, messageBytes.byteLength);
  memory.copy(hashInputPtr + messageBytes.byteLength, secretBytesPtr,   secretBytes.byteLength);
 
  const signatureBytes = new ArrayBuffer(32);
  const signatureBytesPtr = changetype<usize>(signatureBytes);
  init();
  update(hashInputPtr, hashInput.byteLength);
  final(signatureBytesPtr)
  */
  const signatureBytes = hmacSHA256(secretBytes,messageBytes);
   
  return encode(ArrayBufferToUint8Array(signatureBytes));
}   

自定义 Base64:

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
// @ts-ignore: decorator
@lazy
  const PADCHAR = "=";
// @ts-ignore: decorator
@lazy
//  const ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; //这是标准base64映射表
const ALPHA = "NhR4UJ+z5qFGiTCaAIDYwZ0dLl6PEXKgostxuMv8rHBp3n9emjQf1cWb2/VkS7yO";//这是本题中使用的新表
   /**
    * Encode Uint8Array as a base64 string.
    * @param bytes Byte array of type Uint8Array.
    */
   export function encode(bytes: Uint8Array): string {
     let i: i32, b10: u32;
      
     const extrabytes = (bytes.length % 3);
     let imax = bytes.length - extrabytes;
     const len = ((bytes.length / 3) as i32) * 4 + (extrabytes == 0 ? 0 : 4);
     let x = changetype<string>(__new(<usize>(len << 1), idof<string>()));
  
     if (bytes.length == 0) {
       return "";
     }
  
     let ptr = changetype<usize>(x) - 2;
     for (i = 0; i < imax; i += 3) {
       b10 =
         ((bytes[i] as u32) << 16) |
         ((bytes[i + 1] as u32) << 8) |
         (bytes[i + 2] as u32);
       store<u16>(ptr+=2, (ALPHA.charCodeAt(b10 >> 18) as u16));
       store<u16>(ptr+=2, (ALPHA.charCodeAt(((b10 >> 12) & 63)) as u16));
       store<u16>(ptr+=2, (ALPHA.charCodeAt(((b10 >> 6) & 63)) as u16));
       store<u16>(ptr+=2, (ALPHA.charCodeAt((b10 & 63)) as u16));
     }
  
     switch (bytes.length - imax) {
       case 1:
         b10 = (bytes[i] as u32) << 16;
         store<u16>(ptr+=2, ((ALPHA.charCodeAt(b10 >> 18)) as u16));
         store<u16>(ptr+=2, ((ALPHA.charCodeAt((b10 >> 12) & 63)) as u16));
         store<u16>(ptr+=2, ((PADCHAR.charCodeAt(0)) as u16));
         store<u16>(ptr+=2, ((PADCHAR.charCodeAt(0)) as u16));
         break;
       case 2:
         b10 = ((bytes[i] as u32) << 16) | ((bytes[i + 1] as u32) << 8);
         store<u16>(ptr+=2, ((ALPHA.charCodeAt(b10 >> 18)) as u16));
         store<u16>(ptr+=2, ((ALPHA.charCodeAt((b10 >> 12) & 63)) as u16));
         store<u16>(ptr+=2, ((ALPHA.charCodeAt((b10 >> 6) & 63)) as u16));
         store<u16>(ptr+=2, ((PADCHAR.charCodeAt(0)) as u16));
         break;
     }
  
     return x;
   }

自定义 HMAC-SHA256 关键点:

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
function hmacSHA256(key: ArrayBuffer, message: ArrayBuffer): ArrayBuffer {
  const blockSize = 64; // SHA256 ????????64 ???
 
  // ??????
  const keyPtr = changetype<usize>(key);
  const paddedKey = new ArrayBuffer(blockSize);
  const paddedKeyPtr = changetype<usize>(paddedKey);
  if (key.byteLength > blockSize) {
    // ????????????????????????????????n      init();
      update(keyPtr, key.byteLength);
      final(paddedKeyPtr);
  }else{
    // ????????????
      memory.copy(paddedKeyPtr, keyPtr, key.byteLength);
      fill(paddedKeyPtr + key.byteLength, 0, blockSize - key.byteLength)
  }
  //console.log(ArrayBufferToUint8Array(paddedKey).toString());
 
  // ??? ipad ??opad
  const ipad = new ArrayBuffer(blockSize);
  const opad = new ArrayBuffer(blockSize);
  const ipadPtr = changetype<usize>(ipad);
  const opadPtr = changetype<usize>(opad);
  for (let i = 0; i < blockSize; i++) {
      store<u8>(ipadPtr + i , load<u8>(paddedKeyPtr + i) ^ 0x76);
      store<u8>(opadPtr + i , load<u8>(paddedKeyPtr + i) ^ 0x3C);
  }
  //console.log(ArrayBufferToUint8Array(ipad).toString());
  //console.log(ArrayBufferToUint8Array(opad).toString());
 
  // ??? innerHash
  const innerInput = new ArrayBuffer(ipad.byteLength + message.byteLength);
  const innerInputPtr = changetype<usize>(innerInput);
  const messagePtr = changetype<usize>(message)
  memory.copy(innerInputPtr, ipadPtr, ipad.byteLength);
  memory.copy(innerInputPtr + ipad.byteLength, messagePtr, message.byteLength);
  //console.log(ArrayBufferToUint8Array(innerInput).toString());
 
  init();
  update(innerInputPtr,innerInput.byteLength);
  //update(ipadPtr,ipad.byteLength);
  //update(messagePtr,message.byteLength);
  const innerHash = new ArrayBuffer(32);
  const innerHashPtr = changetype<usize>(innerHash);
  final(innerHashPtr);
  //console.log(ArrayBufferToUint8Array(innerHash).toString());
 
  // ??? outerHash
  const outerInput = new ArrayBuffer(opad.byteLength + innerHash.byteLength);
  const outerInputPtr = changetype<usize>(outerInput);
  memory.copy(outerInputPtr, innerHashPtr, innerHash.byteLength);
  memory.copy(outerInputPtr + innerHash.byteLength, opadPtr, opad.byteLength);
  //console.log(ArrayBufferToUint8Array(outerInput).toString());
 
  init();
  update(outerInputPtr,outerInput.byteLength);
  //update(opadPtr,opad.byteLength);
  //update(innerHashPtr,innerHash.byteLength);
  const outerHash = new ArrayBuffer(32);
  const outerHashPtr = changetype<usize>(outerHash);
  final(outerHashPtr);
  //console.log(ArrayBufferToUint8Array(outerHash).toString());
 
  return outerHash;
}

总结不同点:

  • 使用 自定义 Base64 字母表,不能用标准库直接编码。
  • HMAC 中的 ipad / opad 常量为 0x760x3C,不是标准的 0x36 / 0x5C
  • HMAC key 为字符串化的时间戳 Date.now().toString()

固定用户名和密码

HTML 最后一行注释:

1
<!-- 测试账号 admin 测试密码 admin -->

所以可以确定:

1
2
username = "admin"
password = "admin"

对每一个候选时间戳:

  1. 用自定义 Base64 编码密码 admin

  2. 构造 message

    {"username":"admin","password":"编码后的密码"}
    
  3. 使用时间戳字符串做key,按**自定义 HMAC-SHA256** 计算签名。

  4. HMAC 结果再做自定义 Base64,得到 signature

  5. 构造最终 JSON:

    {"username":"admin","password":"...","signature":"..."}
    
  6. 计算 check = MD5(JSON.stringify(finalJSON))

  7. 判断 check 是否以 ccaf33e3512e31f3 开头。


时间范围与爆破脚本

根据文件时间(题目提示「爆肝到周一凌晨」对,题目描述是这样的):

  • release.js:2025/12/22 00:29
  • release.wasm:2025/12/22 00:57
  • index.html:2025/12/22 01:06

推测时间戳大致落在 2025-12-22 00:00:00 ~ 02:00:00(UTC+8)之间,直接在该区间内暴力搜索即可。

Node.js 脚本如下:

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
const crypto = require('crypto');
 
// 自定义 Base64 字母表
const ALPHA = "NhR4UJ+z5qFGiTCaAIDYwZ0dLl6PEXKgostxuMv8rHBp3n9emjQf1cWb2/VkS7yO";
 
function base64Custom(buf) {
  let out = '';
  let i = 0;
  for (; i + 2 < buf.length; i += 3) {
    const b10 = (buf[i] << 16) | (buf[i + 1] << 8) | buf[i + 2];
    out += ALPHA[(b10 >> 18) & 63];
    out += ALPHA[(b10 >> 12) & 63];
    out += ALPHA[(b10 >> 6) & 63];
    out += ALPHA[b10 & 63];
  }
  const rem = buf.length - i;
  if (rem === 1) {
    const b10 = buf[i] << 16;
    out += ALPHA[(b10 >> 18) & 63];
    out += ALPHA[(b10 >> 12) & 63];
    out += '=';
    out += '=';
  } else if (rem === 2) {
    const b10 = (buf[i] << 16) | (buf[i + 1] << 8);
    out += ALPHA[(b10 >> 18) & 63];
    out += ALPHA[(b10 >> 12) & 63];
    out += ALPHA[(b10 >> 6) & 63];
    out += '=';
  }
  return out;
}
 
function sha256(buf) {
  return crypto.createHash('sha256').update(buf).digest();
}
 
// 自定义 HMAC (ipad/opad 常量不同)
function hmacSHA256(key, message) {
  const blockSize = 64;
  let paddedKey = Buffer.alloc(blockSize, 0);
  if (key.length > blockSize) {
    sha256(key).copy(paddedKey, 0);
  } else {
    key.copy(paddedKey, 0);
  }
  const ipad = Buffer.alloc(blockSize);
  const opad = Buffer.alloc(blockSize);
  for (let i = 0; i < blockSize; i++) {
    const b = paddedKey[i];
    ipad[i] = b ^ 0x76;
    opad[i] = b ^ 0x3C;
  }
  const innerHash = sha256(Buffer.concat([ipad, message]));
  const outerHash = sha256(Buffer.concat([innerHash, opad]));
  return outerHash;
}
 
const username = 'admin';
const password = 'admin';
const encodedPassword = base64Custom(Buffer.from(password, 'utf8'));
const message = `{"username":"${username}","password":"${encodedPassword}"}`;
const messageBytes = Buffer.from(message, 'utf8');
 
const prefix = 'ccaf33e3512e31f3';
const start = Date.parse('2025-12-22T00:00:00+08:00');
const end = Date.parse('2025-12-22T02:00:00+08:00');
 
for (let ts = start; ts < end; ts++) {
  const key = Buffer.from(String(ts), 'utf8');
  const sigBytes = hmacSHA256(key, messageBytes);
  const signature = base64Custom(sigBytes);
  const finalMessage = `{"username":"${username}","password":"${encodedPassword}","signature":"${signature}"}`;
  const check = crypto.createHash('md5').update(finalMessage).digest('hex');
  if (check.startsWith(prefix)) {
    console.log('FOUND', ts, check);
    break;
  }
}

爆破结果:

1
2
时间戳:1766334550699
check:ccaf33e3512e31f36228f0b97ccbc8f1

最终得到:

1
flag{ccaf33e3512e31f36228f0b97ccbc8f1}

更快的办法不需要分析后端的细节:

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
<!-- Brute-force timestamp helper (CTF) -->
  <div class="mt-6 text-center text-sm text-neutral-500">
    <button id="bruteforce-btn" class="mt-2 px-4 py-2 rounded-lg bg-secondary text-white">?????</button>
    <div id="bruteforce-status" class="mt-2 text-neutral-400">???</div>
  </div>
<script type="module">
    import { authenticate } from "./build/release.js";
 
    const bfBtn = document.getElementById('bruteforce-btn');
    const bfStatus = document.getElementById('bruteforce-status');
 
    bfBtn.addEventListener('click', async () => {
      const prefix = "ccaf33e3512e31f3";
      const username = "admin";
      const password = "admin";
      const start = Date.parse("2025-12-22T00:00:00+08:00");
      const end = Date.parse("2025-12-22T02:00:00+08:00");
 
      const oldNow = Date.now;
      bfStatus.textContent = "???...";
      bfBtn.disabled = true;
 
      for (let ts = start; ts < end; ts++) {
        Date.now = () => ts;
        const authResult = authenticate(username, password);
        const data = JSON.parse(authResult);
        const check = CryptoJS.MD5(JSON.stringify(data)).toString(CryptoJS.enc.Hex);
        if (check.startsWith(prefix)) {
          bfStatus.textContent = `FOUND: ${ts} | ${check}`;
          Date.now = oldNow;
          bfBtn.disabled = false;
          return;
        }
      }
 
      Date.now = oldNow;
      bfStatus.textContent = "???";
      bfBtn.disabled = false;
    });
</script>

在前端页面增加这样的代码,一会就有结果了

图片描述

BabyGame

1
利用项目直接解包: [821K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6s2c8q4u0q4g2r3!0G2L8s2y4Q4x3V1k6Y4k6s2y4V1k6h3y4G2L8i4m8Q4x3V1k6J5k6h3I4W2j5i4y4W2M7#2)9J5c8Y4c8S2k6#2)9J5c8Y4j5J5i4K6u0W2y4q4)9J5k6e0m8Q4y4f1c8Q4x3U0S2Z5N6s2c8H3M7#2)9K6b7g2)9J5c8W2)9J5c8X3N6A6N6r3S2#2j5W2)9J5k6h3y4G2L8g2)9J5c8V1N6p5f1V1g2f1L8$3!0D9M7#2)9J5c8X3N6V1M7$3c8W2j5$3!0E0M7q4)9J5c8Y4u0W2L8r3g2S2M7$3g2K6i4K6u0r3N6r3q4Y4i4K6u0r3N6U0u0Q4x3X3f1@1i4K6u0W2x3q4)9J5z5b7`.`. //Godot引擎可以解包源码

scripts/flag.gd 中初始化:

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
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")

仅依赖这里的 key 还不足以得到最终 flag,因为游戏过程中 key 会被修改

scripts/game_manager.gd

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

也就是说,当获得第一个金币时:

1
2
3
FanAglFanAglOoO! -> F an A gl F an A gl OoO!
                   B    B      (A→B)
最终 key = FbnBglFbnBglOoO!

再次回到 flag.gd 的加密逻辑(AES-ECB):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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")

**目标:**找到一个输入,对 修改后的 key (FbnBglFbnBglOoO!)做 AES-ECB 加密,密文为

1
d458af702a680ae4d089ce32fc39945d

可以直接在本地用 Python 复现 Godot 的 AES 行为。

解密脚本

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
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
 
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
 
# 初始key
initial_key = "FanAglFanAglOoO!"
 
# 得分为1后,key中的'A'被替换为'B'
final_key = initial_key.replace("A", "B")
print(f"初始 key: {initial_key}")
print(f"最终 key: {final_key}")
 
# 目标加密结果
target_encrypted_hex = "d458af702a680ae4d089ce32fc39945d"
target_encrypted = bytes.fromhex(target_encrypted_hex)
 
print(f"\n目标密文 (hex): {target_encrypted_hex}")
print(f"目标密文 (bytes): {target_encrypted}")
print(f"密文长度: {len(target_encrypted)} bytes")
 
# 使用AES-ECB模式解密
try:
    # 创建AES cipher对象 (ECB模式)
    cipher = AES.new(final_key.encode('utf-8'), AES.MODE_ECB)
 
    # 解密
    decrypted = cipher.decrypt(target_encrypted)
 
    # 尝试去除padding
    try:
        flag = unpad(decrypted, AES.block_size).decode('utf-8')
        print(f"\n[OK] 成功解密 (带padding): {flag}")
    except:
        # 如果去除padding失败,直接解码
        flag = decrypted.decode('utf-8').rstrip('\x00')
        print(f"\n[OK] 成功解密 (无padding): {flag}")
 
    # 验证 - 重新加密看是否匹配
    cipher_verify = AES.new(final_key.encode('utf-8'), AES.MODE_ECB)
    # Godot的AES会自动padding到16字节的倍数
    flag_padded = flag.encode('utf-8')
    if len(flag_padded) % 16 != 0:
        flag_padded = pad(flag_padded, AES.block_size)
 
    encrypted_verify = cipher_verify.encrypt(flag_padded)
 
    if encrypted_verify.hex() == target_encrypted_hex:
        print(f"[OK] 验证成功!加密结果匹配")
    else:
        print(f"[FAIL] 验证失败")
        print(f"  预期: {target_encrypted_hex}")
        print(f"  实际: {encrypted_verify.hex()}")
 
    print(f"\n{'='*50}")
    print(f"FLAG: {flag}")
    print(f"{'='*50}")
 
except Exception as e:
    print(f"\n[FAIL] 解密失败: {e}")
    import traceback
    traceback.print_exc()

最终得到:

1
flag{wOW~youAregrEaT!}

EzFlag

主逻辑(C++ 伪代码)

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax
  __int64 v4; // rax
  _BYTE v6[32]; // [rsp+0h] [rbp-50h] BYREF
  _BYTE v7[12]; // [rsp+20h] [rbp-30h] BYREF
  int v8; // [rsp+2Ch] [rbp-24h] BYREF
  char v9; // [rsp+33h] [rbp-1Dh]
  int i; // [rsp+34h] [rbp-1Ch]
  unsigned __int64 v11; // [rsp+38h] [rbp-18h]
 
  std::string::basic_string(v6, argv, envp);
  std::operator<<<std::char_traits<char>>(&std::cout, "Enter password: ");
  std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, v6);
  if ( (unsigned __int8)std::operator!=<char>(v6, "V3ryStr0ngp@ssw0rd") )
  {
    v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Wrong password!");
    std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
  }
  else
  {
    std::operator<<<std::char_traits<char>>(&std::cout, "flag{");
    std::ostream::flush((std::ostream *)&std::cout);
    v11 = 1;
    for ( i = 0; i <= 31; ++i )
    {
      v9 = f(v11);
      std::operator<<<std::char_traits<char>>(&std::cout, (unsigned int)v9);
      std::ostream::flush((std::ostream *)&std::cout);
      if ( i == 7 || i == 12 || i == 17 || i == 22 )
      {
        std::operator<<<std::char_traits<char>>(&std::cout, "-");
        std::ostream::flush((std::ostream *)&std::cout);
      }
      v11 *= 8LL;
      v11 += i + 64;
      v8 = 1;
      std::chrono::duration<long,std::ratio<1l,1l>>::duration<int,void>(v7, &v8);
      std::this_thread::sleep_for<long,std::ratio<1l,1l>>(v7);
    }
    v4 = std::operator<<<std::char_traits<char>>(&std::cout, "}");
    std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
  }
  std::string::~string(v6);
  return 0;
}
//c++看起来太乱了 以下代码会更清晰
 
if (input != "V3ryStr0ngp@ssw0rd") {
  print("Wrong password!");
} else {
  print("flag{");
  v11 = 1;
  for (i = 0; i <= 31; ++i) {
    ch = f(v11);
    print(ch);
    if (i == 7 || i == 12 || i == 17 || i == 22)
      print("-");
    v11 = v11 * 8 + i + 64; // 64-bit unsigned
    sleep(1);
  }
  print("}");
}
 
// f
v5 = 0; v4 = 1;
for (i = 0; i < a1; ++i) {
  v2 = v4;
  v4 = (v5 + v4) & 0xF; // modulo 16
  v5 = v2;
}
return K[v5];

字符生成函数 f

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 __fastcall f(unsigned __int64 a1)
{
  __int64 v2; // [rsp+10h] [rbp-20h]
  unsigned __int64 i; // [rsp+18h] [rbp-18h]
  __int64 v4; // [rsp+20h] [rbp-10h]
  __int64 v5; // [rsp+28h] [rbp-8h]
 
  v5 = 0;
  v4 = 1;
  for ( i = 0; i < a1; ++i )
  {
    v2 = v4;
    v4 = ((_BYTE)v5 + (_BYTE)v4) & 0xF;
    v5 = v2;
  }
  return *(unsigned __int8 *)std::string::operator[](&K, v5);
}

可以看成 Fibonacci 序列模 16 后索引一个 16 字节表,再配合特定递推生成的 v

如果你有耐心 可以运行程序然后耐心的等待...等不下去了...

利用周期性加速

Fibonacci 序列模 16 是周期性的,周期为 24。预先把 F(n) mod 16 的 24 项打表即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
K = "012ab9c3478d56ef"
 
# 预计算 F(n) mod 16 的 24 周期
F_MOD16 = [0, 1]
for _ in range(22):  # total 24
    F_MOD16.append((F_MOD16[-1] + F_MOD16[-2]) & 0xF)
 
def f(a1: int) -> str:
    idx = F_MOD16[a1 % 24]
    return K[idx]
 
def solve():
    out = ["flag{"]
    v11 = 1
    for i in range(32):
        out.append(f(v11))
        if i in (7, 12, 17, 22):
             out.append("-")
        v11 = (v11 * 8 + i + 64) & 0xFFFFFFFFFFFFFFFF
    out.append("}")
    return "".join(out)
 
if __name__ == "__main__":
    print(solve())

运行结果:

1
flag{10632674-1d219-09f29-14769-f60219a24}

Eternum

题目给出:

  • go 编译的 kworker 可执行文件
  • 捕获的流量 tcp.pcap
  • 启动脚本 run.sh

Go 文件已去符号,但仍可通过 GoReSym 恢复较完整的符号信息:

1
GoReSym.exe kworker > gorelog.txt 2>&1 //控制台打印不下 有800kb....

从生成的日志中可以定位 main.main

{
   "Start": 6656832, //10--->16即可
  "Start": 6656832,
  "End": 6657248,
  "PackageName": "main",
  "FullName": "main.main"
}

在逆向过程中,iupHvc2q4 包含核心逻辑(而且主要部分都在混淆过函数名的函数下,那种一眼就能看到干什么的函数都显得不重要了):

1
2
3
4
5
6
7
8
main.main        = 0x659340
 
// iupHvc2q4.* 是主业务包
//   Run          : 驱动会话(newproc + chanrecv)
//   fuzkMtvzPreC : 收包解析链:recv → 校验 → AES‑GCM → zstd → protobuf
//   d4k0A9zcOh   : 发包链的一部分
//   OnJCbKpp     : AES‑GCM 校验 / 解密
//   HaNDRB_IhET  : 构造自定义 magic

连接与 goroutine 调度

iupHvc2q4.(*H1eV17y).Run 主要逻辑:

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
//iupHvc2q4.(*H1eV17y).Run (0x6571E0)
  _QWORD *sub_6571E0()
  {
    ...
    if ((*(v0) || !Connect()) && !o6wghH0urlbA()) {
      // goroutine1: fuzkMtvzPreC
      v2 = runtime_newobject();
      *v2 = sub_657360;                 // Run.gowrap1 -> fuzkMtvzPreC
      v2[1] = result;
      sub_4511A0();                      // newproc
 
      // goroutine2: KeepAlive
      v5 = runtime_newobject();
      *v5 = sub_657300;                 // Run.gowrap2 -> KeepAlive
      v5[1] = result;
      sub_4511A0();                      // newproc
 
      (*(void (**)(void))(result[4] + 32LL))();  // 启动/唤醒某通道
      runtime_chanrecv1_0();                     // 等待
    }
    return result;
  }
//下是手写的伪代码
if ((connect_ok || already_connected) && !o6wghH0urlbA()) {
  // goroutine 1:收包处理 fuzkMtvzPreC
  go fuzkMtvzPreC(session)
 
  // goroutine 2:心跳/keepalive
  go KeepAlive(session)
 
  // 等待会话结束
  chan_recv(done_chan)
}

收包解析链:fuzkMtvzPreC

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
//iupHvc2q4.(*H1eV17y).fuzkMtvzPreC (0x657420)
 
  __int64 __fastcall sub_657420()
  {
    ...
    v41 = sub_6584A0();                 // 生成 magic (ET3RNUMX)
 
    while (1) {
      (*(void (**)(void))(v54[4] + 32))();    // 等待/触发接收
      result = runtime_selectnbrecv();        // 非阻塞接收
      if ((_BYTE)result) break;
 
      v50 = runtime_makeslice();              // 分配接收 buffer
      sub_4B3F40();                           // 读数据到 buffer
      if (!err) {
        v30 = sub_658FA0();                   // memcmp/magic检查
        if (ok) {
          v49 = runtime_makeslice();          // 读长度字段
          sub_4B3F40();                       // 继续读
          if (!err) {
            v34 = _byteswap_ulong(*v49);      // big-endian length
            v48 = runtime_makeslice();        // 读 payload
            sub_4B3F40();                     // 继续读
 
            v33 = v34 + 12;                   // payload长度 + nonce长度
            v20 = runtime_makeslice();
 
            if (v48 != v20 + ((-v34 >> 63) & 0xC))
              sub_482C20();                   // copy payload
 
            *(retval_658DE0 *)v30._r0 = sub_658DE0(); // OnJCbKpp: 校验+AES-GCM
 
            if (!decrypt_err) {
              v52 = runtime_newobject();
              if (!sub_538B80(&off_74C2A0, v52, ...)) // protobuf 反序列化
                sub_657860();                         // 处理消息
            }
          }
        }
      }
    }
    return result;
  }
//下是手写的伪代码
magic = HaNDRB_IhET(); // "ET3RNUMX"
 
while (true) {
  wait_readable();
 
  // 读取并检查 magic
  read_exact(sock, buf, 8);
  if (!check_magic(buf, magic)) continue;
 
  // 读取长度(4 字节,大端)
  read_exact(sock, &len_be, 4);
  uint32_t len = bswap32(len_be);
 
  // 读取 payload
  read_exact(sock, cipher_buf, len);
 
  // 尝试 AES‑GCM 解密+校验
  if (!OnJCbKpp(cipher_buf, len, &plain_buf))
    continue;
 
  // zstd 解压 + protobuf 反序列化
  if (!protobuf_unmarshal(plain_buf, &msg))
    continue;
 
  handle_msg(session, &msg);
}

自定义魔数 HaNDRB_IhET

函数 HaNDRB_IhET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//iupHvc2q4.HaNDRB_IhET (0x6584A0)
 
  __int64 __fastcall sub_6584A0()
  {
    ...
    result = runtime_makeslice();
    for (i = 0; i < 8; ++i)
      *(byte*)(result+i) = byte_98F158[i] ^ 0x99;
    return result;
  }
//下是手写的伪代码
  byte_98F158 XOR 0x99 = ET3RNUMX
  → 用于帧头 magic。
   
result = make([]byte, 8);
for i in range(8) {
  result[i] = byte_98F158[i] ^ 0x99;
}

byte_98F158 内容:

1
DC CD AA CB D7 CC D4 C1

0x99 异或后的 ASCII

1
ET3RNUMX

这与 pcap中每个应用层负载开头的 8 字节完全一致。

AES-GCM 密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//iupHvc2q4.OnJCbKpp (0x658DE0)
 
  retval_658DE0 sub_658DE0()
  {
    ...
    if (len >= 12 &&
        (magic = sub_6584A0(),
         memcmp_ok = sub_658FA0(magic, ...)) &&
        (v7 = bswap32(*(u32*)(buf+8)) + 12, len >= v7)) {
 
      v8 = off_99B220;               // AES key 结构
      v9 = qword_99B228;             // AES key 长度
      sub_6586C0();                   // AES 初始化
 
      if (!v8)
        sub_658A60(...);             // AES-GCM 解密/验证
    } else {
      sub_502B00();                  // 错误路径
    }
    return result;
  }

OnJCbKpp 中调用 AES 相关函数,密钥位于只读段:

1
2
78 66 71 47 63 56 6a 72 4f 57 70 35 74 55 47 43
50 46 51 71 34 34 38 6e 50 44 6a 49 4c 54 65 37

转为 ASCII

1
xfqGcVjrOWp5tUGCPFQq448nPDjILTe7

这是一条 32 字节 AES 密钥,用于 AES‑GCM

自定义帧格式

结合逆向代码和流量分析,可得每条消息格式:

1
2
3
4
[8  字节] 魔数      : "ET3RNUMX"
[4  字节] 长度 (BE) : n
[12 字节] GCM nonce
[n-12 字节] GCM 密文+认证标签

解密后 payloadzstd 压缩的 protobuf

提取与解密流量脚本

大致流程:

  1. tcp.pcap 解析以太网 / IP / TCP。
  2. 以四元组 (src_ip, src_port, dst_ip, dst_port) 聚合重组 TCP 流。
  3. 在流内查找魔数 ET3RNUMX,按长度字段切包。
  4. 使用密钥 xfqGcVjrOWp5tUGCPFQq448nPDjILTe7 调用 AES‑GCM 解密。
  5. 使用 zstd解压缩明文。
  6. 在明文中搜索可疑的 base32 字符串并解码。

脚本如下:

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
141
142
143
144
145
146
import struct
from pathlib import Path
from collections import defaultdict
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import zstandard as zstd
import base64
 
pcap_path = Path(r"C:\Users\Euarno\Desktop\eternum_665a4bf7903bb6ea0b05c1fa8a447f4e\tcp.pcap")
key = b"xfqGcVjrOWp5tUGCPFQq448nPDjILTe7"
aesgcm = AESGCM(key)
 
def parse_pcap(path):
    data = path.read_bytes()
    magic = struct.unpack_from('<I', data, 0)[0]
    endian = '<' if magic == 0xa1b2c3d4 else '>'
    offset = 24
    pkts = []
    while offset + 16 <= len(data):
        _, _, incl_len, _ = struct.unpack_from(endian + 'IIII', data, offset)
        offset += 16
        if offset + incl_len > len(data):
            break
        pkts.append(data[offset:offset+incl_len])
        offset += incl_len
    return pkts
 
def parse_ether(pkt):
    if len(pkt) < 14:
        return None
    eth_type = struct.unpack('!H', pkt[12:14])[0]
    return eth_type, pkt[14:]
 
def parse_ipv4(pkt):
    if len(pkt) < 20:
        return None
    ver_ihl = pkt[0]
    ihl = (ver_ihl & 0x0F) * 4
    if ver_ihl >> 4 != 4 or len(pkt) < ihl:
        return None
    total_len = struct.unpack('!H', pkt[2:4])[0]
    proto = pkt[9]
    src = pkt[12:16]
    dst = pkt[16:20]
    payload = pkt[ihl:total_len]
    return proto, src, dst, payload
 
def parse_tcp(pkt):
    if len(pkt) < 20:
        return None
    src_port, dst_port = struct.unpack('!HH', pkt[:4])
    seq = struct.unpack('!I', pkt[4:8])[0]
    data_offset = (pkt[12] >> 4) * 4
    if len(pkt) < data_offset:
        return None
    payload = pkt[data_offset:]
    return src_port, dst_port, seq, payload
 
def ip_str(b):
    return '.'.join(str(x) for x in b)
 
pkts = parse_pcap(pcap_path)
flows = defaultdict(list)
for pkt in pkts:
    eth = parse_ether(pkt)
    if not eth:
        continue
    eth_type, payload = eth
    if eth_type != 0x0800:
        continue
    ipv4 = parse_ipv4(payload)
    if not ipv4:
        continue
    proto, src, dst, ip_payload = ipv4
    if proto != 6:
        continue
    tcp = parse_tcp(ip_payload)
    if not tcp:
        continue
    sp, dp, seq, tcp_payload = tcp
    if not tcp_payload:
        continue
    keyf = (ip_str(src), sp, ip_str(dst), dp)
    flows[keyf].append((seq, tcp_payload))
 
streams = {}
for keyf, segs in flows.items():
    segs_sorted = sorted(segs, key=lambda x: x[0])
    data = b""
    cur = None
    for seq, payload in segs_sorted:
        if cur is None:
            cur = seq
            data += payload
            cur += len(payload)
            continue
        if seq < cur:
            overlap = cur - seq
            if overlap < len(payload):
                data += payload[overlap:]
                cur += len(payload) - overlap
        elif seq == cur:
            data += payload
            cur += len(payload)
        else:
            data += b"\x00" * (seq - cur)
            data += payload
            cur = seq + len(payload)
    streams[keyf] = data
 
MAGIC = b"ET3RNUMX"
 
def parse_messages(data):
    msgs = []
    i = 0
    while i + 12 <= len(data):
        if data[i:i+8] != MAGIC:
            j = data.find(MAGIC, i+1)
            if j == -1:
                break
            i = j
            continue
        length = struct.unpack('>I', data[i+8:i+12])[0]
        total = 12 + length
        if i + total > len(data):
            break
        payload = data[i+12:i+total]
        msgs.append(payload)
        i += total
    return msgs
 
zctx = zstd.ZstdDecompressor()
 
for flow, data in streams.items():
    msgs = parse_messages(data)
    for payload in msgs:
        nonce = payload[:12]
        ct = payload[12:]
        pt = aesgcm.decrypt(nonce, ct, None)
        try:
            decomp = zctx.decompress(pt, max_output_size=100000)
        except Exception:
            continue
        if b"MZWGCZ" in decomp:
            print(decomp)
            s = decomp.split(b"\n")[0].split(b"\x12")[-1]
            print(base64.b32decode(s))

查找 base32 字符串

1
2
3
4
5
6
7
if b"MZWGCZ" in decomp:
 
print(decomp)
 
s = decomp.split(b"n")[0].split(b"x12")[-1]
 
print(base64.b32decode(s))

最终得到

1
MZWGCZ33MI3WGNJYG4YDALJSMIYDCLJUMRSDILJYGUZDMLLBGRQTIN3BGY2WCMLBHF6QU===

将其以 base32 解码即可得到 flag:

1
flag{b7c58700-2b01-4dd4-8526-a4a47a65a1a9}

vvvmmm没做 过两天做做再发吧


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

最后于 19小时前 被江树编辑 ,原因: 增加wp
上传的附件:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回