July 21, 2022

ACTF2022-Inflated

Inflated!!!

  1. C++异常化处理
  2. OLLVM-控制流平坦化
  3. Two Puzzles

Exception

一般碰到C++异常逆向,确定了异常分发、处理部分,直接把call throw改为jmp catch块,再F5即可

PS: 多个catch块根据rdx来当为异常处理数值决定哪个为对应的catch块

(关于以上,这篇讲的很详细)

https://4nsw3r.top/2022/02/03/SCTF-REVERSE-CplusExceptionEncrypt-%E8%B5%9B%E5%90%8E%E5%A4%8D%E7%8E%B0/#Clang-x64

然而,这题没这么简单,套了个ollvm!?基于异常处理的ollvm,无论从哪个角度都没法使用之前的老套路

耐心看完这两篇文章就会有所收获,对于此题的被异常处理搞乱掉的cfg就会有所理解

https://www.cnblogs.com/catch/p/3604516.html

https://www.cnblogs.com/catch/p/3619379.html

OLLVM

要是平常的ollvm都可以按照这篇来解决

https://bluesadi.github.io/0x401RevTrain-Tools/angr/10_%E5%88%A9%E7%94%A8angr%E7%AC%A6%E5%8F%B7%E6%89%A7%E8%A1%8C%E5%8E%BB%E9%99%A4%E6%8E%A7%E5%88%B6%E6%B5%81%E5%B9%B3%E5%9D%A6%E5%8C%96/

其他的原理讲的非常好,问题是这题并不是那么简单,但为了去ollvm我们的思路也是一样的,所以要对ollvm的cfg熟悉,并懂得我们该如何恢复一个被ollvm混淆后的代码

现在就开始写我对这题的看法!

参考Write up:

https://github.com/Lnkvct/CTF-for-Fun/blob/main/Challenges/Inflated-ACTF2022/writeup.md

https://www.cnblogs.com/FW-ltlly/p/16472171.html

lchild师傅的Write up(pdf所以没法给链接)

0x00 日常查壳

(感觉好久没写wp了)

无壳64位

image-20220720122120679

0x01 CFG

GETC

在讲这题ollvm与异常处理之前,有必要先搞懂我们到底是怎么输入的

一共有三处getc处理我们第一段输入的地方

1
2
3
407629
40553A(专门用来处理箭头)
405676(专门用来处理箭头)

程序最先开始运行的是 407629,这里我们可以输入上下左右箭头与特定的数字

(同时我们的输入还会决定异常类型)

Official Write up: The value of the first field of the thrown StdObfException object comes from the second input passed to the construct of StdObfException.

image-20220720123419556

(这图好大…有没有什么办法处理这些图片还有博客页面的方法)

那么异常处理先不深究,继续回来箭头如何处理这个问题

