CTF · 2024年10月30日

浅析新时代的wasm逆向思路-由源鲁杯入门题和N1junior零解题开始

首发于先知社区。

前言

WebAssembly的逆向工程在实际应用和CTF竞赛中愈发频繁。当前网络上的一些关于 Wasm 逆向的文章和教程往往显得过于繁琐或过时,因此本文旨在探讨 2024 年我们该如何更高效解决 Wasm 的逆向问题。

本文将对 Wasm 进行简要介绍,接着阐述几种常见的逆向方法及其优缺点,最后通过两道 2024 年的赛题进行实践演示。

关于wasm

WebAssembly(简称 Wasm)是一种新兴的低级字节码格式,旨在提升 Web 应用程序的性能和效率。Wasm 代码能够在现代浏览器中以接近原生的执行速度运行,此外,它还可以适用于服务器和嵌入式设备等多种环境。这种特性使得 Wasm 成为开发高性能 Web 应用的理想选择。

Wasm 的设计初衷是为了让开发者能够在不同平台上实现高效的代码执行,同时保持良好的跨平台特性。由于其低级特性,Wasm 也逐渐成为逆向工程师关注的焦点。

对于想要进行 Wasm 逆向工程的开发者和研究者,可以利用一些在线工具,比如https://mbebenita.github.io/WasmExplorer/ ,该网站允许用户进行简单的 Wasm 编译和优化,提供了一个直观的界面以便于快速实验和学习。

逆向wasm的几种方法

解包反编译wasm,配合ida

首先,可以使用 wasm2wat 工具将 .wasm 文件转换为文本格式的 .wat 文件。这一步骤有助于分析 Wasm 的结构和逻辑。

$ ./wasm2wat wasm.wasm -o wasm.wat
$ ./wasm2c wasm.wasm -o wasm.c

经过以上步骤后,将会生成 wasm.c 和 wasm.h 文件。这些文件可以与 IDA Pro 配合使用,进行更深入的静态分析。

虽然这种方法相对繁琐,但其优势在于利用了 IDA Pro 强大的分析能力,相比之下,Ghidra 的反编译效果往往不尽如人意。

ghidra插件一把梭

对于希望快速进行 Wasm 反编译的用户,可以使用 Ghidra 的 Wasm 插件,具体信息可参考 https://github.com/nneonneo/ghidra-wasm-plugin/

这种方法的优势在于其简单快捷,用户只需安装插件即可开始分析。然而,Ghidra 的反编译和操作体验有时会让人感到不够理想,尤其在面对复杂代码时。

IDA9.0

IDA 9.0 版本添加了对 Wasm 的反汇编支持,但目前尚未实现反编译功能。在处理 Wasm 文件时,可以考虑将 IDA 9.0 与 Ghidra 结合使用,以便更全面地解决逆向工程中的问题。

chrome 开发者工具 / VSCODE nodejs

Chrome 开发者工具和 VSCode 的 Node.js 插件提供了对 Wasm 的调试支持。结合 Ghidra 的插件,可以在 Chrome 中设置断点并进行调试。这种方法适合需要动态分析的场景,能够实时查看内存和调用栈的变化。

更多关于 Wasm 调试和内存查看的方法,可以参见参考文章:https://panda0s.top/2021/05/14/WebAssembly-Reverse/#%E8%B0%83%E8%AF%95%E6%96%B9%E6%B3%95

2024源鲁杯round2 wasm

解题分析

题目给了一个wasm文件,这里我们采用ghidra一把梭的方式。得到反编译的代码。

undefined4 unnamed_function_7(void)

{
  int local_20 [4];
  int local_10;
  int local_c;
  int local_8;
  undefined4 local_4;

  local_4 = 0;
  local_8 = unnamed_function_14(s_GZCTF_FLAG_ram_00000429);
  local_c = unnamed_function_20(local_8);
  if ((local_c != 0) && (*(char *)(local_8 + local_c + -1) == '\n')) {
    *(undefined *)(local_8 + local_c + -1) = 0;
  }
  for (local_10 = 0; *(char *)(local_8 + local_10) != '\0'; local_10 = local_10 + 1) {
    if ((*(char *)(local_8 + local_10) < 'A') || ('Z' < *(char *)(local_8 + local_10))) {
      if ((*(char *)(local_8 + local_10) < 'a') || ('z' < *(char *)(local_8 + local_10))) {
        *(undefined *)(local_8 + local_10) = *(undefined *)(local_8 + local_10);
      }
      else {
        *(char *)(local_8 + local_10) = *(char *)(local_8 + local_10) + -0x20;
        *(char *)(local_8 + local_10) = (char)((*(char *)(local_8 + local_10) + -0x3a) % 0x1a) + 'A'
        ;
      }
    }
    else {
      *(char *)(local_8 + local_10) = *(char *)(local_8 + local_10) + ' ';
      *(char *)(local_8 + local_10) = (char)((*(char *)(local_8 + local_10) + -0x5a) % 0x1a) + 'a ';
    }
  }
  for (local_10 = 0; *(char *)(local_8 + local_10) != '\0'; local_10 = local_10 + 1) {
    local_20[0] = (int)*(char *)(local_8 + local_10);
    unnamed_function_15(0x43a,local_20);
  }
  return 0;
}

