类型混乱确实是一个很有趣也是很精巧的一个内存漏洞。而type confusion导致RCE(remote code execution)在最近几年也多了起来。
其实就像是控制流劫持(control flow hijack)对象强制类型转换后导致虚拟页表的混乱,本来p类不能使用的经过强制类型转换后,可以使用d类的虚函数表并使用其中的函数。
具体到程序中的实际使用情况如下:
class Base{...}
calss Exec:public Base{
public:
virtual void exec(const char *prg){
system(prg);
}
};
class Greet:public Base{
public:
virtual void sayHi(const char *str){
std::cout<<str<<std::endl;
}
};
int main(){
Base *b1 = new Greeter();
Base *b2 = new Exec();
Greeter *g;
g = static_cast<Greeter*>(b1);//g[0][0](str)
g->sayHi("say hi!");
g = static_cast<Greeter*>(b2);//g[0][0](str)
g->sayHi("/usr/bin/xcalc");
delete b1;
delete b2;
return 0;
}
这种攻击就是在软件读取缓冲区结束或者开始之前的数据。通常。这种攻击会从其他不属于自己的内存区域得到信息或者造成crash。当代码读取大量的的数据并且读到一个终止符去停止时就会产生崩溃。这种预期的标记可能没有设置边界外内存,导致读取过多的数据,导致错误读取或缓冲区溢出。这时软件可能会修改下标,并且改变指针算数的行为,让它指向边界外的缓冲区,这样就会得到一个未授权的数据。
举个例子:
int getValueFromArray(int *array,int len,int index){
int value;
//check that the array index is less than the maximum
//length of the array
if(index < len){
//get the value at the specified index of the array
value = array[index];
}
//if array index is invalid
else{
printf("Value is :%d\n", array[index]);
value = -1;
}
return value;
}
这函数只是检查了数组下标是否是小于最大值,而没有检查是否是大于最小值,当让这种错误一般大家都不会犯,但是我所举的是攻击的简单例子,而一种攻击可以衍生出很多类型的攻击,比如说很有名的heart bleed(CVE-2014-0160)就是一个变种,有兴趣可以去看看真的非常有意思!!
先说栈溢出,栈溢出是由于错误代码导致栈所在的内存被访问并改变了这些在这些内存区域的值。最常见的内存溢出攻击:
1.
#include
int main()
{
int a = 0;
a += 1;
main();
return 0;
}
在这个例子中我们在main函数中声明了一个变量 a,一直运行就会重复声明直到内存消耗完毕。然后运行这个命令:
~/practice $ ./rcrsn
Segmentation fault
这里我们看到,只要我运行上面的程序,就会出现一个seg-fault/崩溃。让我们修改一下代码,添加一个调试printf语句:
#include
unsigned int count = 1;
int main(void)
{
int a=0;
a += 1;
printf("\n [%d] \n",count++);
main();
return 0;
}
这样就可以知道运行了多少次:
...
...
...
...
...
[327293]
[327294]
[327295]
[327296]
[327297]
[327298]
[327299]
[327300]
[327301]
[327302]
[327303]
[327304]
[327305]
Segmentation fault
这样就发生了一个简单的栈溢出。
2.访问数组外
#include
unsigned int count = 1;
int main(void)
{
int b = 10;
int a[3];
a[0] = 1;
a[1] = 2;
a[2] = 3;
printf("\n b = %d \n",b);
a[3] = 12;
printf("\n b = %d \n",b);
return 0;
}
在上面的代码中,我声明了一个有3个整数的数组。但是假设,如果代码试图更改保存在[3](这是一个数组不能访问的内存位置)的值,那么让我们看看会发生什么。下面是输出:
~/practice $ ./rcrsn
b = 10
b = 12
在上面的输出中,我们看到’b’的值从10变为12。但问题是,我们并没有在代码中显式地更改’b’,这是如何发生的呢?让我们在代码中加入一些调试打印语句,这样代码就变成:
#include
unsigned int count = 1;
int main(void)
{
int b = 10;
int a[3];
a[0] = 1;
a[1] = 2;
a[2] = 3;
printf("\n b = %d \n",b);
printf("\n address of b = %x, address of a[3] = %x \n",&b, &a[3]);
a[3] = 12;
printf("\n b = %d \n",b);
return 0;
}
可以看一下输出:
~/practice $ ./rcrsn
b = 10
address of b = a246661c, address of a[3] = a246661c
b = 12
现在我们看到,由语句a[3]对存储“b”值的内存地址进行非法访问。因此,我们看到’b’的值被悄悄地改变了。
3.使用保存无用地址的未定义或已释放的指针
#include
unsigned int count = 1;
int main(void)
{
int b = 10;
char *ptr;
printf("\n garbage address held by ptr = %x\n", ptr);
return 0;
}
在上面的代码中,我们发现指针ptr没有初始化。这是一种错误的做法,但即使在专业编写的代码中,有时也会发生这种情况。让我们看看输出:
~/practice $ ./rcrsn
garbage address held by ptr = 303e1f80
现在,我们看到那个指针有一些无用地址,当我们说无用地址时,我们的意思是任何数字都可以存储在那里。有可能这个无用地址是整数“b”。如果我们偶然访问指针,我们可能不会得到任何运行时错误和’b’的值可以悄悄地改变。
现在来说一下堆溢出,就是在输入超出了与分配的额空间大小,就会覆盖该空间之后的一段存储区域。如果该区域刚好就比较重要的的数据就,嘭!出事了。
具体例子就借用一下别人的东西了:探索Heap Corruption
在VC里面,用release模式编译运行程序的时候,堆分配(Heap allocation)的时候调用的是malloc,如果你要分配10byte的空间,那么就会只分配10byte空间,而用debug模式的时候,堆分配调用的是_malloc_dbg,如果你只要分配10byte的空间,那么它会分配出除了你要的10byte之外,还要多出约36byte空间,用于存储一些薄记信息,debug堆分配出来之后就会按顺序连成一个链。
那么我们再来看看薄记信息中有些什么。还是上面10byte分配空间的例子,那么分配出的10byte空间的前面会有一个32byte的附加信息,存储的是一个_CrtMemBlockHeader结构,可以在DBGINT.H中找到该结构的定义:
typedef struct _CrtMemBlockHeader
{
// Pointer to the block allocated just before this one:
struct _CrtMemBlockHeader *pBlockHeaderNext;
// Pointer to the block allocated just after this one:
struct _CrtMemBlockHeader *pBlockHeaderPrev;
char *szFileName; // File name
int nLine; // Line number
size_t nDataSize; // Size of user block
int nBlockUse; // Type of block
long lRequest; // Allocation number
// Buffer just before (lower than) the user's memory:
unsigned char gap[nNoMansLandSize];
} _CrtMemBlockHeader;
/* In an actual memory block in the debug heap,
* this structure is followed by:
* unsigned char data[nDataSize];
* unsigned char anotherGap[nNoMansLandSize];
*/
结构中的_CrtMemBlockHeader结构两个指针就不用解释是干嘛的了,szFileName是存储的发起分配操作的那行代码所在的文件的路径和名称,而nLine则是行号。nDataSize是请求分配的大小,我们的例子里当然就是10了,nBlockUse是类型,而lRequest是请求号。最后一项gap,又称NoMansLand,是4byte(nNoMansLandSize=4)大小的一段区域,注意看最后几行注释就明白了,在这个结构后面跟的是用户真正需要的10byte数据区域,而其后还跟了一个4byte的Gap,那么也就是说用户申请分配的区域是被一个头结构,和一个4byte的gap包起来的。在释放这10byte空间的时候,会检查这些信息。Gap被分配之后会被以0xFD填充。检查中如果gap中的值变化了,就会以Assert fail的方式报错。不过vc6中提示的比较难懂,DAMAGE :after Normal block(#dd) at 0xhhhhhhhh,而vs2005里面会提示Heap Corruption Detected!而如果你是release版本,那么这个错误就会潜伏直到它的破坏力发生作用。也许其后的区域存储着一个除数,而你的heap corruption把它改写成了0,那么会怎么样呢?
实际堆溢出内部结构更加复杂,共勉!
这个use after free攻击的利用过程和堆溢出非常的相似。通过合适的堆内存布局中,被释放的内存被受控的Vector数据占用,落入其中一个易受攻击的内存漏洞。调用悬空指针的成员函数后,完全可控的内存写入将破环一段区域,其他方面就和堆溢出类似。
这里附上一个讲解:use-after-free