首页
社区
课程
招聘
[原创]KCTF 2020 题目设计思路 & YANSOllvm 介绍
2020-3-24 23:19 17620

[原创]KCTF 2020 题目设计思路 & YANSOllvm 介绍

2020-3-24 23:19
17620

前言(灌水)

已经很久没有参加看雪 CTF 了. 主要原因还是能力有限: 想做攻击方却逆不出来题, 想做防守方又没有什么好点子, 还会被 riatre, ccfer 等诸多高手吊打, 可以说是十分惨烈了. 这一年补了补代数数论的课, 看了看密码学, 奈何脑子不好, 终究学了这个忘那个, 逆向能力早就被别人甩了好几个秒差距. 以前还能看到别人背影而兴叹, 如今连影子都看不到了.

 

好在看雪论坛里面学习资料不少, 把自己当萌新(划掉, 已经变成萌新了)在论坛里面找了些基础题目做做. 发现自己对于 OLLVM 之类的混淆还是非常头疼, 虽然这些年一直靠 bird 写的利用符号执行自动 patch 的脚本苟活, 但是如果把混淆稍微改改就又一脸懵逼了. 玩了玩 IDA 的 microcode 只感觉一知半解, 试了试 binary ninja 对于太大的函数又难以分析, 每次做题都像是在"启发式逆向", 闭着眼睛逆一点是一点, 颇感力不从心.

 

所以本着 "打不过它就加入它" 的思路, 想来看雪 CTF 里面出个 OLLVM 的题. 但是 OLLVM 毕竟已是将近 7 年的老技术了, 拿过来出题未免有点贻笑大方. 而且正好看到去年有一道 "叹息之墙" 似乎就是 OLLVM 的混淆, 结果被各路高手轮番攻破, 令在下胆战心惊. 可能正如前人所言, 要想在看雪 CTF 里面存活, 最好的办法就是把这些大佬们拉到一起聚个餐, 觥筹交错, 大醉三日而还.

 

但后来转念一想, 虽然 OLLVM 拦不住这些逆向机器, 但是这个在编译阶段混淆的思路还是挺不错的. 于是花了点时间研究了一下 LLVM, 实现了几个新的 Obfuscation Pass, 又写了一个 CrackMe, 用这些 Pass 混淆过后, 来参加今年看雪的春季赛, 且看究竟能撑过几天.

题目提交

题目通过看雪题库提交, 请管理员查收.

 

之前一直想做一个这样的 CrackMe:

  • 验证算法本身非常简单, 不是密码学/数学题
  • 使用某种通用的程序保护的方法对它进行混淆. 然后将混淆的方法开源, 并附详细说明(而不遮遮掩掩)
  • 混淆后的程序在看雪 CTF 中存活

这一次我在看雪题库提交的题目, 除了最后一点不敢保证, 但是前两点均完全满足. 之前虽然也想这样做, 但是因为题目设计思路和题目 flag 都在一个帖子内, 无法在比赛期间公开设计帖. 这次提交题目的通道是看雪题库, 故而可以和本帖分离. 所以希望管理员能在攻击方比赛开始的时候, 把本帖公开. 让大家可以提前了解题目中应用的混淆方法.

 

执行程序如同跋山涉水, 行路时阡陌纵横, 每一次分支都是一次歧路. 古人曾感慨 "行路难, 行路难. 多歧路, 今安在". 这一个程序所采用的混淆, 恰恰就是增加了许多歧路, 彻底解决 "行路难" 的问题 23333. 但是歧路多了, 走着走着可能就跟丢了, 正巧这次 CTF 赛题皆以十二生肖为名. 那我这个程序, 不如就叫 "歧路亡羊" 吧.

YANSOllvm

YANSOllvm 是在下这几天编写的一个类 OLLVM 项目, 它的全称是 Yet Another Not So Obfuscated LLVM, 源代码已公开到 Github 上. 记得在 2018 年看雪论坛上似乎发生过有人抄袭 Hikari Obfuscator 的争论, 当时觉得 OLLVM 的代码一定很高大上, 自己写起来多半困难重重. 这几天粗略上手试了一下, 感觉如果不考虑用户体验和各种实际代码的情况, 简单写写自己玩好像还挺容易的. 再后来我又发现, 除了 CFG Flattening, LLVM 上能做的变换实在是太多了, 于是我又自己实现了几个新的混淆 Pass, 抛砖引玉, 供大家开阔思路.

编译与使用

原则上来讲, OLLVM 的代码树完全可以不用包含 LLVM 和 Clang 的部分, 可是为什么 YANSOllvm 的编译需要 LLVM 呢? 一个原因是 YANSOllvm 实现了对 Calling Convention 的混淆, 而 Calling Convention 是 Backend 的约定, 所以需要对 LLVM backend 进行修改. 另外一个原因是 LLVM 有很多代码变换的 Utils, 这些 Utils 稍作修改就是一种不错的代码混淆.

 

编译的方法在 Github 上已经很详细了, 这里主要说一下本题中所使用的命令行参数:

  • clang -O1 -Wall -Wextra -c -emit-llvm pediy.c
  • ../bin/opt -load ../lib/LLVMObf.so -vm -merge -bb2func -flattening -obfZero -connect -obfZero -obfCall pediy.bc -o pediy.obf.bc
  • ../bin/llc -O3 --disable-block-placement --max-loads-per-memcmp=0 pediy.obf.bc
  • cat pediy.obf.s | grep -v ud2 > pediy.s
  • clang -Wall -Wextra -Wl,-nodefaultlib:libcmt -lmsvcrt pediy.s

