printf 成链攻击

TOC

  1. 1. 原理
  2. 2. 利用
  3. 3. 实例

printf 成链攻击在某些情况下,可以达到任意地址读写的目的,其威力不可小觑。

原理

简单来说就是利用printf%n来修改参数地址。

首先fmt必须要是可控的,但是有时候fmt并不在栈上,而是在堆上或者bss段中,这样对于一般的printf攻击就不能读写任意地址了,也就是说这时我们只能利用栈上已有的地址和数据。

而 printf 成链攻击就是对于fmt在堆上或者bss段中时,利用栈已有的地址和数据进行任意地址读写的攻击手法 。

最典型的特征如下:

这里我举出SUCTF2019 pwn playfmt的栈布局为例。

Breakpoint *0x0804889F
pwndbg> stack
00:0000│ esp 0xffe040f0 —▸ 0x804b040 (buf) ◂— '%6$p\n'
01:0004│ 0xffe040f4 —▸ 0x8048cac ◂— jno 0x8048d23 /* 'quit' */
02:0008│ 0xffe040f8 ◂— 0x4
03:000c│ 0xffe040fc —▸ 0x80488e8 (logo()+59) ◂— add esp, 0x10
04:0010│ 0xffe04100 —▸ 0x8048cb1 ◂— cmp eax, 0x3d3d3d3d /* '=====================' */
05:0014│ 0xffe04104 —▸ 0x8048ac4 (main+440) ◂— call 0x804884b
06:0018│ ebp 0xffe04108 —▸ 0xffe04128 —▸ 0xffe04158 ◂— 0x0
07:001c│ 0xffe0410c —▸ 0x80488f0 (logo()+67) ◂— call 0x804884b
pwndbg>
08:0020│ 0xffe04110 —▸ 0xf7e28000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x195db0
09:0024│ 0xffe04114 ◂— 0x0
0a:0028│ 0xffe04118 —▸ 0xffe04158 ◂— 0x0
0b:002c│ 0xffe0411c —▸ 0x8048ac4 (main+440) ◂— call 0x804884b
0c:0030│ 0xffe04120 —▸ 0xf7e28d60 (_IO_2_1_stdout_) ◂— 0xfbad2887
0d:0034│ 0xffe04124 ◂— 0x0
0e:0038│ 0xffe04128 —▸ 0xffe04158 ◂— 0x0
0f:003c│ 0xffe0412c —▸ 0x8048ac4 (main+440) ◂— call 0x804884b
pwndbg>
10:0040│ 0xffe04130 —▸ 0xf7e283dc (__exit_funcs) —▸ 0xf7e291e0 (initial) ◂— 0x0
11:0044│ 0xffe04134 ◂— 0x0
12:0048│ 0xffe04138 —▸ 0x96fea28 —▸ 0xf7e287b0 (main_arena+48) —▸ 0x96fee60 ◂— 0x0
13:004c│ 0xffe0413c —▸ 0x96fee30 ◂— 0x0
... ↓
17:005c│ 0xffe0414c —▸ 0xf7ca99fb (__libc_start_main+412) ◂— jmp 0xf7ca9a49

我们需要的就是像这样的栈结构06:0018│ ebp 0xffe04108 —▸ 0xffe04128 —▸ 0xffe04158 ,最明显的特征就是多个栈地址成了一条链。

利用

printf成链攻击的缺点就是需要很多次printf,但是如果你有任意地址读写权限后,相信做到这一点也并非难事。

当我们有这样的06:0018│ ebp 0xffe04108 —▸ 0xffe04128 —▸ 0xffe04158 一条栈数据链后,我们就可以利用printf%n对不同链层的数据进行修改,从而使得这条数据链可以指向任意地址。

这里我用上面的数据链简单举个例,我们可以利用0e:0038│ 0xffe04128 —▸ 0xffe04158 ◂— 0x0来修改0xffe04158地址的数据,但是printf%n最大只能修改到0x2000,也就是说我们一般只能修改一个byte,原本这并没有什么用,但是别忘了,这是一条完整的数据链,我们可以利用06:0018│ ebp 0xffe04108 —▸ 0xffe04128 —▸ 0xffe04158 修改0xffe04128的地址数据(0xffe04158)的低地址,简单来说就是修改为0xffe04159,然后0e:0038│ 0xffe04128 —▸ 0xffe04158 ◂— 0x0就会变成0e:0038│ 0xffe04128 —▸ 0xffe04159 ◂— 0x0,接下来我们就可以利用这条链修改0xffe04159地址的值,也就是第二个byte,依次类推,我们就能在栈上写任意地址,然后在用栈上的地址进行任意读写。就这样往往复复,造成了这个恶性循环。

简单来说就是一条栈数据链,前面的链功能是修改我们要任意读写的地址,后面的链的功能则是对前面修改出来的地址进行任意读写。

注意:这里我特别强调一点,在单次printf操作中,是没有办法完成printf成链攻击的,因为单次printf时,一旦你对已经修改过的地址的值进行修改时,则要不就直接crash,要不就根本没反应,所以一定要多次printf才能完成该攻击方式。这是我自己尝试过的,然后单次就能完成printf成链攻击,那么这将是一个非常致命的漏洞。

接下来我简单说一下利用方式,printf成链攻击的实施一般至少需要两次printf才行(除非你运气好到爆棚,恰好有一个地址指向了函数的返回地址),第一次我们可以使栈数据链指向某个函数的返回地址,一般为了简单我们可以直接指向第二次printf的返回地址,由于栈布局是固定的,我们确实可以预测其返回地址。然后第二次printf操作时,便可以劫持其返回地址,然后重新返回main或者指向一个可以让printf复用的地址,然后我们就可以重复使用printf实现任意地址读写,这样就完成了一次printf成链攻击。

实例