解题思路:
加密手段:
数据结构分析:
逆向分析中的问题:
获得flag:flag{99754106633f94d350db34d548}
学到的知识:
题目类型:
WebAssembly分析和调试技巧
浏览器本地服务器搭建
MD5算法API
直接爆破
题目信息:
简介:
wp借鉴:XMAN【我真的好菜-同pizza师傅修炼笔记一】easyvm&easywasm - 灰信网(软件开发博客聚合) (freesion.com)
wasm2c needed_dynlibs_wasm2c使用不了-CSDN博客
预运行发现这个wasm是单文件没有其他可以运行他的程序!
所以先使用js把它加载进入浏览器看看有什么东西!!
创建main.js
fetch('easywasm.wasm')
.then(response => {
return WebAssembly.instantiateStreaming(response);
})
.then(results => {
// results.instance 是Wasm实例,你可以通过它访问导出的函数和变量
const exports = results.instance.exports;
// 假设你的Wasm模块导出了一个名为'myFunction'的函数
exports.myFunction();
})
创建index.html
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<style>
</style>
</head>
<body>
<span id="container"></span>
<script src="./main.js"></script>
</body>
</html>
用python来映射本地端口来调试js代码!
python -m http.server 8888
成功加载出easywasm文件看看他的基本信息!
成功加载!!!
//这里是这个wasm所用的环境变量
(memory $env.memory (;0;) (import "env" "memory") 256)
(table $env.table (;0;) (import "env" "table") 16 funcref)
(global $env.memoryBase (;0;) (import "env" "memoryBase") i32)
(global $env.tableBase (;1;) (import "env" "tableBase") i32)
(func $env.abort (;0;) (import "env" "abort") (param i32))
(memory $env.memory (;0;) 256)
(global $global2 (mut i32) (i32.const 0))
(global $global3 (mut i32) (i32.const 0))
(global $_flag_enc (;4;) (export "_flag_enc") i32 (i32.const 0))
(global $_k (;5;) (export "_k") i32 (i32.const 1104))
(global $_r (;6;) (export "_r") i32 (i32.const 1360))
//这里使用的是wasm所使用的函数!
(elem $elem0
(global.get $env.tableBase)
funcref
(ref.func $func11)
(ref.func $_to_bytes)
(ref.func $_to_int32)
(ref.func $_memcpy)
(ref.func $_memset)
(ref.func $_memcmp)
(ref.func $_strlen)
(ref.func $_md5)
(ref.func $_check)
(ref.func $func11)
(ref.func $func11)
(ref.func $func11)
(ref.func $func11)
(ref.func $func11)
(ref.func $func11)
(ref.func $func11)
)
获取这些信息后大概明白了,尤其是其中的check函数因该就是我们要分析的目标了!!
如果直接硬看哇什么的话太难了,所以直接丢给jeb!
成功翻译成伪代码!
但是太复杂了只知道check函数只需要一个参数大概就是我们需要的flag了!
还发现了一大段静态数据大概就是我们要对照的表!
简化一下逻辑提取出来!
int _check(int INput) {
//...
//初始化一大堆变量
//...
int len = _strlen(INput);
if(len == 32) {
do {
table1[idx1 + "14c260d9e8cf4ed38c77a"] =
*(char*)(INput + idx);//貌似是取出一个字符
_md5(idx1, 32, idx1 + "3b7e7a51b5213470a8d");//进行一次md5
int v6 = "562fe3cc50014c260d9e8cf4ed38c77a";
do {
...进行一次循环好像在找什么东西
}
while(v2 != "0d9e8cf4ed38c77a");
_md5(idx1, 32, idx1 + 64);//又进行了一次md5
v6 = "562fe3cc50014c260d9e8cf4ed38c77a";
do {
...进行一次循环好像在找什么东西
}
while(v1 != "0d9e8cf4ed38c77a");
//这里毫无以为是进行flag验证了
int v7 =
_memcmp(32, idx * &table2 + (int)memoryBase, idx1 + "0d9e8cf4ed38c77a");
if(v7) { //如果不等于就退出循环!
INput = "562fe3cc50014c260d9e8cf4ed38c77a";
__g2 = idx1;
return INput;
}
...如果等于就继续循环
++idx;
}
while(v0 < 32);这里发现会循环32次,也就意味这我们的flag有32个字节的长度!
...
}
...
}
下面就剩下动态调试了!让chatgpt优化一下调试脚本!
const imports = {
env: {
memory: new WebAssembly.Memory({ initial: 256 }),
table: new WebAssembly.Table({ initial: 16, element: 'anyfunc' }),
memoryBase: 0,
tableBase: 0,
abort: function(param) {
// 实现 abort 函数的逻辑
}
}
};
fetch('easywasm.wasm')
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.instantiate(buffer, imports))
.then(results => {
const instance = results.instance; //导入函数
const env1 = imports.env; //导入变量
const memoryBuffer = new Uint8Array(env1.memory.buffer); //创建一片空间指向env1.memory.buffer
// 要传递给 Wasm 的字符串
const str = "abcdaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
// 使用 TextEncoder 将字符串转换为 UTF-8 字节数组
const encoder = new TextEncoder();
const strBytes = encoder.encode(str);
// 将字节数组写入 WebAssembly 内存
const offset = 1800; // 从内存的哪个位置开始写入 ,相当于内存地址
memoryBuffer.set(strBytes, offset); //将数据写入内存
// 调用 Wasm 函数,传递字节数组的地址和长度
const result = instance.exports._check(1800); //这里的1800相当于我们存储的字符串的地址
console.log('Result:', result);
});
开始在check函数下断点!在两个md5函数和memcmp函数下个断点看看他们的参数!
第一个md5的参数:根据wasm的地址机制,猜测112和32是一个内存地址!
去看看果然是一个地址:
发现居然是一个字符串:“2333333333333333333333333333333a”,这个a大概就是我们的输入了!
多改变几次变量发现这个a也是跟随变化!!那么就可以得出第一条规则!
1. 112: 50 -》2
2. 113: 51 ——》3
3. 114: 51
.....
30. 141: 51
31. 142: 51
32. 143: 97 ——》 a
33. 144: 0
第一条加密规则:读取输入的一个字节拼接上"2333333333333333333333333333333"进行md5加密!
开始动调第二个md5:
再去看看地址64:
发现这里的内容貌似都是可见字符是:8ac44c16486e4d259983938fad820a7a
这里又可以合理猜测一波,将上一次md5的值传入第二次md5!
1. 64: 56 -》8
2. 65: 97 -》a
3. 66: 99 ->c
4. 67: 52
5. 68: 52
6. 69: 99
7. 70: 49
8. 71: 54
9. 72: 52
10. 73: 56
11. 74: 54
12. 75: 101
13. 76: 52
14. 77: 100
15. 78: 50
16. 79: 53
17. 80: 57
18. 81: 57
19. 82: 56
20. 83: 51
21. 84: 57
22. 85: 51
23. 86: 56
24. 87: 102
25. 88: 97
26. 89: 100
27. 90: 56
28. 91: 50
29. 92: 48
30. 93: 97
31. 94: 55
32. 95: 97
33. 96: 0
第二条加密规则:读取第一次md5加密产生的16进制字符的md5值进行再次md5!!
合理猜测一下第一次的md5加密到的二次的md5加密之间的代码是在进行赋值操作,将第一次加密的md5值放入64的位置!因为第一次md5运算后64的地址为空,当我调试中间这段代码时发现64地址逐渐有值了!
继续调试到第三个断点!
比较函数嘛,一定是比较两个地址的内存了呗!猜测是比较地址16和地址0
直接查看:
16地址:发现就是我们第二次加密的结果:64c286cfc623aa8d7df7c088ebf7d718
0地址去查看却发现是一串乱数据QAQ
但是去看地址0却发现不对劲这个数值不对!!按理来说这里应该是我们的输入经过两次md5后的值呀QAQ!
又去看了看:
发现这一大段的数据都是放置在memoryBase的地址处!但是我们给他赋值为0了!
而且我们的程序使用的暂存变量地址都是0,所以前面的地址内存数据会被破坏,所以我们的数据就被损坏了memcmp函数对比的两个数据!
(data (global.get $env.memoryBase) "562fe3cc50014c260d9e8cf4ed38c77a\00c022ad0cc0075a9ab14b412a1082d5f3\0064c286cfc623aa8d7df7c088ebf7d718\0083664bdee4b613b7e7a51b5213470a8d\00b020bf598aaa2b3e03ed02c85436268a\004fdac5ac807506938103e775c50099ed\004fdac5ac807506938103e775c50099ed\00c231d607b6823fd0a68e813760809754\00d168c21d10371a5ab61bcfe6c759ef6e\00f60d709ccf989d849028f97a03d2f3ba\00a0184f8240e2fe46861dc8d15a819cb0\009dbec414336e741e9c73422df59de297\006fb5209d8fc8bb8507245bcfa24ae11f\006fb5209d8fc8bb8507245bcfa24ae11f\0000c77fbc60a5bfc466d3d069876ec348\0000c77fbc60a5bfc466d3d069876ec348\00df33464fb471c46abaf691c000a0e30d\004fdac5ac807506938103e775c50099ed\00f60d709ccf989d849028f97a03d2f3ba\00fcc94a20596f2619868f3a4bf52eadf7\0000c77fbc60a5bfc466d3d069876ec348\00d168c21d10371a5ab61bcfe6c759ef6e\009dbec414336e741e9c73422df59de297\00fcc94a20596f2619868f3a4bf52eadf7\009b37db091979bedf00a7095851ba6f59\0000c77fbc60a5bfc466d3d069876ec348\00f60d709ccf989d849028f97a03d2f3ba\00fcc94a20596f2619868f3a4bf52eadf7\00d168c21d10371a5ab61bcfe6c759ef6e\00f60d709ccf989d849028f97a03d2f3ba\00183342997ffed4b3189e977d077a60b4\00f404a3368d2d8f57464f739d4ed01c0e\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00x\a4j\d7V\b7\c7\e8\dbp $\ee\ce\bd\c1\af\0f|\f5*\c6\87G\13F0\a8\01\95F\fd\d8\98\80i\af\f7D\8b\b1[\ff\ff\be\d7\5c\89\22\11\90k\93q\98\fd\8eCy\a6!\08\b4Ib%\1e\f6@\b3@\c0QZ^&\aa\c7\b6\e9]\10/\d6S\14D\02\81\e6\a1\d8\c8\fb\d3\e7\e6\cd\e1!\d6\077\c3\87\0d\d5\f4\ed\14ZE\05\e9\e3\a9\f8\a3\ef\fc\d9\02og\8aL*\8dB9\fa\ff\81\f6q\87\22a\9dm\0c8\e5\fdD\ea\be\a4\a9\cf\deK`K\bb\f6p\bc\bf\be\c6~\9b(\fa'\a1\ea\850\ef\d4\05\1d\88\049\d0\d4\d9\e5\99\db\e6\f8|\a2\1feV\ac\c4D\22)\f4\97\ff*C\a7#\94\ab9\a0\93\fc\c3Y[e\92\cc\0c\8f}\f4\ef\ff\d1]\84\85O~\a8o\e0\e6,\fe\14C\01\a3\a1\11\08N\82~S\f75\f2:\bd\bb\d2\d7*\91\d3\86\eb\07\00\00\00\0c\00\00\00\11\00\00\00\16\00\00\00\07\00\00\00\0c\00\00\00\11\00\00\00\16\00\00\00\07\00\00\00\0c\00\00\00\11\00\00\00\16\00\00\00\07\00\00\00\0c\00\00\00\11\00\00\00\16\00\00\00\05\00\00\00\09\00\00\00\0e\00\00\00\14\00\00\00\05\00\00\00\09\00\00\00\0e\00\00\00\14\00\00\00\05\00\00\00\09\00\00\00\0e\00\00\00\14\00\00\00\05\00\00\00\09\00\00\00\0e\00\00\00\14\00\00\00\04\00\00\00\0b\00\00\00\10\00\00\00\17\00\00\00\04\00\00\00\0b\00\00\00\10\00\00\00\17\00\00\00\04\00\00\00\0b\00\00\00\10\00\00\00\17\00\00\00\04\00\00\00\0b\00\00\00\10\00\00\00\17\00\00\00\06\00\00\00\0a\00\00\00\0f\00\00\00\15\00\00\00\06\00\00\00\0a\00\00\00\0f\00\00\00\15\00\00\00\06\00\00\00\0a\00\00\00\0f\00\00\00\15\00\00\00\06\00\00\00\0a\00\00\00\0f\00\00\00\15\00\00\0023333333333333333333333333333333")
所以我们就可以开始修改memoryBase的地址,为这段数据重新分配一片新的空间
const imports = {
env: {
memory: new WebAssembly.Memory({ initial: 256 }), //为整个wasm分配一片空间
table: new WebAssembly.Table({ initial: 16, element: 'anyfunc' }),
memoryBase: 1900,//常量字符串的存放地址
tableBase: 2000, //一个变量的存放地址
abort: function(param) {
// 实现 abort 函数的逻辑
}
}
};
fetch('easywasm.wasm')
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.instantiate(buffer, imports))
.then(results => {
const instance = results.instance; //导入函数
const env1 = imports.env; //导入变量
const memoryBuffer = new Uint8Array(env1.memory.buffer); //创建一片空间指向env1.memory.buffer
// const Base = new Uint8Array(env1.memoryBase.buffer); //创建一片空间指向env1.memory.buffer
// 要传递给 Wasm 的字符串
const str = "abcdaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
// 使用 TextEncoder 将字符串转换为 UTF-8 字节数组
const encoder = new TextEncoder();
const strBytes = encoder.encode(str);
// 将字节数组写入 WebAssembly 内存
const offset = 1800; // 从内存的哪个位置开始写入 ,相当于内存地址
memoryBuffer.set(strBytes, offset); //将数据写入内存
// 调用 Wasm 函数,传递字节数组的地址和长度
const result = instance.exports._check(1800); //这里的1800相当于我们存储的字符串的地址
console.log('Result:', result);
});
现在再去调试就发现是正确的了!对比16地址和1900地址的值,如果正确就继续下一次循环!
最终分析出结果!
加密算法就是:将我们输入的32个字节每个提取出来拼接上一个字符串进行md5加密产生一个16进制的字符串值,再将这个字符串进行md5加密继续产出16进制的字符串值,与目标值进行对比!
由于md5不可逆,所以只需要采用简单爆破就好了!
脚本就出来了!
脚本:
from hashlib import md5
a=['562fe3cc50014c260d9e8cf4ed38c77a',
'c022ad0cc0075a9ab14b412a1082d5f3',
'64c286cfc623aa8d7df7c088ebf7d718',
'83664bdee4b613b7e7a51b5213470a8d',
'b020bf598aaa2b3e03ed02c85436268a',
'4fdac5ac807506938103e775c50099ed',
'4fdac5ac807506938103e775c50099ed',
'c231d607b6823fd0a68e813760809754',
'd168c21d10371a5ab61bcfe6c759ef6e',
'f60d709ccf989d849028f97a03d2f3ba',
'a0184f8240e2fe46861dc8d15a819cb0',
'9dbec414336e741e9c73422df59de297',
'6fb5209d8fc8bb8507245bcfa24ae11f',
'6fb5209d8fc8bb8507245bcfa24ae11f',
'00c77fbc60a5bfc466d3d069876ec348',
'00c77fbc60a5bfc466d3d069876ec348',
'df33464fb471c46abaf691c000a0e30d',
'4fdac5ac807506938103e775c50099ed',
'f60d709ccf989d849028f97a03d2f3ba',
'fcc94a20596f2619868f3a4bf52eadf7',
'00c77fbc60a5bfc466d3d069876ec348',
'd168c21d10371a5ab61bcfe6c759ef6e',
'9dbec414336e741e9c73422df59de297',
'fcc94a20596f2619868f3a4bf52eadf7',
'9b37db091979bedf00a7095851ba6f59',
'00c77fbc60a5bfc466d3d069876ec348',
'f60d709ccf989d849028f97a03d2f3ba',
'fcc94a20596f2619868f3a4bf52eadf7',
'd168c21d10371a5ab61bcfe6c759ef6e',
'f60d709ccf989d849028f97a03d2f3ba',
'183342997ffed4b3189e977d077a60b4',
'f404a3368d2d8f57464f739d4ed01c0e']
flag = ""
for i in range(0, 32):
ci = 0
for cc in range(0x20, 0x7F):
ch = chr(cc).encode('ascii')
str1 = md5(b'2333333333333333333333333333333' + ch).hexdigest()
x = md5(str1.encode('utf-8') ).hexdigest()
if(x == a[i]):
ci = cc
break
assert(ci != 0)
flag += chr(ci)
print(flag)