Pass 介绍

我们以如下源代码为例, 后面加上每一个 pass 的效果图:

#include <stdio.h>

short zero[2] = {0,1};
static short *d(void){
  return zero;
}
static short c(int x){
  if(x == 0)
    return (*(d()+1) ^ 12);
  return c(x-1)+1;
}

static int b(int x){
  int sum = 0;
  for(int i = 0; i < x; i++){
    sum += c(i);
  }
  return sum;
}

static void a(unsigned long long x){
  for(int i = 0; i < x; i++){
    int temp = b(i) + 1;
    printf("%d ", temp);
  }
}

int main(int argc, char *argv[]){
  int i;
  if(argc > 1){
    sscanf(argv[1], "%d", &i);
    a(i);
  }
  return 0;
}

VM

VM Pass 非常简单, 就是将 LLVM IR 中的一些简单二元操作替换成函数调用. 比如将 a = b + c 替换成 a = Add(b, c). 上述代码混淆后的效果图:
vm

Merge

Merge 的主要作用是将所有 internal linkage 的函数 (比如 static 函数, 和 VM Pass 添加的 vm 函数等) 合并成一个新的函数. 这样做的性能开销并不大, 但是会形成一个大型函数从而可以进一步混淆, 并且大幅拖慢反编译器的速度. 下图中把 abcd 四个函数合并成了同一个函数:
merge

bb2func

这个 Pass 的作用正好和 Merge 相反, 它可以从一个函数中把较大的 Basic Block 切分, 并且强行提取出来作为新的函数. 因为一般而言, 函数是某个独立的功能单位, 而这样随机抽取的函数往往只是一个功能单位的某一部分, 从而干扰逆向者与反编译器. 并且配合文末介绍的 ObfCall Pass 可以进一步干扰 IDA 的分析.
bb2func

Flattening

这个 Flattening 是在 OLLVM 原有的 flattening pass 的基础上进行的修改. OLLVM 的 CFG 平坦化存在一个明显问题: 每一个 basic block 都直接暴露了其后继的 basic block. 抽象来看, Basic Block 都对应着一个 internal state, 而 Switch Dispatcher 的分发则依赖 switchVar. 在 OLLVM 的平坦化算法中, internal state 就等同于 switchVar, 并且 internal state 的转换则通过直接赋值而进行, 这样做会使得静态分析变得尤其容易.

 

YANSOllvm 将 internal state 和 switchVar 分别看待. switchVar 由 internal state 的 hash 计算得出, 而 internal state 之间的转移则通过 xor 来完成. 这样就能够避免攻击者直接通过静态分析从一个 basic block 获取到其后继的所有 basic block.
flattening

obfZero

这个 Pass 的作用很简单, 利用复杂的恒等式 (如 MBA) 将 IR 中的常量 0 替换成几个变量的运算结果. 从而干扰分析器, 配合 Flattening 和 Connect Pass 可以产生很多虚假分支.
obfCon
可以看到图中将原有的 if(x == 0) 中的 0 混淆成关于 x 的表达式

Connect

Connect 将 Basic Block 切开, 然后通过 switch 来链接他们. 正确的分支自然只有一个, 其他的虚假分支通过 obfZero 来混淆. 并且会在其中一个虚假分支里插入一些简陋的花指令来干扰反汇编:
connect
注意到 a 并没有被成功识别为函数:
patch 掉几个干扰分析的花指令后, CFG 比原来复杂很多, 但是大部分分支是无用的:
connect_patched

obfCall

在编译时, genObfCC.py 会随机生成 10 个 Calling Convention, 他们的返回寄存器和传参寄存器均不为 AMD64 ABI 中对应的常用寄存器, 从而混淆反汇编器的分析.
obfCall

保护全开

full_protect

附件提供了上述示例源代码在 YANSOLLVM 打开所有混淆下编译的 DEMO (包括 PE 和 ELF), 使用的混淆参数略有不同, 但是可作为简单的参考

尾声

写了一大篇帖子好累 orz. YANSOllvm 只是一个兴趣之作, 甚至可以说仅为此次 CTF 而生, 仅供学习参考. 很多情况并没有考虑到, 代码质量极差, 也有许多 bug, 希望大家多多谅解. 最后, 祝大家看雪 CTF 玩得愉快~~

Update

比赛结束后的更新. 先看题目源代码:

// Small tricks prepared for pizza XD
static char **stolen_input[2];

#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include <Windows.h>
#include <winternl.h>

#define rotr64(w, c) (((w)>>c)|((w)<<(64-c)))
#define rotr32(w, c) (((w)>>c)|((w)<<(32-c)))

#define G(r,i,a,b,c,d)                      \
    do {                                    \
        a = a + b + m[sigma[r%10][2*i+0]];  \
        d = rotr64(d ^ a, 32);              \
        c = c + d;                          \
        b = rotr64(b ^ c, 24);              \
        a = a + b + m[sigma[r%10][2*i+1]];  \
        d = rotr64(d ^ a, 16);              \
        c = c + d;                          \
        b = rotr64(b ^ c, 63);              \
    } while(0)