那么箭头其实为三字节码,上下左右箭头分别对应 ^[[A ^[[B ^[[C ^[[D

(此时开始动调,我第一次输入为上箭头,同时注意RAX)

那么在 407629 第一次处理箭头会读取为1B

image-20220720123755360

随后到 40553A 读取为5B

image-20220720123842835

最后到达 405676 可以发现我们的上箭头代码所对应的字符为A

image-20220720124119872

以上就解释了第一段输入的处理,等到最后解密第一段输入就会用到此

OLLVM

引用这张图,想要去掉ollvm最基本的是要认识这几个块

https://security.tencent.com/index.php/blog/msg/112

image-20220720133325311

先抛去原题,来认识一下这些名词

  1. 函数的开始地址为序言(Prologue)的地址
  2. 序言的后继为主分发器(Main dispatcher)
  3. 后继为主分发器的块为预处理器(Predispatcher)
  4. 后继为预处理器的块为真实块(Relevant blocks)
  5. 无后继的块为retn块
  6. 剩下的为无用块与子分发器(Sub dispatchers)

那参考文章,总结来说,利用angr符号执行去除控制流平坦化的步骤可以归结为三个步骤:

  1. 静态分析CFG得到序言/入口块(Prologue)、主分发器(Main dispatcher)、子分发器/无用块(Sub dispatchers)、真实块(Relevant blocks)、预分发器(Predispatcher)和返回块(Return)
  2. 利用符号执行恢复真实块的前后关系,重建控制流
  3. 根据第二步重建的控制流Patch程序,输出恢复后的可执行文件

简单来说就是获取所有的块,利用angr符号执行我们的真实块,查看真实块之间的流程,再抛去我们不要的块,patch程序,完成!

(那么具体的实现看文章)

https://bluesadi.github.io/0x401RevTrain-Tools/angr/10_%E5%88%A9%E7%94%A8angr%E7%AC%A6%E5%8F%B7%E6%89%A7%E8%A1%8C%E5%8E%BB%E9%99%A4%E6%8E%A7%E5%88%B6%E6%B5%81%E5%B9%B3%E5%9D%A6%E5%8C%96/

然而这题…,根本不像啊!可以看出这题的CFG根本看不懂,不像单单ollvm混淆过的cfg那么漂亮

image-20220720134654996

Exception

为了搞懂CFG为什么成这样了,得先了解下异常的原理,参考原文

https://www.cnblogs.com/catch/p/3604516.html

对于最基本的thown catch不再赘述,这篇讲到很清楚

https://4nsw3r.top/2022/02/03/SCTF-REVERSE-CplusExceptionEncrypt-%E8%B5%9B%E5%90%8E%E5%A4%8D%E7%8E%B0/#Clang-x64

异常抛出后,发生了什么事情?

  1. 如果当前函数没有catch,就沿着函数的调用链继续往上抛,然后出现两种情况

    • 在某个函数中找到相应的catch

    • 没找到相应的catch,调用 std::terminate() (这个函数是把程序abort)

  2. 如果想找到了相应的catch,执行相应的操作

    • 程序中catch的代码块有个专有名词:Landing pad
  3. 从抛异常到开始 -> 执行Landing pad代码 这整个过程叫作Stack unwind

Stack unwind

  1. 从抛异常函数开始,对调用链上的函数逐个往前查找Landing pad
  2. 如果没有找到Landing pad则把程序abort,如果找到则记下Landing pad的位置,再重新回到抛异常的函数那里开始,一帧一帧地清理调用链上各个函数内部的局部变量,直到 landing pad 所在的函数为止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void func1()
{
cs a; // stack unwind时被析构。
throw 3;
}

void func2()
{
cs b;
func1();
}

void func3()
{
cs c;
try
{
func2();
}
catch (int)
{
//进入这里之前, func1, func2已经被unwind.
}
}

stack unwind的过程可以简单看成函数调用的逆过程,这个过程在实现上由一个专门的stack unwind库来实现

Itanium C++ ABI

ltanium C++ ABI定义了一系列函数以及数据结构来建立整个异常处理的流程及框架,主要函数包括以下列:

1
2
3
4
5
6
7
8
9
10
_Unwind_RaiseException,
_Unwind_Resume,
_Unwind_DeleteException,
_Unwind_GetGR,
_Unwind_SetGR,
_Unwind_GetIP,
_Unwind_SetIP,
_Unwind_GetRegionStart,
_Unwind_GetLanguageSpecificData,
_Unwind_ForcedUnwind

其中 _Unwind_RaiseException() 函数进行stack unwind,它在用户执行throw的时被调用

主要功能

那么稍稍总结一下,就是当程序抛出异常就要进行 stack unwind 操作

而这个操作具体是 _Unwind_RaiseException() 中的 personality routine() 实现了检查catch和清理栈上的局部变量

C++ ABI

基于前面介绍的 ltanium ABI,编译器层面也定义了一系列 ABI 与之交互

当我们在代码中写下 throw xxx,编译器会分配一个数据结构 __cxa_exception 来表示该异常,该异常也有一个头部,定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct __cxa_exception 
{
std::type_info * exceptionType;
void (*exceptionDestructor) (void *);
unexpected_handler unexpectedHandler;
terminate_handler terminateHandler;
__cxa_exception * nextException;

int handlerCount;
int handlerSwitchValue;
const char * actionRecord;
const char * languageSpecificData;
void * catchTemp;
void * adjustedPtr;

_Unwind_Exception unwindHeader;
};

当用户 throw 一个异常时,编译器会帮我们调用相应的函数分配出如下的结构

image-20220717215150054

其中 __cxa_exception 就是头部,exception_obj 则是 “throw xxx” 中的 xxx,这两部分在内存中是连续的。

当我们在程序里执行了抛出异常的操作,编译器为我们做了如下的事情:

  1. 调用 __cxa_allocate_exception 函数,分配一个异常对象(__cxa_exception,数据结构如上)
  2. 调用 __cxa_throw 函数,这个函数会将异常对象做一些初始化
  3. __cxa_throw() 调用 Itanium ABI 里的 _Unwind_RaiseException() 从而开始 unwind
  4. _Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine()
  5. 该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理
  6. _Unwind_RaiseException() 将控制权转到相应的catch代码
  7. unwind 完成,用户代码继续执行

总结太Bravo了!

再看异常处理

有了这些前置知识,再看题目中的异常,由前面描述可知实现 unwind stack 的具体过程是通过 __gxx_personality_v0(即personality routine)实现

这时候我们再去IDA里调整此函数

1
2
3
4
5
6
_Unwind_Reason_Code __fastcall _gxx_personality_v0(
int Version,
_Unwind_Action actions,
__int64 exceptionClass,
_Unwind_Exception *exceptionObject,
_Unwind_Context *context)

光标在函数,按Y修改类型

image-20220720154638797

scan_eh_tab

回忆__gxx_personality_v0函数功能

  1. 检查当前函数是否有相应的 catch 语句。
  2. 清理当前函数中的局部变量。

在personality routine()下的 scan_eh_tab() 该函数有我们最关心的两个值,同时也是魔改处

与源码对比:https://code.woboq.org/llvm/libcxxabi/src/cxa_personality.cpp.html#__cxxabiv1::scan_eh_tab

Shfit + F1 -> INS 导入结构体

1
2
3
4
5
6
7
8
9
struct scan_results
{
int64_t ttypeIndex;
const uint8_t* actionRecord;
const uint8_t* languageSpecificData;
uintptr_t landingPad;
void* adjustedPtr;
_Unwind_Reason_Code reason;
};

光标在scan_eh_tab函数上按Y修改

1
void scan_eh_tab(scan_results *results, _Unwind_Action actions, bool native_exception, _Unwind_Exception *unwind_exception, _Unwind_Context *context)

Landing pad

Landing pad(指向catch块的分发处,只单单拿到landing pad还不够,这时候还缺少一个对应异常类型ttypeIndex)

image-20220720160405446

ttypeIndex

首先要求父类为StdObfException的异常

最后的ttypeIndex由 thrown_object_ptr(由我们的第一段输入所决定的thrown_object_ptr) 和 原始固定固定typeIndex 决定

image-20220720160818159

Official Write up: And we have figured out that the ttypeIndex is determined by the first field of the thrown StdObfException object and the lptinfo passed to __cxa_throw. The value of the first field of the thrown StdObfException object comes from the second input passed to the construct of StdObfException.

那么这两个值到底具体指的是什么??

其实上面已经给出了答案,反复调试可知,可以发现我们的第一段输入设置了父类StdObfException

the first field of the thrown StdObfException object 指的就是我们的输入

the lptinfo passed to __cxa_throw 指的就是当 ___cxa_allocate_exception 创建的异常,也就是固定的

image-20220720164747324

现在知道了魔改后的流程是从哪里来到哪里去,人工方式就是跳到landing pad再设置rdx为ttypeIndex就可以到达我们所对应的catch块

什么叫CFG!

那么现在知道了routine personality 中的 scan_eh_tab被修改了,而IDA平常能识别throw catch这些块的原因就是这些正常的源码

然而landingpad与ttypeIndex都被修改了,所以导致了IDA识别的CFG成了这个样子

我们根本没法用肉眼知道throw的块在哪,只有通过动调才能确定,然而这就导致了原先的deflat脚本都不不行了

原因主要为两点

  1. 无法确定throw后的块
  2. throw可能对着多个catch块,这时候就通过rdi(ttypeIndex)进行catch块分发(landingPad)

原因还有种种就不一一举例,就无法正常原先deflat所需要的CFG块

image-20220720222946227

以下开始就是跟着官方脚本复现

我们再回忆一下正常的ollvm的执行流程

Prologue(入口块)-> Main dispatcher(主分发器)-> Sub dispathers(子分发器)-> Relevant blocks(真实块)-> Predispather(预分发器)-> Main dispatcher(主分发器)…

总结一下这道题的CFG

我们的下一个真实块取决于系统生产的lptinfo和我们的第一段输入所导致的StdObfException,在每个真实块的结束,我们不只是跳往与预分发器,而是调用 __cxa_throw 进行第二次调度,我们称二次调用为 second dispatch

所以我们的执行流就是

… -> main dispatcher -> sub dispatchers -> relevant block -> throw StdObfException exception -> Secondary dispatchers -> pre-dispatcher -> main dispatcher -> …

除此之外,程序还抛出了一些真正的异常,对于这些异常,第二次调用发生于Landing pad末尾

… -> main dispatcher -> sub dispatchers -> relevant block that throws real exceptions -> the according real LandingPad block -> throw StdObfException exception -> Secondary dispatchers -> pre-dispatcher -> main dispatcher -> …

0x02 Deflat Solution

去该平坦化控制流,有两个步骤:

  1. 找到所有的真实块
  2. 找到真实块之间的关系

Find all relevant blocks

我们可以从主分发器开始寻找,找到所有子分发器的后继者,这些后继者本身不是子分发器

官方WP中一眼丁真发现子分发器由该指令格式组成

1
2
3
sub dispathers such as:
cmp
jx

于是由此区别出来

1
2
3
4
5
6
7
isCmpRI = lambda instr: instr.mnemonic == "cmp" and\
hasattr(instr.operands[0], "_X86RegisterOperand__key") and\
hasattr(instr.operands[1], "_X86ImmediateOperand__key")
isCJmp = lambda instr: instr.mnemonic.startswith("j") and \
instr.mnemonic != "jmp"
isSubDispatcher = lambda bb: (len(bb.instrs) == 2) and\
isCmpRI(bb.instrs[0]) and isCJmp(bb.instrs[1])

首先判断是否为子分发器,然后排除法找到所有真实块

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
class PatchHelper:
## ......
# To get all cfgs
def block(self, addr):
bb = self.cfg.find_basic_block(addr)
if bb is None:
bb = barf.bb_builder.strategy._disassemble_bb(addr, barf.binary.ea_end, {})
return bb

def get_relevant_blocks(cfg, patch_helper, main_dispatcher):
isCmpRI = lambda instr: instr.mnemonic == "cmp" and\
hasattr(instr.operands[0], "_X86RegisterOperand__key") and\
hasattr(instr.operands[1], "_X86ImmediateOperand__key")
isCJmp = lambda instr: instr.mnemonic.startswith("j") and \
instr.mnemonic != "jmp"
isSubDispatcher = lambda bb: (len(bb.instrs) == 2) and\
isCmpRI(bb.instrs[0]) and isCJmp(bb.instrs[1])
relevant_blocks = []
visited = set()
q = SimpleQueue()
q.put(patch_helper.block(main_dispatcher))
while not q.empty():
bb = q.get()
# Either Sub Patchers or Relevant blocks?
if isSubDispatcher(bb):
for succ, cond in bb.branches:
if succ in visited:
continue
q.put(patch_helper.block(succ))
visited.add(succ)
else:
relevant_blocks.append(bb)
return relevant_blocks

Relevant blocks:

1
2
3
*******************relevant blocks************************
main_dispatcher:0x404a80
relevant_blocks: ['0x409437', '0x406443', '0x404ab8', '0x408031', '0x407842', '0x407d31', '0x407437', '0x407f4f', '0x4076bd', '0x407a6b', '0x40723e', '0x407fc4', '0x409458', '0x407bc7', '0x40732f', '0x407ebc', '0x407566', '0x407960', '0x4070fa', '0x405e7a', '0x4078e3', '0x407e5a', '0x4074ca', '0x405c87', '0x407741', '0x407af5', '0x4072b4', '0x405ded', '0x4077b6', '0x407c6b', '0x4073a4', '0x405b29', '0x4075f9', '0x407a06', '0x4071aa', '0x406cfe', '0x406c94', '0x406ef0', '0x406859', '0x40707d', '0x406b62', '0x406f5f', '0x4065c9', '0x406e5d', '0x406a72', '0x406d7b', '0x406704', '0x406def', '0x406964', '0x40944b', '0x4064a5', '0x405469', '0x405a5f', '0x404fae', '0x40532c', '0x40589c', '0x404d58', '0x4053d3', '0x405923', '0x404ec5', '0x40529a', '0x4057b8', '0x404bc4', '0x405f2a', '0x4056f0', '0x406299', '0x4068f0', '0x4063b0', '0x406bf9', '0x406323', '0x406646', '0x40620f', '0x406b00', '0x4060e7', '0x4067bb', '0x40617c', '0x4069e3', '0x40606d', '0x406521', '0x4051fe', '0x405647', '0x404e14', '0x4055b5', '0x4050cc', '0x40550b', '0x404ca4']

Find the flow

官网WP指出抽象出来)留个坑,以后熟了试试

Official Write up: A good idea is to abstract the throw StdObfException -> catch process and do the one basic block symbolic execution (You can refer to Deobfuscation: recovering an OLLVM-protected program or 利用符号执行去除控制流平坦化 for more information).

于是官网WP又给了个更有趣的方法,GDB脚本!

为了找到真实块之间的流程,通过普通的执行然后打印真实块需要的信息!

但是我们不一样能得到所有的流程因为部分可能没执行到,但是我们依然可以利用提取出来的信息去恢复部分控制流,并弄清楚如何输入可以恢复更多流程。(怎么好像梦到过我在这写wp…)

生成GDB的脚本如下

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
cmds = """\
set pagination off

b *0x40A3D4
commands
silent
printf "landingPad: %x\\n", $rdx
continue
end

b _ZN18StdSubObfExceptionC2Ec
commands
silent
printf "selector: %x\\n", $rsi
continue
end

define mytrace
break $arg0
commands
silent
printf "%x\\n", $pc
python gdb.execute('continue')
end
end
"""
for bb in relevant_blocks:
cmds += (f"mytrace *{hex(bb.address)} \n")
cmds += "run\n"
with open("test.gdb", "w") as f:
f.write(cmds)
1
2
3
4
cat teatin
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

gdb inflated -x test.gdb --batch < testin > testout

于是可以获取真实块接下来的landing pad与异常类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Breakpoint 1 at 0x40a3d4
......
Breakpoint 88 at 0x404ca4
4075f9
selector: 0
landingPad: 4089bf
4072b4
selector: 0
landingPad: 408503
4075f9
selector: 2
landingPad: 4089bf
4060e7
selector: 0
......
40617c
selector: 0
landingPad: 409100
409437
[Inferior 1 (process 13732) exited normally]

然后就写个PARSER分析

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
def parse_logs(logfn, prologue, patch_helper):
with open(logfn, "r") as f:
t = f.readlines()
i = 0
selector_s = "selector: "
landingpad_s = "landingPad: "
relations = set()
laddr = prologue
lselector = 0
landingpad = 0
while i < len(t):
try:
addr = int(t[i], 16)
except:
i += 1
continue
if not laddr is None:
relations.add((laddr, lselector, addr))
if t[i+1].startswith(selector_s):
selector = int(t[i+1][len(selector_s):], 16)
i += 2
elif t[i+1].startswith(landingpad_s):
landingpad = int(t[i+1][len(landingpad_s):], 16)
relations.add((addr, -1, landingpad))
addr = landingpad
while not patch_helper.is_unreachable(patch_helper.block(addr).direct_branch):
addr = patch_helper.block(addr).direct_branch
if t[i+2].startswith(selector_s):
selector = int(t[i+2][len(selector_s):], 16)
i += 3
elif t[i+1].startswith("[Inferior "):
i += 1
else:
print("Warning: %x doesn't have selector. "%addr)
exit(0)
laddr = addr
lselector = selector
return list(relations)

print('************************flow******************************')
relations = parse_logs(sys.argv[3], prologue, patch_helper)
relations.sort(key = lambda x:x)
flow = {}
for bb, selector, child in relations:
if bb in flow:
while len(flow[bb]) < selector:
flow[bb].append(-1)
flow[bb].append(child)
assert(len(flow[bb]) == selector+1)
else:
flow[bb] = [child]
for (k, v) in list(flow.items()):
print('%#x:' % k, [hex(child) for child in v])

Flows:

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
************************flow******************************
0x404820: ['0x4075f9']
0x404ab8: ['0x404ab8', '0x406c94']
0x404bc4: ['0x407bc7']
0x404ca4: ['0x406bf9']
0x404ec5: ['0x4053d3']
0x404fae: ['0x406b00']
0x4051fe: ['0x40707d']
0x4053d3: ['0x406521']
0x405469: ['0x407d31']
0x4056f0: ['0x405a5f', '0x4056f0']
0x4057b8: ['0x404ab8']
0x405923: ['0x405923', '0x406e5d']
0x405a5f: ['0x4067bb']
0x405b29: ['0x406964', '0x406646']
0x405c87: ['0x405c87', '0x407437']
0x405f2a: ['0x405f2a', '0x4063b0']
0x4060e7: ['0x40723e']
0x40617c: ['0x409437']
0x40620f: ['0x405f2a']
0x406299: ['0x404bc4', '0x4057b8']
0x4063b0: ['0x4063b0', '0x405469']
0x4064a5: ['0x406704', '0x40620f']
0x406521: ['0x4074ca', '0x404bc4']
0x4065c9: ['0x40723e']
0x406646: ['0x406964']
0x406704: ['0x405c87']
0x4067bb: ['0x4082b6']
0x406964: ['0x405b29', '0x404ca4']
0x4069e3: ['0x408281']
0x406a72: ['0x404fae']
0x406b00: ['0x406299']
0x406bf9: ['0x405923']
0x406c94: ['0x4074ca']
0x406cfe: ['0x40723e']
0x406e5d: ['0x406e5d', '0x4077b6']
0x406f5f: ['0x406f5f', '0x407566']
0x40707d: ['0x40707d', '0x407960']
0x4070fa: ['0x406f5f']
0x4071aa: ['0x4056f0']
0x40723e: ['0x4072b4']
0x4072b4: ['0x4075f9', '0x4071aa']
0x407437: ['0x407437', '0x4064a5']
0x4074ca: ['0x404ec5', '0x407c6b']
0x407566: ['0x407566', '0x407a6b']
0x4075f9: ['0x4072b4', '-0x1', '0x4060e7', '0x406cfe', '0x4078e3', '0x4065c9']
0x4076bd: ['0x404ec5']
0x4077b6: ['0x406bf9', '0x4070fa']
0x4078e3: ['0x40723e']
0x407960: ['0x4081f5']
0x407a6b: ['0x4070fa', '0x406704']
0x407bc7: ['0x406a72', '0x407bc7']
0x407c6b: ['0x4069e3']
0x407d31: ['0x407d31', '0x407ebc']
0x407ebc: ['0x407ebc', '0x40617c']
0x4081f5: ['0x405b29']
0x408281: ['0x4051fe']
0x4082b6: ['0x4076bd']

Patch

修复程序环节!

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
def patch_branches(self, bb, va_targets):
va_start, size = self.get_patchable_from_relblk(bb)
if size < PatchHelper.JMP_SIZE:
print("[Warning] patch_jmp at block %x may fail. size: %d."%(bb.address, size))
org_start = va_start
print(f"va_start: {hex(va_start)}, bb addr: {hex(bb.address)}, size: {size}")
## `cmp esi, v` instr takes 3 bytes while `je xxx` takes 6 bytes
## And the last jmp instr takes 5 bytes.
total_size = 9 * len(va_targets) - 4
if size < total_size:
## If the nop block at the end of current block is not large enough,
## try to find another nop block and then jump to it.
nx_va_start, nx_size = self.get_nop_by_size(total_size)
if nx_size == 0:
print("[Error] `patch_branches` needs a nop block with size larger than %d."%(total_size))
self.patch_jmp(va_start, nx_va_start)
va_start, size = nx_va_start, nx_size

for i, t in enumerate(va_targets[:-1]):
cmp_instr = bytes([0x83,0xfe,i])
self.do_patch(va_start, cmp_instr)
va_start += len(cmp_instr)
cj_instr = bytes([PatchHelper.opcode['j'],PatchHelper.opcode['e']])
if t == -1:
## -1 represent that we do not know the flow for this selector value for now.
cj_instr += struct.pack('<i', self.func_terminate-va_start-6)
# cj_instr = asm(f"je {hex(self.func_terminate)}", vma=va_start)
else:
cj_instr += struct.pack('<i', t-va_start-6)
# cj_instr = asm(f"je {hex(t)}", vma=va_start)
self.do_patch(va_start, cj_instr)
va_start += len(cj_instr)
va_start += self.patch_jmp(va_start, va_targets[-1])
if va_start > org_start+size:
print("[Warning] patches at (%x, %x) overlaps next blk. "%(org_start, va_start))

官方完整脚本

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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
## filename: deflat.py
from ast import Tuple
from xmlrpc.client import Boolean
from barf.barf import BARF
import angr
import struct
import sys
from pwnlib import elf
from queue import SimpleQueue
# from pwn import *

class PatchHelper:
opcode = {'a' :0x87, 'ae':0x83, 'b' :0x82, 'be':0x86, 'c' :0x82, 'e' :0x84, 'z' :0x84, 'g' :0x8F,
'ge':0x8D, 'l' :0x8C, 'le':0x8E, 'na':0x86, 'nae':0x82,'nb':0x83, 'nbe':0x87,'nc':0x83,
'ne':0x85, 'ng':0x8E, 'nge':0x8C,'nl':0x8D, 'nle':0x8F,'no':0x81, 'np':0x8B, 'ns':0x89,
'nz':0x85, 'o' :0x80, 'p' :0x8A, 'pe':0x8A, 'po':0x8B, 's' :0x88, 'nop':0x90,'jmp':0xE9, 'j':0x0F}
JMP_SIZE = 5

def is_unreachable(self, bb):
if isinstance(bb, int):
bb = self.block(bb)
for i in range(len(bb.instrs)):
if bb.instrs[i].mnemonic != "call":
continue
target = bb.instrs[i].operands[0].immediate
if target == self.func_terminate:
return True

def block(self, addr):
bb = self.cfg.find_basic_block(addr)
if bb is None:
bb = barf.bb_builder.strategy._disassemble_bb(addr, barf.binary.ea_end, {})
return bb

@staticmethod
def is_imm(operand):
return (hasattr(operand, "_X86ImmediateOperand__key"))

@staticmethod
def is_reg(operand):
return (hasattr(operand, "_X86RegisterOperand__key"))

def is_call_throw(self, instr):
return instr.mnemonic == "call" and \
self.is_imm(instr.operands[0]) and\
instr.operands[0].immediate == self.func_throw

def is_call_allocate_exception(self, instr):
return instr.mnemonic == "call" and \
self.is_imm(instr.operands[0]) and\
instr.operands[0].immediate == self.func_allocate_exception

def is_call_obf_exception(self, instr):
return instr.mnemonic == "call" and \
self.is_imm(instr.operands[0]) and\
instr.operands[0].immediate == self.func_obf_exception


def skip_call_args(self, bb, i):
while ((bb.instrs[i].mnemonic in ["xor","mov","lea"]) and\
(len(bb.instrs[i].operands) > 0) and (self.is_reg(bb.instrs[i].operands[0])) and\
(bb.instrs[i].operands[0].name in ["edx", "rdx", "esi", "rsi", "edi", "rdi"])) or \
bb.instrs[i].mnemonic == "nop":
i -= 1
return i

def get_patchable_from_relblk(self, bb):
i = 0
end = bb.start_address + bb.size
while i < len(bb.instrs) and not self.is_call_throw(bb.instrs[i]):
i += 1
i = self.skip_call_args(bb, i-1)
if i == len(bb.instrs) - 1:
start = end
else:
start = bb.instrs[i+1].address
self.fill_nops(start, end)
return (start, end-start)

def __init__(self, proj, elf, barf, cfg) -> None:
self.p = proj
obj = proj.loader.main_object
self.func_terminate = obj.symbols_by_name["__clang_call_terminate"].rebased_addr
self.func_throw = obj.plt["__cxa_throw"]
self.func_allocate_exception = obj.plt["__cxa_allocate_exception"]
self.func_obf_exception = obj.symbols_by_name["_ZN18StdSubObfExceptionC2Ec"].rebased_addr
self.elf = elf
self.elfData = bytearray(self.elf.data)
self.barf = barf
self.cfg = cfg
self.nops = []

def append_nop(self, nopblk):
if nopblk[1] > 0:
self.nops.append(nopblk)

def finalize(self):
self.nops.sort()
idx = 0
while idx < len(self.nops) - 1:
if self.nops[idx][0] + self.nops[idx][1] != self.nops[idx+1][0]:
idx += 1
continue
self.nops[idx]=(self.nops[idx][0], self.nops[idx][1]+self.nops[idx+1][1])
del self.nops[idx+1]

def fill_nops(self, va_start, va_end):
assert not self.elf is None
start = self.elf.vaddr_to_offset(va_start)
end = self.elf.vaddr_to_offset(va_end)
for i in range(start, end):
self.elfData[i] = PatchHelper.opcode['nop']

def get_nop_by_size(self, min_size):
for idx, nop in enumerate(self.nops):
if nop[1] > min_size:
del self.nops[idx]
return nop
return (-1, 0)

def do_patch(self, va_start, codes):
start = self.elf.vaddr_to_offset(va_start)
for i in range(len(codes)):
self.elfData[start+i] = codes[i]

def patch_jmp(self, va_start, va_target):
offset = va_target - va_start - PatchHelper.JMP_SIZE
jmp = bytes([PatchHelper.opcode['jmp']])+struct.pack('<i', offset)
self.do_patch(va_start, jmp)
return PatchHelper.JMP_SIZE

def patch_branches(self, bb, va_targets):
va_start, size = self.get_patchable_from_relblk(bb)
if size < PatchHelper.JMP_SIZE:
print("[Warning] patch_jmp at block %x may fail. size: %d."%(bb.address, size))
org_start = va_start
print(f"va_start: {hex(va_start)}, bb addr: {hex(bb.address)}, size: {size}")
## `cmp esi, v` instr takes 3 bytes while `je xxx` takes 6 bytes
## And the last jmp instr takes 5 bytes.
total_size = (3+6) * len(va_targets) - 4
if size < total_size:
## If the nop block at the end of current block is not large enough,
## try to find another nop block and then jump to it.
nx_va_start, nx_size = self.get_nop_by_size(total_size)
if nx_size == 0:
print("\033[31m[Error]\033[0m `patch_branches` needs a nop block with size larger than %d."%(total_size))
self.patch_jmp(va_start, nx_va_start)
va_start, size = nx_va_start, nx_size
for i, t in enumerate(va_targets[:-1]):
cmp_instr = bytes([0x83,0xfe,i])
self.do_patch(va_start, cmp_instr)
va_start += len(cmp_instr)
cj_instr = bytes([PatchHelper.opcode['j'],PatchHelper.opcode['e']])
if t == -1:
## -1 represent that we do not know the flow for this selector value for now.
cj_instr += struct.pack('<i', self.func_terminate-va_start-6)
# cj_instr = asm(f"je {hex(self.func_terminate)}", vma=va_start)
else:
cj_instr += struct.pack('<i', t-va_start-6)
# cj_instr = asm(f"je {hex(t)}", vma=va_start)
self.do_patch(va_start, cj_instr)
va_start += len(cj_instr)
va_start += self.patch_jmp(va_start, va_targets[-1])
if va_start > org_start+size:
print("[Warning] patches at (%x, %x) overlaps next blk. "%(org_start, va_start))

def get_relevant_blocks(cfg, patch_helper, main_dispatcher):
isCmpRI = lambda instr: instr.mnemonic == "cmp" and\
hasattr(instr.operands[0], "_X86RegisterOperand__key") and\
hasattr(instr.operands[1], "_X86ImmediateOperand__key")
isCJmp = lambda instr: instr.mnemonic.startswith("j") and \
instr.mnemonic != "jmp"
isSubDispatcher = lambda bb: (len(bb.instrs) == 2) and\
isCmpRI(bb.instrs[0]) and isCJmp(bb.instrs[1])
relevant_blocks = []
visited = set()
q = SimpleQueue()
q.put(patch_helper.block(main_dispatcher))
while not q.empty():
bb = q.get()
if isSubDispatcher(bb):
patch_helper.append_nop((bb.start_address, bb.size))
for succ, cond in bb.branches:
if succ in visited:
continue
q.put(patch_helper.block(succ))
visited.add(succ)
else:
relevant_blocks.append(bb)
return relevant_blocks


def parse_logs(logfn, prologue, patch_helper):
with open(logfn, "r") as f:
t = f.readlines()
i = 0
selector_s = "selector: "
landingpad_s = "landingPad: "
relations = set()
laddr = prologue
lselector = 0
landingpad = 0
while i < len(t):
try:
addr = int(t[i], 16)
except:
i += 1
continue
if not laddr is None:
relations.add((laddr, lselector, addr))
if t[i+1].startswith(selector_s):
selector = int(t[i+1][len(selector_s):], 16)
i += 2
elif t[i+1].startswith(landingpad_s):
landingpad = int(t[i+1][len(landingpad_s):], 16)
relations.add((addr, -1, landingpad))
addr = landingpad
while not patch_helper.is_unreachable(patch_helper.block(addr).direct_branch):
addr = patch_helper.block(addr).direct_branch
if t[i+2].startswith(selector_s):
selector = int(t[i+2][len(selector_s):], 16)
i += 3
elif t[i+1].startswith("[Inferior "):
i += 1
else:
print("Warning: %x doesn't have selector. "%addr)
exit(0)
laddr = addr
lselector = selector
return list(relations)


def generate_gdb_script(relevant_blocks):
cmds = """\
set pagination off

b *0x40A3D4
commands
silent
printf "landingPad: %x\n", $rdx
continue
end

b _ZN18StdSubObfExceptionC2Ec
commands
silent
printf "selector: %x\n", $rsi
continue
end

define mytrace
break $arg0
commands
silent
printf "%x\\n", $pc
python gdb.execute('continue')
end
end
"""
for bb in relevant_blocks:
cmds += (f"mytrace *{hex(bb.address)} \n")
cmds += "run\n"
with open("test.gdb", "w") as f:
f.write(cmds)


if __name__ == '__main__':
if len(sys.argv) < 3:
print('Usage: python deflat.py filename function_address(hex) [logfile]')
exit(0)

# context.arch = "amd64"
# context.os = "linux"
# context.endian = "little"

filename = sys.argv[1]
start = int(sys.argv[2], 16)

origin = elf.ELF(filename)
b = angr.Project(filename, load_options={'auto_load_libs': False, 'main_opts':{'custom_base_addr': 0}})
barf = BARF(filename)
cfg = barf.recover_cfg(start=start)
patch_helper = PatchHelper(b, origin, barf, cfg)
blocks = cfg.basic_blocks

prologue = start
main_dispatcher = patch_helper.block(prologue).direct_branch
relevant_blocks = get_relevant_blocks(cfg, patch_helper, main_dispatcher)
nop = patch_helper.get_patchable_from_relblk(patch_helper.block(prologue))
patch_helper.append_nop(nop)

print('*******************relevant blocks************************')
print('main_dispatcher:%#x' % main_dispatcher)
print('relevant_blocks:', [hex(bb.address) for bb in relevant_blocks])


if len(sys.argv) < 4:
generate_gdb_script(relevant_blocks)
exit(0)

print('************************flow******************************')
relations = parse_logs(sys.argv[3], prologue, patch_helper)
relations.sort(key = lambda x:x)
flow = {}
for bb, selector, child in relations:
if bb in flow:
while len(flow[bb]) < selector:
flow[bb].append(-1)
flow[bb].append(child)
assert(len(flow[bb]) == selector+1)
else:
flow[bb] = [child]
for (k, v) in list(flow.items()):
print('%#x:' % k, [hex(child) for child in v])

print('************************patch*****************************')
patch_helper.finalize()
for (parent, childs) in list(flow.items()):
## Patch jmps
blk = patch_helper.block(parent)
patch_helper.patch_branches(blk, childs)
## Nop call allocate_exception and call obf_exception
for idx, instr in enumerate(blk.instrs):
if patch_helper.is_call_allocate_exception(instr) or\
patch_helper.is_call_obf_exception(instr):
# si = patch_helper.skip_call_args(blk, idx-1)+1
# start = blk.instrs[si].address
start = instr.address
end = instr.address + instr.size
patch_helper.fill_nops(start, end)

with open(filename + '.recovered', 'wb') as f:
f.write(bytes(patch_helper.elfData))
print('Successful! The recovered file: %s' % (filename + '.recovered'))

Work flow:

1
2
3
$ python deflat.py inflated 0x404820
$ gdb inflated -x test.gdb --batch < testin > testout
$ python deflat.py inflated 0x404820 testout

(按照以上流程,test.gdb可能会报个错,程序把本身有个\n是脚本中需要打印的,但直接转义成真换行了需要手动恢复)

观看修复后的流程

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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
int __cdecl main(int argc, const char **argv, const char **envp)
{
......
v3 = fileno(stdin);
tcgetattr(v3, &intermiosBufBackup);
cfmakeraw(&intermiosBuf);
tcsetattr(v3, 0, &intermiosBuf);
*(_OWORD *)v196 = 0LL;
v195 = 0LL;
*(_OWORD *)s = 0LL;
*(_QWORD *)&v196[13] = 0LL;
v124 = &v168;
v123 = &v167;
v164 = v199;
v187 = &v198;
v186 = &v96;
v185 = &v97;
v184 = &v100;
v122 = &s[12];
v108 = v103;
v163 = &v197;
v183 = &v99;
v162 = &v166;
......
v5 = 0LL;
do
{
v72 = v4;
v98 = getc(stdin);
v73 = v98 << 24;
v74 = v98 << 24 == 0x1B000000;
if ( v98 << 24 == 0x31000000 )
v74 = 2;
if ( v73 == 0x37000000 )
v74 = 3;
if ( v73 == 0x33000000 )
v74 = 4;
if ( v73 == 0x34000000 )
v74 = 5;
v101 = v5;
v102 = v72;
v119 = v72;
if ( v74 )
{
if ( v74 == 1 )
_clang_call_terminate(5LL);
if ( v74 == 2 )
{
v107 = v102 + (4LL << (3 * (unsigned __int8)v101));
v85 = v98;
}
else if ( v74 == 3 )
{
v107 = v102 + (5LL << (3 * (unsigned __int8)v101));
v85 = v98;
}
else
{
if ( v74 == 4 )
v107 = v102 + (6LL << (3 * (unsigned __int8)v101));
else
v107 = v102 + (7LL << (3 * (unsigned __int8)v101));
v85 = v98;
}
s[v101] = v85;
v119 = v107;
}
v5 = v101 + 1;
v174 = v119;
}
while ( v101 != 11 );
s[12] = 0;
v69 = fileno(stdin);
tcsetattr(v69, 0, &intermiosBufBackup);
for ( i = 0LL; i < 5; ++i )
*((_BYTE *)v136 + i) = byte_40E0F3[i] - byte_40E0F8[i];
v188 = &v190;
v190 = v136[0];
v189 = 4LL;
v191 = 0;
__isoc99_scanf(&v190, v122);
v26 = v188;
v175 = v188;
*(_OWORD *)v188 = xmmword_40E040;
v26[4] = 639210836;
*((_BYTE *)v26 + 20) = 16;
*(_QWORD *)((char *)v26 + 34) = 0x1005E763241AA6B1LL;
*(_OWORD *)((char *)v26 + 21) = xmmword_40E148;
__cxa_begin_catch(v26);
v155 = strlen(v122);
v128 = 0LL;
v113 = 0;
v125 = v155;
v147 = 0LL;
do
{
v133 = v125 - 1;
v86 = v122[v147];
v160 = v128;
v110 = v113;
v176 = v147;
isalnum(v86);
v50 = (unsigned int)(v160 + 1);
*(&v95 + (int)v160) = v86;
v181 = v176 + 1;
v130 = v50;
v112 = v110;
v146 = 0LL;
if ( (_DWORD)v50 == 4 )
{
do
{
v106 = 0LL;
v149 = v146;
do
{
v199[v106 + 16] = byte_40E071[v106] - byte_40E0B2[v106];
++v106;
}
while ( v106 < 0x41 );
v56 = v163;
*(_QWORD *)v163 = v164;
v165 = 64LL;
v169 = (_OWORD *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::_M_create(
v56,
&v165,
0LL);
v9 = (void **)v163;
v10 = v169;
*(_QWORD *)v163 = v169;
v11 = v165;
*(_QWORD *)v164 = v165;
v12 = MEMORY[5];
v13 = MEMORY[0x15];
v14 = MEMORY[0x25];
v10[3] = MEMORY[0x35];
v10[2] = v14;
v10[1] = v13;
*v10 = v12;
*(_QWORD *)v187 = v11;
*((_BYTE *)v10 + v11) = 0;
v15 = v149;
*(&v95 + v15) = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::find(
v9,
(unsigned int)*(&v95 + v149),
0LL);
v177 = *v9;
operator delete(v177);
v146 = v149 + 1;
}
while ( v149 != 3 );
v17 = *v186;
*v183 = (4 * *v57) | ((unsigned __int8)*v186 >> 4) & 3;
v18 = *v185;
*v59 = (16 * v17) | ((unsigned __int8)*v185 >> 2) & 0xF;
*v184 = *v58 + (v18 << 6);
v152 = v110;
v151 = 0LL;
do
{
v6 = v151;
v7 = (unsigned __int8)*(&v99 + v151) / 0xAu;
v8 = v152;
v199[v152 + 96] = (unsigned __int8)*(&v99 + v151) % 0xAu;
v199[v8 + 97] = v7;
v151 = v6 + 1;
v152 = v8 + 2;
v182 = v8 + 2;
}
while ( v6 != 2 );
v130 = 0LL;
v112 = v182;
}
v128 = v130;
v113 = v112;
v125 = v133;
v147 = v181;
}
while ( v133 );
__cxa_end_catch();
v193 = 152788034LL;
v192[3] = xmmword_40E130;
v192[2] = xmmword_40E120;
v192[1] = xmmword_40E110;
v192[0] = xmmword_40E100;
v138 = 152788034LL;
cipher_helper<12037464u,StList<0ul,1ul,2ul,3ul,4ul,5ul,6ul,7ul,8ul,9ul,10ul,11ul,12ul,13ul,14ul,15ul,16ul,17ul,18ul,19ul,20ul,21ul,22ul,23ul,24ul,25ul,26ul,27ul,28ul,29ul,30ul,31ul,32ul,33ul,34ul,35ul,36ul,37ul,38ul,39ul>>::get_array(
152788034LL,
"Knows the futility yet does it anyway. ");
v55 = v138;
*(_OWORD *)(v138 + 56) = xmmword_40E16D;
*(_OWORD *)(v55 + 40) = xmmword_40E15D;
*(_QWORD *)(v55 + 72) = 0x6FF0E70B5B3F60A4LL;
v137 = (void *)0x6FF0E70B5B3F60A4LL;
__cxa_begin_catch((void *)0x6FF0E70B5B3F60A4LL);
v145 = 0LL;
do
{
v67 = v145;
*((_DWORD *)v192 + 2 * v145) ^= 0x9005408u;
v145 = v67 + 1;
}
while ( v67 != 8 );
__cxa_end_catch();
*(_OWORD *)v75 = xmmword_40E030;
*((_QWORD *)v75 + 2) = 0x48D1556A814FF991LL;
*((_QWORD *)v75 + 5) = 0x48B0E10161EA8322LL;
v25 = -2.526699287193993e95;
*(_OWORD *)(v75 + 24) = xmmword_40E185;
__cxa_begin_catch(v75);
v121 = 0LL;
v109 = 0;
do
{
v27 = v121;
v179 = (unsigned __int64 *)v192 + (unsigned int)v121 / 9uLL;
v28 = *v179;
v29 = (unsigned int)v121 % 9;
v30 = pow(v25, (double)(int)((unsigned int)v121 % 9 + 1));
v178 = v28;
v31 = v28 % (unsigned int)(int)(v30 + 0.5);
y = (double)v29;
v32 = pow(11.0, (double)v29) + 0.5;
v33 = (unsigned int)(int)v32;
v25 = v32 - 9.223372036854776e18;
v158 = v27;
v157 = v109;
v111 = v109;
if ( v31 < v33 )
{
v111 = v157 + 1;
v51 = v199[(int)v157 + 96];
v52 = pow(v25, y) + 0.5;
v53 = (unsigned int)(int)v52;
v25 = v52 - 9.223372036854776e18;
*v179 = v178 + v51 * v53;
}
v121 = (unsigned int)(v158 + 1);
v109 = v111;
}
while ( (_DWORD)v158 != 80 );
__cxa_end_catch();
v88 = 1;
v140 = 0LL;
do
{
v60 = v108;
v108[8] = 0;
*(_QWORD *)v60 = 0LL;
v171 = *((_QWORD *)v192 + v140);
v126 = 0LL;
v170 = v140;
do
{
v19 = v126;
v20 = v126 + 1;
v21 = pow(v25, (double)((int)v126 + 1));
v22 = v171 % (unsigned int)(int)(v21 + 0.5);
v23 = pow(11.0, (double)v19) + 0.5;
v24 = (unsigned int)(int)v23;
v25 = v23 - 9.223372036854776e18;
v103[v22 / v24] = 1;
v141 = 1LL;
v89 = v88;
v126 = v20;
}
while ( v20 != 9 );
do
{
v61 = v89;
if ( !v103[v141] )
v61 = 0;
++v141;
v115 = v61;
v89 = v61;
}
while ( v141 != 10 );
v140 = v170 + 1;
v131 = 0LL;
v87 = v115;
v88 = v115;
}
while ( v170 != 8 );
do
{
v68 = v108;
v108[8] = 0;
*(_QWORD *)v68 = 0LL;
v172 = (double)((int)v131 + 1);
v40 = (double)(int)v131;
v173 = (double)(int)v131;
v161 = (unsigned int)v131;
v142 = 0LL;
do
{
v62 = v142;
v63 = *((_QWORD *)v192 + v142);
v64 = v63 % (unsigned int)(int)(pow(v40, v172) + 0.5);
v65 = pow(11.0, v173) + 0.5;
v66 = (unsigned int)(int)v65;
v40 = v65 - 9.223372036854776e18;
v103[v64 / v66] = 1;
v142 = v62 + 1;
v144 = 1LL;
v90 = v87;
}
while ( v62 != 8 );
do
{
v71 = v90;
if ( !v103[v144] )
v71 = 0;
++v144;
v116 = v71;
v90 = v71;
}
while ( v144 != 10 );
v131 = (unsigned int)(v161 + 1);
v132 = 0LL;
v92 = v116;
v87 = v116;
}
while ( (_DWORD)v131 != 9 );
do
{
v54 = v108;
v108[8] = 0;
*(_QWORD *)v54 = 0LL;
v135 = 3 * ((unsigned int)v132 / 3);
v134 = 3 * ((unsigned int)v132 % 3) + 1;
v129 = 0LL;
v159 = (unsigned int)v132;
do
{
v34 = v129;
v35 = *((_QWORD *)v192 + (int)(v135 + (unsigned int)v129 / 3));
v36 = (v134 + (unsigned int)v129 % 3) % 9;
v37 = v35 % (unsigned int)(int)(pow(v40, (double)(v36 + 1)) + 0.5);
v38 = pow(11.0, (double)v36) + 0.5;
v39 = (unsigned int)(int)v38;
v40 = v38 - 9.223372036854776e18;
v103[v37 / v39] = 1;
v129 = (unsigned int)(v34 + 1);
v150 = 1LL;
v94 = v92;
}
while ( v34 != 8 );
do
{
v70 = v94;
if ( !v103[v150] )
v70 = 0;
++v150;
v104 = v70;
v94 = v70;
}
while ( v150 != 10 );
v132 = (unsigned int)(v159 + 1);
v92 = v104;
}
while ( (_DWORD)v159 != 8 );
v48 = v108;
v108[8] = 0;
*(_QWORD *)v48 = 0LL;
v127 = 0LL;
do
{
v41 = v127;
v42 = 9 - v127;
if ( !(_DWORD)v127 )
v42 = 0;
v43 = *((_QWORD *)v192 + v42);
v44 = v127 + 1;
v45 = v43 % (unsigned int)(int)(pow(v40, (double)((int)v127 + 1)) + 0.5);
v46 = pow(11.0, (double)v41) + 0.5;
v47 = (unsigned int)(int)v46;
v40 = v46 - 9.223372036854776e18;
v103[v45 / v47] = 1;
v143 = 1LL;
v91 = v104;
v127 = v44;
}
while ( v44 != 9 );
do
{
v49 = v91;
if ( !v103[v143] )
v49 = 0;
++v143;
v117 = v49;
v91 = v49;
}
while ( v143 != 10 );
v16 = v108;
v108[8] = 0;
*(_QWORD *)v16 = 0LL;
v139 = 0LL;
do
{
v76 = v139 + 1;
v77 = v139 == 8;
v78 = v139 + 1;
if ( v139 == 8 )
v78 = 0;
v79 = *((_QWORD *)v192 + v139);
v80 = v79 % (unsigned int)(int)(pow(v40, (double)(v78 + 1)) + 0.5);
v81 = pow(11.0, (double)v78) + 0.5;
v82 = (unsigned int)(int)v81;
v40 = v81 - 9.223372036854776e18;
v103[v80 / v82] = 1;
v148 = 1LL;
v93 = v117;
v139 = v76;
}
while ( !v77 );
do
{
v83 = v93;
if ( !v103[v148] )
v83 = 0;
++v148;
v118 = v83;
v93 = v83;
}
while ( v148 != 10 );
return 0;
}

0x03 Solve the Puzzles

PART ONE

之前也提到过,由于我们的输入部分流可能执行不到,很明显我们刚刚根本没有输入上下左右箭头啥的

所以关于处理上下左右箭头的代码无了

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
do
{
v72 = v4;
input1 = getc(stdin);
v73 = input1 << 24;
shift_input1 = input1 << 24 == 0x1B000000;
if ( input1 << 24 == 0x31000000 )
shift_input1 = 2;
if ( v73 == 0x37000000 )
shift_input1 = 3;
if ( v73 == 0x33000000 )
shift_input1 = 4;
if ( v73 == 0x34000000 )
shift_input1 = 5;
count = v5;
v102 = v72;
v119 = v72;
if ( shift_input1 )
{
if ( shift_input1 == 1 )
_clang_call_terminate((void *)5);
if ( shift_input1 == 2 )
{
v107 = v102 + (4LL << (3 * (unsigned __int8)count));
org_input = input1;
}
else if ( shift_input1 == 3 )
{
v107 = v102 + (5LL << (3 * (unsigned __int8)count));
org_input = input1;
}
else
{
if ( shift_input1 == 4 )
v107 = v102 + (6LL << (3 * (unsigned __int8)count));
else
v107 = v102 + (7LL << (3 * (unsigned __int8)count));
org_input = input1;
}
s[count] = org_input;
v119 = v107;
}
v5 = count + 1;
v174 = v119;
}
while ( count != 11 );

这个时候就可以更改我们的输入(指的是输入箭头再输入字符)再来一遍

成功解析出我们的第一段输入

image-20220721003105302

由于两个文件分析过程不贴了,可以直接看官方WP给出的源码

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
int part1_size = 12;
while(count < part1_size) {
char a = getchar();
if (a == 27) {
if (getchar() == 91) {
char c = getchar();
try {
rmCjJ0(true, c);
} catch(Le3KW5 &cc) {
char c = cc.state;
if (c == 65) {
state += 0ull << (3 * count);
} else if (c==66) {
state += 2ull << (3 * count);
} else if (c==67) {
state += 1ull << (3 * count);
} else if (c==68) {
state += 3ull << (3 * count);
}
}
flag[count] = c;
}
} else if (a=='1') {
state += 4ull << (3 * count);
flag[count] = a;
} else if (a=='7') {
state += 5ull << (3 * count);
flag[count] = a;
} else if (a=='3') {
state += 6ull << (3 * count);
flag[count] = a;
} else if (a=='4') {
state += 7ull << (3 * count);
flag[count] = a;
}
count += 1;
}
// ... Second Part ...
// Check Part
if (... && state == 0xb3e659480) {
std::cout << LIT("Congratulation! \n") << LIT("Your flag is ACTF{") << flag << LIT("_amazing!}") << std::endl;
}

PART TWO

这个部分完全跟着lchild的分析来了

接着就是第二段输入

首先是经过一段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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
  if ( (_DWORD)v50 == 4 )
{
do
{
v106 = 0LL;
v149 = v146;
do
{
baseTable[v106 + 16] = byte_40E071[v106] - byte_40E0B2[v106];// baseTable
++v106;
}
while ( v106 < 0x41 );
v56 = (__int64)v163;
*(_QWORD *)v163 = v164;
v165 = 64LL;
v169 = (_OWORD *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::_M_create(
v56,
&v165,
0LL);
v9 = (void **)v163;
v10 = v169;
*(_QWORD *)v163 = v169;
v11 = v165;
*(_QWORD *)v164 = v165;
v12 = MEMORY[5];
v13 = MEMORY[0x15];
v14 = MEMORY[0x25];
v10[3] = MEMORY[0x35];
v10[2] = v14;
v10[1] = v13;
*v10 = v12;
*(_QWORD *)v187 = v11;
*((_BYTE *)v10 + v11) = 0;
v15 = v149;
*(&copy_input1 + v15) = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::find(
v9,
(unsigned int)*(&copy_input1 + v149),
0LL);
v177 = *v9;
operator delete(v177);
v146 = v149 + 1;
}
while ( v149 != 3 );
v17 = *v186;
*v183 = (4 * *v57) | ((unsigned __int8)*v186 >> 4) & 3;
v18 = *v185;
*v59 = (16 * v17) | ((unsigned __int8)*v185 >> 2) & 0xF;
*v184 = *v58 + (v18 << 6);
v152 = v110;
v151 = 0LL;
do
{ // 对输入进行操作分值操作
v6 = v151;
v7 = (unsigned __int8)*(&v99 + v151) / 0xAu;
v8 = v152;
baseTable[v152 + 96] = (unsigned __int8)*(&v99 + v151) % 0xAu;
baseTable[v8 + 97] = v7;
v151 = v6 + 1;
v152 = v8 + 2;
v182 = v8 + 2;
}
while ( v6 != 2 );
v130 = 0LL;
v112 = v182;
}
v128 = v130;
v113 = v112;
copy_len = v133;
v147 = v181;
}
while ( v133 ); // 以上是对input进行了base64解码

之后计算了九个数值,和一堆pang臭的代码,不过干的事情不是很复杂

image-20220721003955450

第一个循环是复制,后两个循环判断行列,不难发现这是个数独,拿网站一把梭了

具体参考lchild师傅的Write up

# https://sudoku.vip/sudoku-x-solver/

0x04 GetFlag!!

第一个解密就直接移回去即可

第二个解密出数独的值,列移动,取出值恢复原权位值,最后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
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
s = []
t = 0xB3E659480
# 每3个字节为一次输入
for i in range(12):
s.append(t & 0x7)
t >>= 3

assert t == 0
key = ''
for i in s:
if i == 0: key += '↑'
elif i == 1: key += '→'
elif i == 2: key += '↓'
elif i == 3: key += '←'
elif i == 4: key += '1'
elif i == 5: key += '7'
elif i == 6: key += '3'
elif i == 7: key += '4'
print(key) # ??↓↓→←→←3417


values = [0x00000000331b6d84, 0x0000000054cab29a, 0x000000000cd0afcd,
0x000000006636db08, 0x0000000000021528, 0x0000000005d62020, 0x00000000070bc7c1,
0x00000000006739bd, 0x00000000001b084a]
table = []
for i in values:
table.append([])
s = ''
value = i
for j in range(9):
table[-1].append(int(value % 11))
s += "%2d" % (value % 11)
value /= 11
# print(s[2: ] + s[: 2])
'''
0 0 0 0 0 0 0 4 0
0 0 5 0 0 0 7 6 0
0 0 0 0 4 0 0 1 0
0 0 0 0 0 0 0 8 0
0 6 3 9 0 0 0 0 0
0 0 0 0 3 0 5 0 0
2 9 0 0 8 0 6 0 0
0 7 0 0 9 3 0 0 0
3 0 0 0 0 1 0 0 0
'''

# print(sum(table, []).count(0))
# https://sudoku.vip/sudoku-x-solver/

solves = [
[8, 1, 6, 7, 5, 2, 3, 4, 9],
[4, 3, 5, 8, 1, 9, 7, 6, 2],
[7, 2, 9, 3, 4, 6, 8, 1, 5],
[9, 4, 7, 1, 6, 5, 2, 8, 3],
[5, 6, 3, 9, 2, 8, 4, 7, 1],
[1, 8, 2, 4, 3, 7, 5, 9, 6],
[2, 9, 1, 5, 8, 4, 6, 3, 7],
[6, 7, 4, 2, 9, 3, 1, 5, 8],
[3, 5, 8, 6, 7, 1, 9, 2, 4]
]

# 数独列右移
for i in range(9):
solves[i] = [solves[i][-1]] + solves[i][: -1]
# print(solves[i])

numbers = []
for y in range(9):
for x in range(9):
if table[y][x] == 0:
# print(table[y][x])
numbers.append(solves[y][x])

assert len(numbers) % 2 == 0

flag = ''
for i in range(0, len(numbers), 2):
flag += chr(numbers[i] + 10 * numbers[i + 1])

import base64
# print(flag)
print(base64.b64encode(str.encode(flag)))

# ↑↑↓↓→←→←3417
# WT05ICpTW0tcPyYxETgMGTBDUSphES1TLgwtVUwd

最后输入上上下下右左右左3417再二段

GetFlag!!

image-20220721005233195

总结

对于我这种0 throw catch小白0 ollvm小白,过程中是补了不少文章…学识尚浅如果有错误的地方欢迎指出,一定会严加学习修改!

最后拿到flag,也是对这题的一个happy ending,真是REVERSE生涯中目前做的最难的一题了

回忆起最开始wjh师傅教我的unicorn再到Helen师傅出的login,这次又上了台阶!逆向真是越来越有趣了。–7.21 00:55

DASCTF X SU
🍬
HFCTF2022
🍪

About this Post

This post is written by P.Z, licensed under CC BY-NC 4.0.