1. 一个跳过代码的例子

考虑如下代码(modified from modify-c-code-with-buffer-overflow-vulnerability-to-skip-code ):

#include <stdio.h>

#define NUMBER_OF_BYTES_TO_RETURN_ADDR 0 
#define OFFSET_TO_SKIP 0 

void 
func() {
  int a = 0;
  int *ret = &a;
  printf("ret:%p\n", ret);
  
  ret += NUMBER_OF_BYTES_TO_RETURN_ADDR;
  printf("ret:%p\n", ret);
  
  *ret = *ret + OFFSET_TO_SKIP;
}

int 
main() {
  int x;
  x = 0;
  func();
  x = 1;
  printf("x is:%d\n", x);

  return 0;
}

要求通过修改

  1. NUMBER_OF_BYTES_TO_RETURN_ADDR
  2. OFFSET_TO_SKIP

使main函数中printf打印出的x的值为0

1.1 分析

如果不修改值,直接编译运行程序:

$ ./a.out
ret:0xffb36314
ret:0xffb36314
x is:1

可以看出printf打印出x的值是1。

要想x打印的值为0,需要修改指定的两个变量的值,使得func函数执行完成,回到main函数继续执行时,跳过x=1语句,如下图:

Fig.1 skip code
Fig.1 skip code

那么如何做到呢?通过修改返回地址(return address)的值!

(关于return address的具体细节,参见理解Procedure Call实现(三)-return address

1.2 修改return address

1.2.1 计算return address和变量a的地址之差

要想修改return address,首先要知道return address的地址。

考察语句:

func() {
  int a = 0;
  int *ret = &a;
  ...

ret指向了栈上本地变量a,也就是保存a的地址。同时我们也知道了return address也同样被保存在栈上。

由于栈是由高低值向地址值扩展,并且return address是先被push到栈上,所以return address的地址要比变量a的地址高,如下图:

Fig.2 a to return address
Fig.2 a to return address

所以只要知道了变量a和return address在栈上的地址之差(即NUMBER_OF_BYTES_TO_RETURN_ADDR),也就能通过变量a修改return address的值。

而这个差值是可以通过gdb查看而计算出来的。

  • 使用gdb查看return address的地址:

gdb在func设置断点,运行程序,查看frame:

(gdb) i frame
Stack level 0, frame at 0xffffcfa0:
 eip = 0x56555581 in func; saved eip = 0x5655561b
 called by frame at 0xffffcfd0
 Arglist at 0xffffcf98, args: 
 Locals at 0xffffcf98, Previous frame's sp is 0xffffcfa0
 Saved registers:
  ebp at 0xffffcf98, eip at 0xffffcf9c

可以看到主函数return address(0x5655561b)被保存在栈0xffffcf9c

  • 使用gdb查看变量a的地址 从func():
(gdb) disas func
Dump of assembler code for function func:
   0x5655557d <+0>: push   %ebp
   0x5655557e <+1>: mov    %esp,%ebp
   0x56555580 <+3>: push   %ebx
=> 0x56555581 <+4>: sub    $0x14,%esp
   ...
   0x5655559a <+29>:  movl   $0x0,-0x14(%ebp)
   ...

可以看出指令:0x5655559a <+29>: movl $0x0,-0x14(%ebp)对应了代码int a = 0;,所以a的地址为-0x14(%ebp)。使用gdb查看:

(gdb) i r ebp
ebp            0xffffcf98 0xffffcf98

所以a的地址:0xffffcf84

return address - a = 0xffffcf9c - 0xffffcf84 = 24 bytes

所以NUMBER_OF_BYTES_TO_RETURN_ADDR的值应该设为6(x86),而代码ret += 6;也就指向了return address的地址。

1.2.2 修改return address

此时ret已经指向了return address,那么修改*ret,也就是修改了return address的值,如下图:

Fig.3 ret points to return address
Fig.3 ret points to return address

修改return address值,使得return address跳过x=1的指令。从main()的指令:

(gdb) disas main
Dump of assembler code for function main:
   ...
   0x56555616 <+36>:  call   0x5655557d <func>
   0x5655561b <+41>:  movl   $0x1,-0xc(%ebp)
   0x56555622 <+48>:  sub    $0x8,%esp
   0x56555625 <+51>:  pushl  -0xc(%ebp)
   ...

原来return address的值是0x5655561b,想要跳过这条指令<+41>,直接执行下一条指令<+48>,只需要把return address的值加上7个bytes,所以OFFSET_TO_SKIP的值就是7。

#define NUMBER_OF_BYTES_TO_RETURN_ADDR 6
#define OFFSET_TO_SKIP 7

编译执行:

ret:0xffa18e74
ret:0xffa18e8c
x is:0

成功跳过了语句x=1;

1.3 总结

这只是一个toy例子,现实中不会这么写代码。但这个例子可以作为谈溢出一个很好的引子,因为同样利用了经典溢出的关键:修改return address,以更改程序执行流,学术的说法是Control Flow Hijack