#define Q(a,b,c,d)                          \
    do {                                    \
        a ^= b; d = rotr64(d + a, 48);      \
        c ^= d; b = rotr64(b + c, 44);      \
        a ^= b; d = rotr64(d - a, 8);       \
        c ^= d; b = rotr64(b - c, 7);       \
    } while(0)

#define D(a,b,c,d)                          \
    do {                                    \
        a -= b; d = rotr32(d ^ a, 12);      \
        c -= d; b = rotr32(b ^ c, 13);      \
        a -= b; d = rotr32(d ^ a, 8);       \
        c -= d; b = rotr32(b ^ c, 7);       \
    } while(0)

_Check_return_
_CRT_STDIO_INLINE int __CRTDECL fake_scanf(
    _In_z_ _Scanf_format_string_ char const* const _Format,
    ...);

_Check_return_opt_
_CRT_STDIO_INLINE int __CRTDECL fake_vfscanf_l(
    _Inout_                       FILE*       const _Stream,
    _In_z_ _Printf_format_string_ char const* const _Format,
    _In_opt_                      _locale_t   const _Locale,
                                  va_list           _ArgList
    );

// Carefully chosen
#define DELAY_THRESHOLD 471925ULL

const uint8_t sigma[10][16] =
{{9, 14, 3, 13, 15, 6, 1, 2, 5, 4, 12, 10, 0, 8, 11, 7},
 {11, 12, 15, 5, 4, 7, 8, 1, 14, 0, 9, 3, 10, 2, 6, 13},
 {10, 5, 0, 9, 6, 3, 7, 8, 12, 11, 13, 1, 2, 14, 4, 15},
 {2, 4, 13, 14, 8, 0, 10, 11, 3, 1, 6, 12, 15, 9, 7, 5},
 {4, 9, 6, 2, 3, 15, 12, 7, 11, 14, 10, 0, 1, 5, 13, 8},
 {3, 0, 1, 12, 9, 10, 5, 13, 15, 8, 2, 6, 7, 11, 14, 4},
 {0, 6, 14, 7, 11, 8, 15, 12, 9, 2, 1, 13, 4, 3, 5, 10},
 {8, 10, 2, 11, 0, 14, 13, 4, 6, 9, 7, 15, 5, 1, 3, 12},
 {1, 7, 11, 4, 10, 13, 9, 5, 0, 3, 8, 2, 14, 15, 12, 6},
 {12, 3, 5, 15, 2, 1, 14, 6, 7, 10, 4, 11, 13, 0, 8, 9}};

const uint64_t dest_hash[2] = {0x77c82a232d423c47ULL, 0xb417841123b531eULL};

uint64_t m[16];
uint64_t v[16] = {
    0x9fa0ff0b30fb40d4ULL, 0x3f258c7a6beccd2fULL,
    0x9c004dd31e213f2fULL, 0xcf9fc9496003e540ULL,
    0x88bbbdb5bfd4af27ULL, 0x98d09675e2034090ULL,
    0x15c361d26e63a0e0ULL, 0x22d4ff8ec2e7661dULL,
    0xc07fd05928683b6fULL, 0x775f50e2ff2379c8ULL,
    0xdf2f865643c340d3ULL, 0xa2d2bd2d887ca41aULL,
    0x346c4819a1c9e0d6ULL, 0x22540f2f61b76d87ULL,
    0xaa54166b2abe32e1ULL, 0xa2d341d022568e3aULL
};

// Useless anti-debug, nobody will get into this
// TODO: Better and more stealthy, and add another fake path
static int check_debugger(void){
    PPEB pPeb = (PPEB)__readgsqword(0x60);
    return pPeb->BeingDebugged == 1;
}

// REALLY USELESS
__attribute__((optnone)) static void useless(uint32_t* d, int times){
    for(int i = 0; i < times; i++){
        d[0] ^= 0x984f019a;
        d[2] ^= 0xfb272947;
        d[1] ^= 0x8561bef9;
        d[3] ^= 0xc9e9611f;
    }
}