ghidra的反编译有些丑陋,我对他进行了一些优化。

undefined4 unnamed_function_7(void)
{
  int local_20 [4];
  int i;
  int local_c;
  int gzflag;
  undefined4 local_4;

  local_4 = 0;
  gzflag = unnamed_function_14(s_GZCTF_FLAG_ram_00000429);
  local_c = unnamed_function_20(gzflag);
  if ((local_c != 0) && (*(char *)(gzflag + local_c + -1) == '\n')) {
    *(undefined *)(gzflag + local_c + -1) = 0;
  }
  for (i = 0; *(char *)(gzflag[i]) != '\0'; i = i + 1) {
    if ((*(char *)(gzflag[i]) < 'A') || ('Z' < *(char *)(gzflag[i]))) {
      if ((*(char *)(gzflag[i]) < 'a') || ('z' < *(char *)(gzflag[i]))) {
        *(undefined *)(gzflag[i]) = *(undefined *)(gzflag[i]);
      }
      else {
        *(char *)(gzflag[i]) = *(char *)(gzflag[i]) + -0x20;
        *(char *)(gzflag[i]) = (char)((*(char *)(gzflag[i]) + -0x3a) % 0x1a) + 'A'
        ;
      }
    }
    else {
      *(char *)(gzflag[i]) = *(char *)(gzflag[i]) + ' ';
      *(char *)(gzflag[i]) = (char)((*(char *)(gzflag[i]) + -0x5a) % 0x1a) + 'a ';
    }
  }
  for (i = 0; *(char *)(gzflag[i]) != '\0'; i = i + 1) {
    local_20[0] = (int)*(char *)(gzflag[i]);
    unnamed_function_15(0x43a,local_20);
  }
  return 0;
}

观察发现这是一个简单的类似于凯撒的加密,不过也不是一模一样,总之guess一下写出脚本就好了。

exp

def decrypt_flag(gzflag):
    decrypted = []

    for char in gzflag:
        if 'A' <= chr(char) <= 'Z':  # 大写字母
            char = char - ord('A')
            char = char - 0x20
            char = (char - ord('A') + 0x5a) % 26 + ord('a')  # 模运算
        elif 'a' <= chr(char) <= 'z':  # 小写字母
            char = char - ord('a')
            char = char + 0x20  # 加上0x20
            char = (char - ord('a') + 0x3a) % 26 + ord('A')  # 模运算

        decrypted.append(chr(char))

    return ''.join(decrypted)

# 测试解密函数
gzflag = [0x66, 0x73, 0x6a, 0x61, 0x6d, 0x7b, 0x48, 0x31, 0x30, 0x49, 0x38, 0x33,
          0x30, 0x37, 0x2d, 0x36, 0x35, 0x4c, 0x4b, 0x2d, 0x34, 0x4c, 0x35, 0x36,
          0x2d, 0x49, 0x30, 0x37, 0x39, 0x2d, 0x30, 0x31, 0x32, 0x37, 0x38, 0x4b,
          0x4d, 0x49, 0x30, 0x32, 0x31, 0x4b, 0x7d]

decrypted_flag = decrypt_flag(gzflag)
print("Decrypted Flag:", decrypted_flag)
for i in gzflag:
    print(chr(i),end="")

2024 N1junior wasm

解题分析

这是当时的一道0解题,现在来看并不是很难,同样的使用ghidra来解决。
我们找到关键的 main.silence函数进行分析。

undefined4 main.slice(int param1,undefined8 param_2,undefined8 param_3,undefined param_4)

