转自:http://www.codesec.net/view/537311.html

一、前言

最近版本的windows有一个新的缓解措施叫做控制流保护(CFG)。在一个非直接调用之前――例如,函数指针和虚函数――针对有效调用地址的表检查目标地址。如果地址不是一个已知函数的入口,程序将会终止运行。

如果一个程序有一个缓冲区溢出漏洞,攻击者可以利用它覆盖一个函数地址,并且通过调用那个指针来控制程序执行流。这是ROP攻击的一种方法,攻击者构建一系列配件地址链,一个配件是一组包含ret指令的指令序列,这些指令都是原始程序中的,可以用来作为非直接调用的起点。执行过程会从一个配件跳到另一个配件中以便做攻击者想做的事,却不需要攻击这提供任何代码。

两种非常广的缓解ROP攻击的技术是地址空间布局随机化(ALSR)和栈保护。前者是随机化模块的加载基址以便达到不可预料的结果。在ROP攻击中的地址依赖实时内存布局,因此攻击者必须找到并利用信息泄漏来绕过ASLR。

关于栈保护,编译器在其他栈分配之上分配一个值,并设置为每个线程的随机值。如果过缓冲区溢出覆盖了函数返回地址,这个值将也被覆盖。在函数返回前,将校验这个值。如果不能与已知值匹配,程序将终止运行。

CFG原理类似,在将控制传送到指针地址前做一个校验,只是不是校验一个值,而是校验目标地址本身。这个非常复杂,不像栈保护,需要平台协调。这个校验必须在所有的可靠的调用目标中被通知,不管是来自主程序还是动态库。

虽然没有广泛部署,但是值得一提的是Clang’s
SafeStack。每个线程有两个栈:一个“安全栈”用来保存返回指针和其他可安全访问的值,另一个“非安全栈”保存buffer之类的数据。缓冲区溢出将破环其他缓冲区,但是不会覆盖返回地址,这样限制了破环的影响。

二、利用例子

使用一个小的C程序,demo.c:

int
main(void)
{
charname[8];
gets(name);
printf("Hello,%s.\n",name);
return0;
}

它读取一个名字存到缓冲区中,并且以换行结尾打印出来。麻雀虽小五脏俱全。原生调用gets()不会校验缓冲区的边界,可以用来缓冲区溢出漏洞利用。很明显编译器和链接器都会抛出警告。

简单起见,假设程序包含危险函数。

void
self_destruct(void)
{
puts("****GOBOOM!****");
}

攻击者用缓冲区溢出来调用这个危险函数。

为了使攻击简单,假设程序不使用ASLR(例如,在GCC/Clang中不使用-fpie和-pie编译选项)。首先,找到self_destruct()函数地址。

$readelf-ademo|grepself_destruct
46:00000000004005c510FUNCGLOBALDEFAULT13self_destruct

因为在64位系统上面,所以是64位的地址。Name缓冲区的大小事8字节,在汇编我看到一个额外的8字节分配上面,所以有16个字节填充,然后8字节覆盖self_destruct的返回指针。

$echo-ne'xxxxxxxxyyyyyyyy\xc5\x05\x40\x00\x00\x00\x00\x00'>boom
$./demo<boom
Hello,xxxxxxxxyyyyyyyy?@.
****GOBOOM!****
Segmentationfault

使用这个输入我已经成功利用了缓冲区溢出来控制了执行。当main试图回到libc时,它将会跳转到威胁代码,然后崩溃。打开堆栈保护可以阻止这种利用。

$gcc-Os-fstack-protector-odemodemo.c
$./demo<boom
Hello,xxxxxxxxaaaaaaaa?@.
***stacksmashingdetected***:./demoterminated
=======Backtrace:=========
...lotsofbacktracestuff...

栈保护成功阻止了利用。为了绕过过这个,我将不得不猜canary值或者发现可以利用的信息泄漏。

栈保护转化为程序看起来就是如下这样:

int
main(void)
{
long__canary=__get_thread_canary();
charname[8];
gets(name);
printf("Hello,%s.\n",name);
if(__canary!=__get_thread_canary())
abort();
return0;
}

然而,实际上不可能在C中实现堆栈保护,缓冲区溢出是不确定行为,并且canary仅对缓冲区溢出有效,还允许编译器优化它。

三、函数指针和虚函数

在攻击者成功上述利用后,上层管理加入了密码保护措施。看起来如下:

void
self_destruct(char*password)
{
if(strcmp(password,"12345")==0)
puts("****GOBOOM!****");
}

这个密码是硬编码的,它是比较愚蠢,但是假设它不为攻击者所知。上层管理已经要求堆栈保护,因此假设已经开启。

另外,程序也做一点改变,现在用一个函数指针实现多态。

structgreeter{
charname[8];
void(*greet)(structgreeter*);
};
void
greet_hello(structgreeter*g)
{
printf("Hello,%s.\n",g->name);
}
void
greet_aloha(structgreeter*g)
{
printf("Aloha,%s.\n",g->name);
}