// THE FIEND
__attribute__((optnone)) static int true_check(uint64_t x, uint64_t y){
    union {
        uint8_t b[16];
        uint16_t w[8];
        uint32_t d[4];
        uint64_t q[2];
    } data = {.b = {0}}, temp = {.q = {x, y}};
    for(int i = 0; i < 2; i++){
        // One can still solve this by MITM or Birthday attack if he doesn't know how to do the math
        if(temp.b[i*8+3]&32){data.q[i]^=0x3796f61d3f496d9aULL;}else{data.q[i]^=0x4d09af3abca28a8dULL;} if(temp.b[i*8+2]&64){data.q[i]^=0xfa62ce8ff9d33901ULL;}else{data.q[i]^=0x9fe10506a2c9a9aULL;} if(temp.b[i*8+5]&1){data.q[i]^=0xc4f9550241fdffa3ULL;}else{data.q[i]^=0xd2cae855c068e1e1ULL;} if(temp.b[i*8+2]&32){data.q[i]^=0x8acdd6e445efbd97ULL;}else{data.q[i]^=0x8e86479f6e6a694ULL;} if(temp.b[i*8+4]&8){data.q[i]^=0x30a83415d047fb98ULL;}else{data.q[i]^=0x6a4bd5f20efd8499ULL;} if(temp.b[i*8+6]&4){data.q[i]^=0x73957581242c53dULL;}else{data.q[i]^=0xa0449a4df2c0f524ULL;} if(temp.b[i*8+3]&1){data.q[i]^=0xaf82609de0aec05cULL;}else{data.q[i]^=0x61b1ee4c89a1c74aULL;} if(temp.b[i*8+0]&8){data.q[i]^=0xad063dbeb266af43ULL;}else{data.q[i]^=0x3e2a0cf5afc54669ULL;} if(temp.b[i*8+7]&32){data.q[i]^=0x435068f420fa4ff0ULL;}else{data.q[i]^=0xbcc700ead1995e75ULL;} if(temp.b[i*8+7]&4){data.q[i]^=0xce6c8c612bd1e439ULL;}else{data.q[i]^=0x281876d27fdd03c6ULL;} if(temp.b[i*8+2]&4){data.q[i]^=0x1d3d3c45d52394cfULL;}else{data.q[i]^=0x601a1255813adf30ULL;} if(temp.b[i*8+6]&128){data.q[i]^=0x1fa5d059c60aa3e3ULL;}else{data.q[i]^=0xf507e8ac585a4e42ULL;} if(temp.b[i*8+6]&2){data.q[i]^=0x3c4d092d773b3a2eULL;}else{data.q[i]^=0xf81bb1d3980a2fa4ULL;} if(temp.b[i*8+0]&16){data.q[i]^=0x97bf010ccff099f9ULL;}else{data.q[i]^=0x18e55a56ca0ad2c7ULL;} if(temp.b[i*8+1]&1){data.q[i]^=0x5c35272c4834ad4dULL;}else{data.q[i]^=0xaad9cfc2182beba0ULL;} if(temp.b[i*8+7]&16){data.q[i]^=0x8a18f8556f480632ULL;}else{data.q[i]^=0xeee198aabdbe8a18ULL;} if(temp.b[i*8+2]&1){data.q[i]^=0x1a9b941774f6cdf4ULL;}else{data.q[i]^=0x63ff598fda7e7d6fULL;} if(temp.b[i*8+3]&8){data.q[i]^=0x3c73b45ae0cdba4ULL;}else{data.q[i]^=0xe15ce23df925822aULL;} if(temp.b[i*8+0]&2){data.q[i]^=0xb93d7864763e24e6ULL;}else{data.q[i]^=0xbc36e215b5689224ULL;} if(temp.b[i*8+5]&64){data.q[i]^=0x6a0ecdebb77cd18fULL;}else{data.q[i]^=0xb7800c9ebae07702ULL;} if(temp.b[i*8+4]&16){data.q[i]^=0x69295501be7ec046ULL;}else{data.q[i]^=0x80931fc6d227f8dcULL;} if(temp.b[i*8+4]&64){data.q[i]^=0x7a530dc89a3fcd12ULL;}else{data.q[i]^=0x3f8df570b658b85dULL;} if(temp.b[i*8+1]&32){data.q[i]^=0x253e5d6e09849a46ULL;}else{data.q[i]^=0x7ba207cb52f24e88ULL;} if(temp.b[i*8+1]&4){data.q[i]^=0xe6de159244d58711ULL;}else{data.q[i]^=0x6a633f82966e82abULL;} if(temp.b[i*8+1]&128){data.q[i]^=0xd1245d0e166d6484ULL;}else{data.q[i]^=0x75e0b5660623690fULL;} if(temp.b[i*8+0]&4){data.q[i]^=0x88520272cc6e4a8dULL;}else{data.q[i]^=0xcc9148b45107b5c9ULL;} if(temp.b[i*8+6]&1){data.q[i]^=0x5f78d84d7401f1b9ULL;}else{data.q[i]^=0x586b216ef43ad48eULL;} if(temp.b[i*8+0]&128){data.q[i]^=0x821447502d8f83a5ULL;}else{data.q[i]^=0xa96b5a8aff1878f3ULL;} if(temp.b[i*8+6]&32){data.q[i]^=0x5c9d9ee1f131c160ULL;}else{data.q[i]^=0x87762db8dce9b73cULL;} if(temp.b[i*8+6]&16){data.q[i]^=0xece764a468850efULL;}else{data.q[i]^=0x6fa7015e6bb367b6ULL;} if(temp.b[i*8+7]&8){data.q[i]^=0xc4769184600cf71ULL;}else{data.q[i]^=0xbfd039b8b4f29c94ULL;} if(temp.b[i*8+2]&128){data.q[i]^=0xcc566b2c807d1b84ULL;}else{data.q[i]^=0x7ee7e8fd8040bd86ULL;} if(temp.b[i*8+5]&2){data.q[i]^=0x4dc8afa3b4485576ULL;}else{data.q[i]^=0x5a0ce9d5d3af4435ULL;} if(temp.b[i*8+6]&8){data.q[i]^=0x9d73ea268c866ac8ULL;}else{data.q[i]^=0xbb3113e0107adedcULL;} if(temp.b[i*8+1]&16){data.q[i]^=0x8133d136d4f81831ULL;}else{data.q[i]^=0x3e7ffb6f3748ae83ULL;} if(temp.b[i*8+4]&128){data.q[i]^=0x1f3c37467929918bULL;}else{data.q[i]^=0xa1f7bf0929977159ULL;} if(temp.b[i*8+6]&64){data.q[i]^=0x9c2bca2ea39c691fULL;}else{data.q[i]^=0xc269314ac1fea8e7ULL;} if(temp.b[i*8+5]&16){data.q[i]^=0xd69f4d2fc2d45b9eULL;}else{data.q[i]^=0x5c064c38f21bc241ULL;} if(temp.b[i*8+0]&32){data.q[i]^=0xd5b60f964288fd32ULL;}else{data.q[i]^=0x120d6129a85d8e4bULL;} if(temp.b[i*8+4]&32){data.q[i]^=0xe9e70afed5ee6cbfULL;}else{data.q[i]^=0xfca8b3ee674f2565ULL;} if(temp.b[i*8+3]&128){data.q[i]^=0xa45472c49bed802fULL;}else{data.q[i]^=0x97c2f6a547610c57ULL;} if(temp.b[i*8+5]&8){data.q[i]^=0x4549c58141a7ccc9ULL;}else{data.q[i]^=0x1619a76f4ebe3d6ULL;} if(temp.b[i*8+4]&4){data.q[i]^=0x4659fd56784637a8ULL;}else{data.q[i]^=0x508180c897ba2fc6ULL;} if(temp.b[i*8+1]&2){data.q[i]^=0xab69d618d946ffaULL;}else{data.q[i]^=0x9e6749482573b96dULL;} if(temp.b[i*8+7]&1){data.q[i]^=0x49f2759549998302ULL;}else{data.q[i]^=0x3ff6cc85c6a56601ULL;} if(temp.b[i*8+7]&128){data.q[i]^=0xbfc400dfef2928c8ULL;}else{data.q[i]^=0x7f9a7ae568ebffb8ULL;} if(temp.b[i*8+1]&64){data.q[i]^=0xfa1507576a21b1aeULL;}else{data.q[i]^=0xbfeff562ce0d5d58ULL;} if(temp.b[i*8+5]&4){data.q[i]^=0x381ba1bd97727cddULL;}else{data.q[i]^=0x294b87e2897091d2ULL;} if(temp.b[i*8+7]&2){data.q[i]^=0x2af20c4b4d98cf16ULL;}else{data.q[i]^=0x1c117ba895f600eeULL;} if(temp.b[i*8+0]&64){data.q[i]^=0xa5141f6dde5be4f0ULL;}else{data.q[i]^=0xbfbe146e10193b6eULL;} if(temp.b[i*8+7]&64){data.q[i]^=0x2bd13515c74a6b36ULL;}else{data.q[i]^=0x8ab612550aa8e1abULL;} if(temp.b[i*8+3]&16){data.q[i]^=0x584603b14f9c07beULL;}else{data.q[i]^=0xdcc914bed9036f0dULL;} if(temp.b[i*8+3]&64){data.q[i]^=0x404cec02bc8b778aULL;}else{data.q[i]^=0xba1343a95d820ba9ULL;} if(temp.b[i*8+2]&16){data.q[i]^=0xb56620e4e50ed47cULL;}else{data.q[i]^=0x2f55690a4cacca44ULL;} if(temp.b[i*8+5]&128){data.q[i]^=0x79467c2907b00174ULL;}else{data.q[i]^=0x5b57ce14daca37fcULL;} if(temp.b[i*8+2]&8){data.q[i]^=0xf6ba88d86fe38a7fULL;}else{data.q[i]^=0x29d2bff018b00740ULL;} if(temp.b[i*8+5]&32){data.q[i]^=0x7c592711e4673a1eULL;}else{data.q[i]^=0xa8a8ff75703dd709ULL;} if(temp.b[i*8+4]&2){data.q[i]^=0x32252e609065990aULL;}else{data.q[i]^=0xf587aaef1f9516fULL;} if(temp.b[i*8+2]&2){data.q[i]^=0xad8e364386cba8d4ULL;}else{data.q[i]^=0xf50617b128a0071eULL;} if(temp.b[i*8+3]&2){data.q[i]^=0xce5280d041f19aaaULL;}else{data.q[i]^=0xfbd4fd51cea9d12bULL;} if(temp.b[i*8+4]&1){data.q[i]^=0xfb738cefcb4ebe76ULL;}else{data.q[i]^=0x7e1f54e20afc1cd9ULL;} if(temp.b[i*8+3]&4){data.q[i]^=0xa44396f44f4b69b8ULL;}else{data.q[i]^=0x90148276bf1e5d49ULL;} if(temp.b[i*8+0]&1){data.q[i]^=0x717b237316b0728ULL;}else{data.q[i]^=0x527ea699de716460ULL;} if(temp.b[i*8+1]&8){data.q[i]^=0xa2d352ba607243f5ULL;}else{data.q[i]^=0x34f21bfc6d7943b3ULL;}
       temp.q[i] = 0;
    }
    if(!check_debugger()){
        // Add an algorithm similar to previous fake ones to make reversers not so sad about their efforts in vain
        D(data.d[0],data.d[1],data.d[2],data.d[3]);
    }
    for(int i = 0; i < 16; i++){
        uint64_t tick;
        for(int j = 0; j < 8; j++){
            // A trivial implementation of implicit dataflow
            // TODO: Make it more like an clumsy anti-debug
            tick = __rdtsc();
            if(data.b[i]&(1<<j)){
                // Annoying branch and will trigger hardware breakpoints uselessly
                useless(temp.d, data.b[i] > 0x7f ? 126: 132);
            }
            tick = __rdtsc() - tick;
            if(tick < DELAY_THRESHOLD){
                int ij = i*8+j;
                int index = ij;
                if(ij%127){
                    index = 1;
                    for(int k = 0; k < i*8+j; k++){
                        index = (index*3)%127;
                    }
                }
                temp.b[index/8] ^= 1<<(index%8);
            }
        }
    }
    // Reveal Entire Map
    // KCTF{BL4ckSH33p}WA11_}
    if((temp.b[15]^temp.b[9])==8){if((temp.b[9]^temp.b[12])==131){if((temp.b[12]^temp.b[11])==155){if((temp.b[11]^temp.b[2])==115){if((temp.b[2]^temp.b[10])==146){if((temp.b[10]^temp.b[5])==147){if((temp.b[5]^temp.b[3])==95){if((temp.b[3]^temp.b[6])==186){if((temp.b[6]^temp.b[4])==55){if((temp.b[4]^temp.b[14])==161){if((temp.b[14]^temp.b[7])==155){if((temp.b[7]^temp.b[0])==216){if((temp.b[0]^temp.b[8])==95){if((temp.b[8]^temp.b[13])==37){if((temp.b[13]^temp.b[1])==124){if(temp.b[1]==52){return 1;}}}}}}}}}}}}}}}}
    return 0;
}

