神奇的 C++ 模板 —— 2022-ACTF-Nagi-Knows Writeup

admin 2022年7月13日00:08:08评论27 views字数 7867阅读26分13秒阅读模式

点击 / 关注我们

Brief

很新颖的一道题目,比赛时花了将近一整天的时间做了出来(没抢到血 qaq,带哥们太猛了)。虽然 XCTF 的分站赛主办方都会公布 writeup,但还是很想记录并顺手分享一下我自己在解这道题目时的整个思考过程。尤其是这几年 CTF 中逆向方向其实已经把基础考点都考的差不多了,现在更多的是在创新性上下文章,分析程序的思路就显得更重要了。

在上手做这道题目之前首先需要对 C++ 的模板及泛型编程有个大概的了解,这里贴一份入门介绍:

[C++ 模板 | 菜鸟教程 (runoob.com)] https://www.runoob.com/cplusplus/cpp-templates.html

我将分几个 stage 来介绍自己的解题过程。

Stage 1: refactoring

拿到一个 .hpp 文件,里面有 2000 余行嵌套的 template definition,然后是一个 struct Nagi ,里面有一个 static member buf 和一个 static function GetFlag, 最后是声明了这个 buf 的具体值。

GetFlag 一看就是 RC4 密码算法, 而且 buf 就是 ciphertext. 因此现在缺的就是正确的 key 了,而想要得到正确的 key 还需要 过掉 RT 处的 check

template<typename...U>
struct Nagi {
    static char buf[108];
    static const char* GetFlag() {
        if (RT<U...>::value == false) {
            return "I don't know the flag, ask some else!";
        }
        int key[] = { HA<U>::value... };
        int S[256];
        int i, j = 0, t;
        for (= 0; i < 256; i++) { S[i] = i; }
        for (= 0; i < 256; i++) {
            j = (+ S[i] + key[% sizeof...(U)]) & 0xff;
            t = S[i], S[i] = S[j], S[j] = t;
        }
        i = j = 0;
        for (int k = 0; k < 107; k++) {
            i = (+ 1) & 0xff;
            j = (+ S[i]) & 0xff;
            t = S[i], S[i] = S[j], S[j] = t;
            buf[k] ^= S[(S[i] + S[j]) & 0xff];
        }
        return buf;
    }
};

key 由 HA<> template 类型提供, 而该类型根据其 template parameter 参数的不同又定义为 CM 类型。

CM<> 是对 int 的封装,因此将之重命名为 IntW,同时也发现 CM<> 附近也有很多类似的简单 template 声明,也将这些 template 根据其含义进行重命名。

这时发现这题还挺巧妙的,出题人通过 template specialization 实现了逻辑上的很多运算,举例如下

typedef BoolW<false> FalseW;
typedef BoolW<true> TrueW;

template <typename T>
struct Not : BoolW<!bool(T::value)>
{
};

template <typename...>
struct And : TrueW
{
};

template <typename T>
struct And<T> : T
{
};

template <typename T, typename... U>
struct And<T, U...> : Ternary<bool(T::value), And<U...>, T>::type
{
};

template <typename T, typename... U>
struct Xor : Ternary<bool(T::value), Not<Xor<U...>>, Xor<U...>>::type
{
};

有些运算仅仅去审计是不太好确定的,这时可以定义一下相应的类型然后看看结果

int main() {
  cout << Xor<TrueW, FalseW, TrueW>::value << endl;
}

通过审计 + 测试,可以大概把前 254 行的 template 声明完全看懂,但随后几个声明过于复杂,比赛时我是没有看懂。此时进入下一个 stage,即理解这一堆声明是在干什么

Stage 2: understanding

再次梳理程序的结构

  • • 0~254: 基本的 template,嵌套层级较少,简单,可以完全看懂,视作白盒

  • • 255~335: 复杂 template,后者嵌套前者,复杂,视作黑盒

  • • 336~375: 共 8 个声明,都依赖于 HN 这个 template,记作 Op_0 ~ Op_7

  • • 376-1910:大量的嵌套,但仅使用了 336-375 处定义的 template,且都是 Add<...> 的结构

  • • 1911-2101:共有 32 个声明,对 376~1910 中定义类型的简单引用,记作 C_0 ~ C_{31}

  • • 2102~2262:同样是后者依赖于前者 template 定义,只不过结构相当统一,都是 And<Pre, IsSame<C_n, Op_m<Float:type>> 的结构,同样共有 32 个

  • • 2263~2287: 8 个声明,对 parameter T 进行判断然后映射到不同的 IntW