现在有一个greeter对象和函数指针来实现运行时多态。把他想想为手写的C的虚函数。下面是新的main函数:

int
main(void)
{
structgreetergreeter={.greet=greet_hello};
gets(greeter.name);
greeter.greet(&greeter);
return0;
}

(在真实的程序中,其他东西会提供greeter并挑选它自己的函数指针)

而不是覆盖返回指针,攻击者有机会覆盖结构中的函数指针。让我们重新像之前一样利用。

$readelf-ademo|grepself_destruct
54:00000000004006a510FUNCGLOBALDEFAULT13self_destruct

我们不知道密码,但是我们确实知道密码校验是16字节。攻击应该跳过16字节,即跳过校验(0x4006a5+16=0x4006b5)。

$echo-ne'xxxxxxxx\xb5\x06\x40\x00\x00\x00\x00\x00'>boom
$./demo<boom
****GOBOOM!****

不管堆栈保护还是密码保护都么有帮助。堆栈保护仅仅保护返回指针,而不保护结构中的函数指针。

这就是CFG起作用的地方。开启了CFG,编译器会在调用greet()之前插入一个校验。它必须指向一个已知函数的开头,否则将想堆栈保护一样终止程序运行。因为self_destruct()不是函数的开头,但是利用后程序还是会终止。

然而,linux还没有CFG机制。因此我打算自己实现它。

四、函数地址表

正如文中顶端PDF链接中描述的,Windows上面的CFG使用bitmap实现。每个位代表8字节内存。如果过8字节包含了函数开头,这个位设置为1。校验一个指针意味着校验在bitmap中它关联的位。

关于我的CFG,我决定保持相同的8字节解决方案:目标地址的低3位将舍弃。其余24位用来作为bitmap的索引。所有指针中的其他位被忽略。24位的索引意味着bitmap最大只能是2MB。

24位对于32位系统已经足够了,但是在64位系统上面是不够的:一些地址不能代表函数的开头,但是设置他们的位为1.这是可以接受的,尤其是只有已知函数作为非直接调用的目标,降低了不利因素。

注意:根据指针转化为整数的位是未指定的且不可移植,但是这个实现不管在哪里都能工作良好。

下面是CFG的参数。我将他们封装为宏以便编译是方便。这个cfg_bits是支持bitmap数组的整数类型。CFG_RESOLUTION是舍弃的位数,一次“3”是8字节的一个粒度。

typedefunsignedlongcfg_bits;
#defineCFG_RESOLUTION3
#defineCFG_BITS24

给一个函数指针f,下面的宏导出bitmap的索引。

#defineCFG_INDEX(f)\
(((uintptr_t)f>>CFG_RESOLUTION)&((1UL<<CFG_BITS)-1))

CFG bitmap只是一个整形数组。初始值为0。

structcfg{
cfg_bitsbitmap[(1UL<<CFG_BITS)/(sizeof(cfg_bits)*CHAR_BIT)];
};

使用cfg_register()在bitmap中手动注册函数。

void
cfg_register(structcfg*cfg,void*f)
{
unsignedlongi=CFG_INDEX(f);
size_tz=sizeof(cfg_bits)*CHAR_BIT;
cfg->bitmap[i/z]|=1UL<<(i%z);
}

因为在运行时注册函数,需要与ASLR一致。如果ASLR开启了,bitmap每次运行都会不同。将bitmap的每个元素与一个随机数异或是值得的,加大攻击者的难度。在完成注册后,bitmap也需要调整为只读权限(mprotect())。

最后,校验函数被用于非直接调用之前。它确保了f先被传递给cfg_register()。因为它调用频繁,所以需要尽量快和简单。

void
cfg_check(structcfg*cfg,void*f)
{
unsignedlongi=CFG_INDEX(f);
size_tz=sizeof(cfg_bits)*CHAR_BIT;
if(!((cfg->bitmap[i/z]>>(i%z))&1))
abort();
}

完成了,现在在main中使用它:

structcfgcfg;
int
main(void)
{
cfg_register(&cfg,self_destruct);//toprovethisworks
cfg_register(&cfg,greet_hello);
cfg_register(&cfg,greet_aloha);
structgreetergreeter={.greet=greet_hello};
gets(greeter.name);
cfg_check(&cfg,greeter.greet);
greeter.greet(&greeter);
return0;
}

现在再次利用:

$./demo<boom
Aborted

正常情况下self_destruct()不会被注册,因为它不是一个非直接调用的合法目标,但是利用依然不能起作用是因为它在self_destruct()中间被调用,在bitmap中它不是一个可靠的地址。校验将在利用前终止程序。

在真实的应用程序中,我将使用一个全局的CFG bitmap,在头文件中使用inline函数定义cfg_check()。

尽管不使用工具直接在C中实现是可能的,但是这将变得更加繁琐和意出错。正确的是该在编译器中实现CFG。

本文系统(linux)相关术语:linux系统 鸟哥的linux私房菜 linux命令大全 linux操作系统