// No one can reverse this
static void secure_hash(void){
    for(int r = 0; r < 16; r++){
        for(int i = 0; i < 4; i++){
            G(r,i,v[i%4],v[i+4],v[i+8],v[i+12]);
        }
        for(int i = 4; i < 8; i++){
            G(r,i,v[i%4],v[(i-3)%4+4],v[(i-2)%4+8],v[(i-1)%4+12]);
        }
    }
}

// Not totally fake
static int check(char *in){
    if(memcmp(in, "KCTF{", 5) || memcmp(&in[15], "}", 2)){
        goto wrong;
    }
    for(int i = 0; i < 16; i++){
        m[i] = in[i] ^ 0x55aa55aa55aa55aaULL;
    }
    for(int i = 0; i < 4; i++){
        Q(m[4*i],m[4*i+1],m[4*i+2],m[4*i+3]);
    }
    //TODO: Unicorn/Angr invisible
    char ***__stolen_input = (char ***)((((uint64_t)stolen_input << 4) + 0x80) >> 4);
    if(*__stolen_input){
        if(*(*__stolen_input+1) == **__stolen_input - 0x10){
            // KCTF{ xxxx xxxx xx}x xxxx }\n
            if(*(uint16_t*)(*(*__stolen_input+1) + 21) == 0xa7d){
                if(true_check((*(uint64_t*)(*(*__stolen_input+1) + 5) * 0x49ba1eefbcfa13ffULL),
                              (*(uint64_t*)(*(*__stolen_input+1) + 13)) * 0xdae79ebc6e26e62bULL))
                    // Surprise !!!
                    goto right;
            }
        }
    }
    secure_hash();
    if(memcmp(v, dest_hash, sizeof(dest_hash))){
        // Don't give up bro! Try harder at this dead end. XD
        goto wrong;
    }else{
        // With possibility less than 2^-58 to success
        goto right;
    }
right:
    return 1;
wrong:
    return 0;
}