发现

  • • 8 和 32 这几个数字在不同的程序段落中出现了好几次,一定有特殊的含义。

  • • 在 0~254 中出现的类型修饰符有 const, float/int, & 3 种,而 2**3 == 8

这时即推测,出题人是用这三类修饰符代表了三个 bit,然后定义了 GF(8) 中的乘法和加法运算

同时

  • • 再去看 336-375 处定义的 8 个 Op,每个 Op 都是由内置的一个类型与传入的类型 T 运算再结合 HN 得到最终结果。考虑到出题人并没有恶心到把 template def 打乱,因此大胆猜测这里面内置的类型就代表了 0~7 8 个数字

  • • 376~1910 中定义的都是 Add<Op(x), Op(y)...> 这样的操作,并且在嵌套之后共有 32 个 Op(x) 相加

不难想到这是矩阵点乘的一个计算过程,出题人实际上就是用 template 的嵌套定义实现了有限域内矩阵的点乘运算

类型 -> 数值的映射如下所示

= {
    0: "int",
    1: "float",
    2: "const int",
    3: "const float",
    4: "int &",
    5: "float &",
    6: "const int &",
    7: "const float &"
}

Stage 3: solving

确定本原多项式

此时,由于是有限域内的运算,还需要确定使用的本原多项式具体是什么

由于 HN 过于复杂,此时采用黑盒的办法来确定

这里介绍一个 trick,llvm 提供了很多 API 可以让我们自定义程序来分析程序,同时也提供了 Python 的封装,使用就非常方便了

可以使用 clang API 来获得一个复杂声明的最终变量类型

one.cpp
#include "nagi.hpp"

int main() {
  Op7<float &>::type a;
}
main.py
import clang.cindex
from clang.cindex import *

Config.set_library_file("libclang-10.so")


def walk(cursor: Cursor):
  l: SourceLocation = cursor.location
  if cursor.kind == CursorKind.VAR_DECL and "one.cpp" in str(l.file):
    t: Type = cursor.type
    t = t.get_canonical()
    print(t.spelling)
  for child in cursor.get_children():
    walk(child)


= {
    0: "int",
    1: "float",
    2: "const int",
    3: "const float",
    4: "int &",
    5: "float &",
    6: "const int &",
    7: "const float &"
}
rm = {}
for k, v in m.items():
  rm[v] = k
index = clang.cindex.Index.create()
walk(index.parse("./one.cpp").cursor)

因此得知 Op7(float &):type -> float ,即在该有限域下 5*7 = 1,即可确定本原多项式

神奇的 C++ 模板 —— 2022-ACTF-Nagi-Knows Writeup
image-20220628205801977.png

求解

大致思路就是分析 376~1910 行间的 template 声明,从中提取加密矩阵和运算结果,然后在 Galois 域上进行运算求解

import numpy as np
import galois
from typing import List, Mapping, Tuple
import clang.cindex
from clang.cindex import *

Config.set_library_file("libclang-10.so")


ENTRY_FILE = "./one.cpp"


def get_template_classes():
  template_classes: Mapping[str, Cursor] = {}

  def walk(node: Cursor):
    if (node.kind == CursorKind.CLASS_TEMPLATE):
      template_classes[node.spelling] = node
    for child in node.get_children():
      walk(child)

  index = clang.cindex.Index.create()
  walk(index.parse(ENTRY_FILE).cursor)
  return template_classes


def get_template_ref_tokens(cursor: Cursor) -> List[Token]:
  return [token for token in cursor.get_tokens() if token.cursor.kind == CursorKind.TEMPLATE_REF]


TEMPLATE_CLASSES = get_template_classes()


def op_val(op: Token) -> int:
  return int(op.spelling.strip('Op'))


def extract_all() -> Tuple[List, List]:
  resutls: List[int] = []
  conditions: List[List[int]] = []
  next_name = 'RT'
  # next_name = 'II'
  for _ in range(32):
    template = TEMPLATE_CLASSES[next_name]
    tokens = get_template_ref_tokens(template)
    for i, token in enumerate(tokens):
      token: Token
      if token.spelling == "IsSame":
        pivot = i
    condition_template = TEMPLATE_CLASSES[tokens[pivot+1].spelling]
    resutls.append(op_val(tokens[pivot+2]))
    conditions.append(parse_top_condition_template(condition_template))

    # update
    if len(tokens) == 4:
      break
    else:
      next_name = tokens[1].spelling
  return conditions, resutls