在Linux中使用C语言实现控制流保护(CFG)【转】的更多相关文章

  1. linux中安装程序及账户管理

    程序安装及管理 1. Linux 应用程序基础  Linux命令与应用程序的关系 1):文件位置 系统命令:一般在/bin和/sbin目录中,或为Shell内部指令 应用程序:通常在/usr/bin和 ...

  2. Linux 下 expect 脚本语言中交互处理常用命令

    Linux 下 expect 脚本语言中交互处理常用命令 1. #!/usr/bin/expect 告诉操作系统脚本里的代码使用那一个 shell 来执行.这里的 expect 其实和 Linux 下 ...

  3. 1.5 Linux中大量使用脚本语言,而不是C语言!

    说到在 Linux 下的编程,很多人会想到用C语言,Linux 的内核.shell.基础命令程序,也的确是用C语言编写的,这首先证明了一点,C语言很强很通用. 到目前为止,C语言依然垄断着计算机工业中 ...

  4. linux中C语言发送广播报文

    2. 指令的解决方法: oute add -net 255.255.255.255 netmask 255.255.255.255 dev eth0 metric 1 或者 route add -ho ...

  5. 在 Linux 中安装 Oracle JDK 8 以及 JVM 的类加载机制

    参考资料 该文中的内容来源于 Oracle 的官方文档 Java SE Tools Reference .Oracle 在 Java 方面的文档是非常完善的.对 Java 8 感兴趣的朋友,可以直接找 ...

  6. 浅谈Linux中的信号处理机制(二)

    首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇<浅谈Linux中的信号处理机制(一)>的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉.本人自知功力不够,尚且不能对着Lin ...

  7. linux中test与[ ]指令的作用

    linux中test与[ ]指令的作用: 在Linux中,test和[ ]功能是一样的,类似于c语言中的( ).不过Linux的test和[ ]是指令.在和if或者while联用时要用空格分开.

  8. Windows转到linux中,文件乱码,文件编码转换 & 解决sqlplus连接oracle乱码

    转载:http://www.cnblogs.com/wanyao/p/3399269.html 最近,学习又重新开始Linux学习,所以一直在Centos中,昨天一朋友把他在Windows下写的C程序 ...

  9. 使用vim在Linux下编写C语言程序

    1.进入字符界面 2.创建文件夹用于存放源文件 mkdir helloworld    //创建文件夹命令 cd helloworld        //进入新建的文件夹,这里应该说目录比较好,win ...

随机推荐

  1. nginx 反向代理 ,入门

    入门:http://www.cnblogs.com/jjzd/p/6691500.html 启动,重新加载:http://blog.csdn.net/zhongguozhichuang/article ...

  2. Qt信号与槽机制

    一.信号和槽机制 信号和槽用于两个对象之间的通信,信号和槽机制是Qt的核心特征,也是Qt不同于其他开发框架的最突出的特征.在GUI编程中,当改变了一个部件时,总希望其他部件也能了解到该变化.更一般来说 ...

  3. iOS-系统 图片、视频 管理控制器UIImagePickerController

        UIImagePickerController 是一个管理系统多媒体文件库(相册)中的图片.视频文件的视图控制器,诞生于iOS4之前,虽然功能不是很完善,我们仍可以用这个视图控制器做一些有创造 ...

  4. 【bzoj4165】矩阵 堆+STL-map

    题目描述 定义和谐矩阵为长不小于 Mina 且宽不小于 Minb 的矩阵,矩阵的权值为整个矩阵内所有数的和.给定一个长为 N,宽为 M 的矩阵 A,求它的所有和谐子矩阵中权值第 K 小的矩阵,并输出它 ...

  5. 【bzoj1707】[Usaco2007 Nov]tanning分配防晒霜 贪心+Treap

    题目描述 奶牛们计划着去海滩上享受日光浴.为了避免皮肤被阳光灼伤,所有C(1 <= C <= 2500)头奶牛必须在出门之前在身上抹防晒霜.第i头奶牛适合的最小和最 大的SPF值分别为mi ...

  6. html的body内标签之input系列2

    一,input系列:name属性用于让后台拿数据.value 只是在屏幕上的显示. 1. input type='text' name='query' value="张三"(相当于 ...

  7. Greenlet-手动切换

    yield()是自己写的协程,Greenlet( )是已经封装好了的协程. 协程:遇到 I/O 操作就切换到别的地方了(先去处理其他携程去了).等原协程的 I/O 操作一完成就切回去.这样就把 I/O ...

  8. win10下ndk编译arm可执行体

    编译参考文章 http://blog.csdn.net/john_1984/article/details/12622215 一.编写soLoader主文件 soLoader.c内容: #includ ...

  9. eclipse里配置Android ndk环境,用eclipse编译.so文件

    做Android NDK开发时,c代码需要用ndk-build来进行编译,而java代码则需要用Android sdk编译. 编译c代码有两种方法: 一.写好c代码后,然后用cygwin搭建ndk-b ...

  10. ZOJ1586:QS Network (最小生成树)

    QS Network 题目链接:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=1586 Description: In th ...