int main(){
    char in[17];
    printf("Please input your flag: ");
    if(fake_scanf("%16s", in) < 0){
        printf("Error read input\n");
        return 1;
    }
    printf("Checking... ");
    if(check(in)){
        printf("Wow, you've got the correct flag!!!!!\n");
    }else{
        printf("Sorry, maybe you should try harder.\n"); // XD
    }
    return 0;
}

_Check_return_
_CRT_STDIO_INLINE int __CRTDECL fake_scanf(
    _In_z_ _Scanf_format_string_ char const* const _Format,
    ...)
{
    int _Result;
    va_list _ArgList;
    __crt_va_start(_ArgList, _Format);
    _Result = fake_vfscanf_l(stdin, _Format, NULL, _ArgList);
    __crt_va_end(_ArgList);
    return _Result;
}

_Check_return_opt_
_CRT_STDIO_INLINE int __CRTDECL fake_vfscanf_l(
    _Inout_                       FILE*       const _Stream,
    _In_z_ _Printf_format_string_ char const* const _Format,
    _In_opt_                      _locale_t   const _Locale,
                                  va_list           _ArgList
    )
{
    // TODO: Still TOOOOOOOOO obvious for dalao
    stolen_input[1] = (char **) _Stream;
    return __stdio_common_vfscanf(
        _CRT_INTERNAL_LOCAL_SCANF_OPTIONS,
        (FILE*)stolen_input[1], _Format, _Locale, _ArgList);
}

设计思路

本题主要是 YANSOllvm 的应用, 题目本身非常简单. 从设计思路上来讲, 总结了前人以 OLLVM 出题而失败的经验: 只混淆了控制流, 而没有混淆数据流. 攻击者只需要几个内存访问断点, 即可定位核心逻辑.

 