def parse_top_condition_template(top_template: Cursor) -> List[int]:
  for token in top_template.get_tokens():
    token: Token
    if token.cursor.kind == CursorKind.TEMPLATE_REF:
      sub1_template = TEMPLATE_CLASSES[token.spelling]
      break

  def parse_sub_template(sub_cursor: Cursor) -> List[int]:
    tokens = get_template_ref_tokens(sub_cursor)

    left_ints = [op_val(tokens[1]), op_val(tokens[2])]
    right_ints = [op_val(tokens[-2]), op_val(tokens[-1])]
    if len(tokens) == 5:
      return left_ints + right_ints
    else:
      next_cursor = TEMPLATE_CLASSES[tokens[-3].spelling]
      return left_ints + right_ints + parse_sub_template(next_cursor)

  return parse_sub_template(sub1_template)


conditions, results = extract_all()

def rc4(key):
  data = "xb0x0dx1fx0ex2ax27x08xd4x1bx8axf9xdex67x86x95x80x4fx92xcaxa1x70x2cx53xaexd7x4exf2x86x4fx37x03xdcxbexf2xc4x0ex7cx8fx8ax00x09x93xf0xd0xf3x37xd4x7ex6fx83x6dx3ex16x99x63x25x7ax3cx30x51xafxf6x3exc5x0fxc8x93xebx4fx6bxbdxc2xa1x96x2bx4exc4xcax91xcdx70xc2x24xe8xa2x92xbex1exeax48xf9x16xb0x78x00x6bx7cx95xb1xa0xcbxf7x06xafx4dxe8x96"

  key = key

  S = [for i in range(256)]
  j = 0
  out = []

  # KSA Phase
  for i in range(256):
    j = (+ S[i] + key[% len(key)]) % 256
    S[i], S[j] = S[j], S[i]

  # PRGA Phase
  i = j = 0
  for d in data:
    i = (+ 1) % 256
    j = (+ S[i]) % 256
    S[i], S[j] = S[j], S[i]
    out.append(chr(ord(d) ^ S[(S[i] + S[j]) % 256]))

  print(''.join(out))


gf = galois.GF(2**3, irreducible_poly=0b1101)
conditions = gf(conditions)
results = gf(np.array(results).T)
plains = np.linalg.inv(conditions).dot(results)
reflect = [3, 5, 7, 11, 13, 17, 19, 21]
forward = [reflect[int(i)] for i in plains]

rc4(forward)
= {
    0b0: "int",
    0b1: "float",
    0b10: "const int",
    0b11: "const float",
    0b100: "int &",
    0b101: "float &",
    0b110: "const int &",
    0b111: "const float &"
}

types = []
for i in plains:
  types.append(m[int(i)])
print(', '.join(types))

得到 flag ACTF{y3s_my_nam3_i5_N4NagiIJfRKfS1_fRfKiS0_S0_S1_S2_S3_S1_RS3_ifS2_S2_RifS0_S1_S4_fS0_S4_ifS3_S0_S0_S2_iEE}

同时咱也把重命名后的 nagi.cpp 在 gist 上贴了一份,dalao 们复现的时候可以参考一下

[2022-ACTF-Nagi-knows-nagi.cpp (github.com)] https://gist.github.com/r4ve1/d4a5968592c920782ea32ead0700f6ef


推荐阅读:
Shiro 历史漏洞分析
WMI检测思路与实现
浅谈pyd文件逆向
CVE-2022-23222漏洞及利用分析
Mimikatz详细使用总结

跳跳糖是一个安全社区,旨在为安全人员提供一个能让思维跳跃起来的交流平台。


跳跳糖持续向广大安全从业者征集高质量技术文章,可以是漏洞分析,事件分析,渗透技巧,安全工具等等。
通过审核且发布将予以500RMB-1000RMB不等的奖励,具体文章要求可以查看“投稿须知”。
阅读更多原创技术文章,戳“阅读全文

原文始发于微信公众号(跳跳糖社区):神奇的 C++ 模板 —— 2022-ACTF-Nagi-Knows Writeup

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年7月13日00:08:08
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   神奇的 C++ 模板 —— 2022-ACTF-Nagi-Knows Writeuphttps://cn-sec.com/archives/1160921.html

发表评论

匿名网友 填写信息