{
  undefined8 *puVar1;
  undefined uVar2;
  int iVar3;
  ulonglong uVar4;
  undefined8 uVar5;

  uVar4 = 0;
  uVar5 = 0;
code_r0x800ee613:
  do {
    while ((uVar2 = (undefined)uVar4, param1 != 0 && (param1 != 1))) {
      if (param1 == 2) goto code_r0x800ee6bc;

      if (param1 != 0x32) {
        if (param1 == 0x33) {
          uVar2 = 0;
        }
        else if (param1 != 0x34) {
          if (param1 == 0x35) {
            *(undefined *)((int)register0x00000008 + 0x38) = 0;
            return 0;
          }
          do {
            halt_trap();
          } while( true );
        }
        *(undefined *)((int)register0x00000008 + 0x38) = uVar2;
        return 0;
      }
code_r0x800eeaba:
      uVar4 = (ulonglong)(*(char *)((int)uVar5 + 0x2f) == '9');
      param1 = 0x34;
    }
    puVar1 = (undefined8 *)register0x00000008;
    if (register0x00000008 <= *(undefined8 **)((int)global_2 + 0x10)) {
      *(undefined8 *)((int)register0x00000008 + -8) = 0x14170000;
      iVar3 = runtime.morestack_noctxt(0);
      puVar1 = (undefined8 *)((int)register0x00000008 + -8);
      if (iVar3 != 0) {
        return 1;
      }
    }
    puVar1[-4] = puVar1[1];
    puVar1[-3] = puVar1[2];
    register0x00000008 = (BADSPACEBASE *)(puVar1 + -5);
    *(undefined8 *)register0x00000008 = 0x14170002;
    iVar3 = main.exit(0,puVar1[-4],puVar1[-3],puVar1[10],puVar1[0xb]);
    if (iVar3 != 0) {
      return 1;
    }
code_r0x800ee6bc:
    uVar4 = *(ulonglong *)((int)register0x00000008 + 0x18);
    uVar5 = *(undefined8 *)((int)register0x00000008 + 0x10);
    if (uVar4 != 0x30) {
      param1 = 0x35;
      goto code_r0x800ee613;
    }
code_r0x800ee6de:
    if (*(char *)uVar5 == 'Y') {
code_r0x800ee6f3:
      if (*(char *)((int)uVar5 + 1) == '3') {
code_r0x800ee707:
        if (*(char *)((int)uVar5 + 2) == 'R') {
code_r0x800ee71c:
          if (*(char *)((int)uVar5 + 3) == 'm') {
code_r0x800ee731:
            if (*(char *)((int)uVar5 + 4) == 'c') {
code_r0x800ee746:
              if (*(char *)((int)uVar5 + 5) == 'H') {
code_r0x800ee75b:
                if (*(char *)((int)uVar5 + 6) == 'V') {
code_r0x800ee770:
                  if (*(char *)((int)uVar5 + 7) == 'u') {
code_r0x800ee785:
                    if (*(char *)((int)uVar5 + 8) == 'a') {
code_r0x800ee79a:
                      if (*(char *)((int)uVar5 + 9) == '3') {
code_r0x800ee7ae:
                        if (*(char *)((int)uVar5 + 10) == 't') {
code_r0x800ee7c3:
                          if (*(char *)((int)uVar5 + 0xb) == 'X') {
code_r0x800ee7d8:
                            if (*(char *)((int)uVar5 + 0xc) == 'Q') {
code_r0x800ee7ed:
                              if (*(char *)((int)uVar5 + 0xd) == 'V') {
code_r0x800ee802:
                                if (*(char *)((int)uVar5 + 0xe) == 'N') {
code_r0x800ee817:
                                  uVar4 = (ulonglong)*(byte *)((int)uVar5 + 0xf);
                                  if (uVar4 == 0x4e) {
code_r0x800ee82e:
                                    uVar4 = (ulonglong)*(byte *)((int)uVar5 + 0x10);
                                    if (uVar4 == 0x58) {
code_r0x800ee845:
                                      if (*(char *)((int)uVar5 + 0x11) == 'z') {
code_r0x800ee85a:
                                        uVar4 = (ulonglong)*(byte *)((int)uVar5 + 0x12);
                                        if (uVar4 == 0x59) {
code_r0x800ee871:
                                          if (*(char *)((int)uVar5 + 0x13) == '2') {
code_r0x800ee885:
                                            if (*(char *)((int)uVar5 + 0x14) == 'N') {
code_r0x800ee89a:
                                              if (*(char *)((int)uVar5 + 0x15) == 'j') {
code_r0x800ee8af:
                                                if (*(char *)((int)uVar5 + 0x16) == 'Y') {
code_r0x800ee8c4:
                                                  uVar4 = (ulonglong)*(byte *)((int)uVar5 + 0x17);
                                                  if (uVar4 == 0x32) {
code_r0x800ee8da:
                                                    if (*(char *)((int)uVar5 + 0x18) == 'N') {
code_r0x800ee8ef:
                                                      uVar4 = (ulonglong)
                                                              *(byte *)((int)uVar5 + 0x19);
                                                      if (uVar4 == 0x6a) {
code_r0x800ee906:
                                                        if (*(char *)((int)uVar5 + 0x1a) == 'Z') {
code_r0x800ee91b:
                                                          if (*(char *)((int)uVar5 + 0x1b) == 'f')  {
code_r0x800ee930:
                                                            if (*(char *)((int)uVar5 + 0x1c) == 'R' )
                                                            {
code_r0x800ee945:
                                                              if (*(char *)((int)uVar5 + 0x1d) ==
                                                                  '0') {
code_r0x800ee959:
                                                                if (*(char *)((int)uVar5 + 0x1e) ==
                                                                    '9') {
code_r0x800ee96d:
                                                                  if (*(char *)((int)uVar5 + 0x1f)
                                                                      == 'B') {
code_r0x800ee982:
                                                                    if (*(char *)((int)uVar5 + 0x20 )
                                                                        == 'T') {
code_r0x800ee997:
                                                                      if (*(char *)((int)uVar5 +
                                                                                   0x21) == 'E') {
code_r0x800ee9ac:
                                                                        if (*(char *)((int)uVar5 +
                                                                                     0x22) == '5') {
code_r0x800ee9c0:
                                                                          if (*(char *)((int)uVar5 +
                                                                                       0x23) == 'H' )
                                                                          {
code_r0x800ee9d5:
                                                                            if (*(char *)((int)uVar 5
                                                                                         + 0x24) ==
                                                                                'X') {
code_r0x800ee9ea:
                                                                              if (*(char *)((int)
                                                  uVar5 + 0x25) == 'z') {
code_r0x800ee9ff:
                                                    if (*(char *)((int)uVar5 + 0x26) == 'Y') {
code_r0x800eea14:
                                                      if (*(char *)((int)uVar5 + 0x27) == '2') {
code_r0x800eea28:
                                                        if (*(char *)((int)uVar5 + 0x28) == 'N') {
code_r0x800eea3d:
                                                          if (*(char *)((int)uVar5 + 0x29) == 'j')  {
code_r0x800eea52:
                                                            if (*(char *)((int)uVar5 + 0x2a) == 'Y' )
                                                            {
code_r0x800eea67:
                                                              if (*(char *)((int)uVar5 + 0x2b) ==
                                                                  '2') {
code_r0x800eea7b:
                                                                if (*(char *)((int)uVar5 + 0x2c) ==
                                                                    'N') {
code_r0x800eea90:
                                                                  if (*(char *)((int)uVar5 + 0x2d)
                                                                      == 'j') {
code_r0x800eeaa5:
                                                                    if (*(char *)((int)uVar5 + 0x2e )
                                                                        == 'Z')
                                                                    goto code_r0x800eeaba;
                                                                    param1 = 0x33;
                                                                  }
                                                        // ....此处省略N行
  } while( true );
}

我们发现他使用了一个嵌套判断来对flag进行判断,我们将它提取出来可以发现这很像一个base编码后的形式,于是尝试一下base64,成功!
很简单的一道题,但很多人因为畏惧wasm所以都没有去解这道题。同时另一个原因是网上绝大多数的教程都没有提到鸡爪插件来解决wasm。绝大多数都是wat工具然后在配合ida,流程过于繁琐。

exp

Y3RmcHVua3tXQVNNXzY2NjY2NjZfR09BTE5HXzY2NjY2NjZ9

base64解码

ctfpunk{WASM_6666666_GOALNG_6666666}

总结

WebAssembly的逆向越来越常见,本文通过介绍几种逆向方法,如使用wasm2wat和ida、Ghidra插件以及Chrome开发者工具,结合两道赛题实例,展示了如何有效解决Wasm逆向问题。

其中,Ghidra插件尤为显著地降低了Wasm逆向的门槛。

希望这篇文章能帮助到各位师傅更好的解决往后的wasm题目。

苏ICP备2024067700号 | 苏公网安备32098202000238号