故而, 本题设计上, 将数据流转化为控制流, 而控制流本身即被 YANSOllvm 所混淆. 比如核心代码里的一堆 if 语句通过判断输入的某个 bit 来进行运算, 以及根据 bit 的值运行不同快慢的函数, 通过 rdtsc 来窃取数据的方法 (即所谓 implicit data flow).

 

并且, 为了进一步防止攻击者找到正确代码, 题目里的 main 函数通过调用一个修改过的 scanf("%16s", s) 来迷惑攻击者, 使得攻击者以为 flag 仅有 16 字节. 接下来校验的代码中, 甚至会比对第 16 个字节是否为 '}', 进一步迷惑攻击者, 使其得到错误的长度信息. 而实际上, 对于错误长度的输入, 是通过一个安全的 hash 算法 (改编自 blake2b) 来验证的, 故而不可能逆向得出合法 flag.

 

但是, 在修改过的 scanf 中, 程序会将 stdin 的值放到一个全局变量中. check 函数会根据 stdin 的内部结构 (取决于 ucrt), 直接读取 stdin buffer 中的数据. 从而不通过 scanf/getchar 等函数, "窃取"到用户的输入, 判断长度正确后再进入真正验证流程. 真正的验证流程是一个 GF(2) 下的矩阵向量乘, 一个改版的 Chacha, 和一个简单的置换. 均可逆而无需爆破.

 

本题最为有趣之处在于, 因为难以有效地逆向控制流, 很多攻击者可能到比赛结束才会发现, 自己连 flag 长度都没找对.

赛后感言

首先, 感谢 ccfer 和 pizza 两位大佬愿意赏光做这道题目. 大佬们速度太恐怖了, 如果不是里面有一个大坑, 可能第一天就被干死了. (一人血书求 writeup)

 

这道题出出来, 主要还是为了好玩. 时间有点匆忙没有太认真打磨细节. 很多点都还处于一个类似于 proof of concept 的样子. 比如 rdtsc 那个部分, 应该继续做的隐蔽一点. anti debug 也只用的最简单的方法, 可能根本不会有人撞上去. 最可惜的可能是利用 scanf 把 stdin 存到了全局变量里. 本来这里应该是非常隐蔽的, 但是有经验的攻击者一看到 scanf 里面无故调用了一个全局变量, 试着下一个硬件断点就能发现. 本来理想的情况应该是稍稍 patch 一下二进制, 在 main 函数之前就放进去的. 但是已经答应了大家公开这题人工处理的部分, 所以没有这样做. 而在 check 函数里调用 API 也会变得非常可疑, 所以只能顺便塞到 scanf 里了.

 

还有一些遗憾, 比如只设计了一个坑, 也许利用 anti debug 之类的设计更多的假路, 或者设计出非常像假路的真路, 虚虚实实会更好玩. 不过担心会被骂也就没有实现. 并且, 在 memcmp 两个 hash 之后, 应该再 check 一下输入的后一个字节是不是回车, 也就不会有多解的疑惑了.

 

最后, 本题其实最想告诉大家的是类 ollvm 混淆器的 "正确" 用法. 控制流无论再怎么混淆, 数据的变换依然未曾改变. 所以第一步是将数据的变换尽量编码进控制流之中. 其次, 在静态反混淆难以进行的情况下, 大家会使用类似于 Concolic Execution 的方法, 与输入无关的控制流路径会被忽略. 一个非常小的 trick 往往就能让人步入歧路. 这就是所谓的 "歧路亡羊" 吧.


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2020-6-20 14:18 被半盲道人编辑 ,原因:
上传的附件:
收藏
点赞2
打赏
分享
最新回复 (20)
雪    币: 1610
活跃值: (548)
能力值: ( LV12,RANK:382 )
在线值:
发帖
回帖
粉丝
半盲道人 3 2020-4-13 12:31
2
0

几点声明:

  • 基于 LLVM 的编译时混淆和传统的 VM 代码虚拟化技术是两回事, 不可混为一谈
  • 本项目除了 Flattening 是基于 OLLVM 的改进, 和 ObfZero 的一小部分代码来自 Quarkslab 以外, 其他所有混淆算法及代码均为原创, 作者从未在任何基于 LLVM 的 Obfuscator 或其他 VM 软件中见到类似算法
  • 具体版权信息见 github repo 下的 README, 整体工程以 GPLv3 协议发布
最后于 2020-4-13 12:38 被半盲道人编辑 ,原因:
雪    币: 16154
活跃值: (5941)
能力值: ( LV13,RANK:861 )
在线值:
发帖
回帖
粉丝
大帅锅 4 2020-4-15 12:37
3
0
我第一个来膜拜的!
最后于 2020-4-15 12:37 被大帅锅编辑 ,原因:
雪    币: 4618
活跃值: (1727)
能力值: ( LV12,RANK:392 )
在线值:
发帖
回帖
粉丝
全盲法师 3 2020-4-15 12:46
4
0
我第二个来膜拜的!
雪    币: 7366
活跃值: (931)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
wqsemc 2020-4-15 12:54
5
0
俺第三个来膜拜的
雪    币: 17780
活跃值: (60083)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2020-4-15 14:14
6
0
我是第四个来膜拜的!
雪    币: 8188
活跃值: (4243)
能力值: ( LV15,RANK:2459 )
在线值:
发帖
回帖
粉丝
ccfer 16 2020-4-15 14:21
7
0
这是嫌只虐大家4天不过瘾,要半个月起步吗
雪    币: 163
活跃值: (79)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
Exploring 2020-4-15 14:22
8
0
我是第五个来膜拜的!
雪    币: 3507
活跃值: (17964)
能力值: ( LV12,RANK:277 )
在线值:
发帖
回帖
粉丝
0x指纹 5 2020-4-15 14:31
9
0
我是第六个来膜拜的!
雪    币: 4225
活跃值: (8385)
能力值: ( LV9,RANK:181 )
在线值:
发帖
回帖
粉丝
nevinhappy 2 2020-4-15 14:45
10
0
我是第七个来膜拜的!
雪    币: 5233
活跃值: (3255)
能力值: ( LV10,RANK:175 )
在线值:
发帖
回帖
粉丝
挤蹭菌衣 1 2020-4-17 20:49
11
0
我是第八个来膜拜的! 
雪    币: 83
活跃值: (1037)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
killpy 2 2020-4-19 03:23
12
0
自己实现了 VM     Merge     bb2func     flattening     obfzero     Connect     obfcall 七个混淆功能 有句话没搞懂 你说为啥用llvm 因为他对调用约定混淆了 所以需要修改它 啥意思
雪    币: 1610
活跃值: (548)
能力值: ( LV12,RANK:382 )
在线值:
发帖
回帖
粉丝
半盲道人 3 2020-4-19 12:16
13
0
killpy 自己实现了 VM Merge bb2func flattening obfzero Connect obfcall 七个混淆功能 有句话没搞懂 你说为啥 ...
我解释的是为什么用我这个混淆需要编译 LLVM, 因为如果不改 backend 的话是可以只用编译成 LLVM 的插件就好了. 编译一次 LLVM 太慢所以我解释了一下. 至于我为什么基于 LLVM 做, 是因为不用自己分析控制流等逻辑, 基于 LLVM 还能用到很多很方便的数据结构, 不需要自己造轮子.
雪    币: 4752
活跃值: (2923)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
LeadroyaL 1 2020-4-19 17:08
14
0
思路不错,学习了
雪    币: 17780
活跃值: (60083)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2020-4-24 11:06
15
0
这是 第九题 歧路亡羊 设计思路,大侠可以提前研究下:
https://ctf.pediy.com/game-season_fight-143.htm
雪    币: 5568
活跃值: (3016)
能力值: ( LV12,RANK:394 )
在线值:
发帖
回帖
粉丝
htg 4 2020-5-1 12:09
16
0
我是来膜拜了,即使开源了,还是看不懂。
雪    币: 6839
活跃值: (3704)
能力值: ( LV13,RANK:1650 )
在线值:
发帖
回帖
粉丝
xym 4 2020-5-4 17:40
17
0


楼主的flag我一开始测试失败,还以为是自己复制错了,于是反复尝试,发现程序后缀为bak的时候失败次数很高,不知道是什么原因。

好吧,继续测试,发现和后缀也没有关系。就是总提示失败。。。也经常成功。。。

最后于 2020-5-4 17:47 被xym编辑 ,原因:
雪    币: 6435
活跃值: (436)
能力值: ( LV12,RANK:831 )
在线值:
发帖
回帖
粉丝
Riatre 8 2020-5-4 18:20
18
0
xym 楼主的flag我一开始测试失败,还以为是自己复制错了,于是反复尝试,发现程序后缀为bak的时候失败次数很高,不知道是什么原因。好吧,继续测试,发现和后缀也没有关系。就是总提示失败。。。也经常成功。。。
因为楼主的程序里构造 implicit data flow 的时候是用的测量计算所消耗的时间带来的 covert channel,而且是写死的 threshold 而不是现场训练的,机器配置不同或者在不同的运行环境下(比如在虚拟机里)的话很容易出现不稳定的情况。
雪    币: 6839
活跃值: (3704)
能力值: ( LV13,RANK:1650 )
在线值:
发帖
回帖
粉丝
xym 4 2020-5-4 19:26
19
0
Riatre 因为楼主的程序里构造 implicit data flow 的时候是用的测量计算所消耗的时间带来的 covert channel,而且是写死的 threshold 而不是现场训练的,机器配置不同或者在 ...
明白了,难怪跑的结果老是变,看来机器不好还真做不出来了。
雪    币: 1610
活跃值: (548)
能力值: ( LV12,RANK:382 )
在线值:
发帖
回帖
粉丝
半盲道人 3 2020-5-4 19:33
20
0
xym 楼主的flag我一开始测试失败,还以为是自己复制错了,于是反复尝试,发现程序后缀为bak的时候失败次数很高,不知道是什么原因。好吧,继续测试,发现和后缀也没有关系。就是总提示失败。。。也经常成功。。。
大爷已经说的很清楚了. 这个 rdtsc 的确不太稳定, 不过测试的大部分机器都是没问题的. 看这个样子感觉你的运行速度正好在这个 threshold 的边缘, 所以要么你比我降频到 3.0 GHz 的 6700K 差不多快两倍, 要么你这个机器慢到有点过分. 这里大胆猜测一下是虚拟机 (
游客
登录 | 注册 方可回帖
返回