网络安全

位置:IT落伍者 >> 网络安全 >> 浏览文章

缓沖区溢出还是问题吗?C++/CLI安全编码


发布日期:2020年09月07日
 
缓沖区溢出还是问题吗?C++/CLI安全编码
C++/CLI是对C++的一个扩展其对所有类型包括标准C++类都添加了对属性事件垃圾回收及泛型的支持

Visual C++ 扩展了对使用C++/CLI(通用语言基础结构)开发运行于带有垃圾回收的虚拟机上的控件及应用程序的支持而C++/CLI是对C++编程语言的一个扩展其对所有类型包括标准C++类都添加了如属性事件垃圾回收及泛型等特性

Visual C++ 支持NET Framework通用语言运行时库(CLR)其是垃圾回收虚拟机Microsoft的实现Visual C++ NET编程的C++语法支持是从Visual C++ NET 中引入的托管扩展C++演化而来的托管扩展C++仍然被支持但在倾向于新语法的情况下已不赞成使用Visual C++ 同时也对本地编程添加了新的特性包括位处理器架构支持及提高了安全性的新库函数

在本文中将主要讲解在以最小代价把现有老系统移植到使用CLR的新环境中来时所要面临的问题目的是为了确定这些程序是否仍然易受折磨C/C++程序多年的缓沖区溢出的影响

会要求用户输入用户名及密码除去用户名之外程序只接受NCC为有效的密码如果用户输入了错误的密码程序将退出(这个程序只是作为C++/CLI代码的漏洞测试而不是演示如何处理密码) 例

#include <stdlibh>

#include <stdioh>

#include <windowsh>

char buff[];

struct user {

char *name;

size_t len;

int uid;

};

bool checkpassword() {

char password[];

puts(Enter character password:);

gets(password);

if (strcmp(password NCC) == ) {

return true;

}

else {

return false;

}

}

int main(int argc char *argv[]) {

struct user *userP = (struct user *)xcdcdcdcd;

size_t userNameLen = xdeadbeef;

userP = (struct user *)malloc(sizeof(user));

puts(Enter user name:);

gets(buff);

if (!checkpassword()) {

userNameLen = strlen(buff) + ;

userP>len = userNameLen;

userP>name = (char *)malloc(userNameLen);

strcpy(userP>name buff); // log failed login attempt

exit();

}

}

程序从行的main()开始执行行使用了一对puts()和gets()来提示输入用户名导致了一个从标准输入到缓沖区字符数组(声明在第行)的不受控制的字符串复制程序中的这两处地方都有可能会导致一个缓沖区溢出的漏洞checkpassword()函数由main()中的行调用并在行中提示用户输入密码这也是使用了一对puts()/gets()对gets()的第二次调用也会导致一个定义在堆栈上的密码字符数组缓沖区溢出

程序使用Microsoft Visual C++ 编译并关闭了缓沖区安全检查选项(/GS打开了托管扩展(/clr)默认情况下缓沖区安全检查是打开的把它关闭并不是个好做法(如本例所示)而/clr选项可允许由托管及非托管代码生成混合的程序集

程序生成过程中产生的几个警告信息都可以忽略掉例如warning C: gets was declared deprecatedwarning C: strcpy was declared deprecated编译器推荐使用gets_s()来代替gets()用strcpy_s()来代替strcpy()如果完全使用这些替代函数那么就可消除缓沖区溢出潜在的可能性然而这些只是警告信息可以忽略甚至关闭忽略这些警告信息是符合用最小的代价移植现有老系统这个前提的

当使用托管扩展时编译器会为main()及checkpassword()函数生成Microsoft媒介语言(MSIL或称为通用媒介语言CIL)CIL字节码会被打包进一个可执行文件在调用即时编译器(JIT)将其翻译为本地程序集指令后接着把控制权交给main()

程序运行时提示用户输入用户名

Enter user name:

rcs

接着程序要求用户输入密码其被读入到声明在行上的个字符数组这个变量中在插如果在密码从标准输入读取之前查看堆栈上的数组地址起始处的数据(本例中为xDFD将会看到分配给密码的存储空间(以黑体字标出)及堆栈上的返回地址(以红色字标出)返回地址在此为小尾字节序(Little Endian)

代码段堆栈上数组地址起始处的数据

DFD f d a b e ycT

DFE f d f f a a e y:N

DFF a b f f d da c fc f d +/yx

DF f d H`@PST

倘若输入了更多的字符以致密码字符数组存储空间无法容纳一个攻击者就可以溢出此缓沖区并以shellcode(可为任意的代码)地址覆盖掉返回地址出于演示的目的在此假定shellcode已被注入且定位于x为执行此代码攻击者只需把下列字符串作为密码输入

Enter character password:

|@

这个输入的字符串被复制到密码字符数组溢出了此缓沖区并覆盖相应的内存包括返回地址字符串中的三个字符|@覆盖了返回地址的前三个字节而返回地址的最后一个字节被一个由gets()函数产生的null结尾字符所覆盖注意如果这个null不在最后一个字节上那么不可能复制整个字符串因为gets()函数会把这个null字符解释为字符串的结尾那为什么要以上这三个字符呢?因为这些字符的十六进制形式提供了内存中表示地址所需的值的ASCII十六进制码为x|x@x如果把这三个字符以顺序{ | @ }连接起来就可将shellcode(x)地址的小尾字节序表示形式写入到内存中最后一个null字节 由字符串的null字符提供(见代码段

代码段

DFD

DFE a e @y:N

DFF a b f f d da c fc f d +/yx

DF f d H`@PST

当checkpassword()函数返回时控制权就传到shellcode而不是main()函数中的原始返回地址上

为了简化这个攻击过程在此关闭了缓沖区安全检查选项/GS如果这个选项没有关闭编译器将会在声明在堆栈上的任何数组(缓沖区)之后插入一个密探实际上为一个Cookie见图

基于密探的缓沖区溢出保护

如果要使用那些不受控制的字符串复制操作如gets()或strcpy()来覆盖掉由密探保护的返回地址(EIP)基指针(EBP)或堆栈上的其他值一个攻击者将首先要覆盖掉这个密探如果密探被修改了当函数返回时将会产生一个错误导致攻击失败除非是为了进行拒绝服务攻击通过暴力枚举猜测这个值或其他方法还是有可能挫败这个密探但是进行一次成功攻击的难度增加了

打开/GS选项不会让程序对缓沖区溢出漏洞彻底免疫堆栈中的缓沖区溢出仍会使程序崩溃攻击者利用基于堆栈的溢出来执行任意代码的可能性即使在打开/GS的情况下仍然存在更重要的是/GS选项不会检测堆中或数据段中的缓沖区溢出

为举例说明使用Win GUI重写了前面那个示例程序这个程序提供一个带有一些简单选项的菜单栏File菜单下有两个菜单项LoginExitLogin会用一个对话框来提示用户输入密码一旦输入了密码在用户点击OK按钮之后将把输入的密码与之前记录的密码相比较

#include stdafxh

#include TestItDanh

#include <stdlibh>

#include <stdioh>

#include <windowsh>

#define MAX_LOADSTRING

struct user {

wchar_t *name;

size_t len;

int uid;

};

HINSTANCE hInst;

TCHAR szTitle[MAX_LOADSTRING];

TCHAR szWindowClass[MAX_LOADSTRING];

TCHAR lpszUserName[] = Lguest;

TCHAR lpszPassword[] = Labcde;

struct user *userP = (struct user *)xcdcdcdcdcdcdcdcd;

size_t userNameLen = ;

size_t userPasswordLen = xffffffff;

int APIENTRY _tWinMain(HINSTANCE hInstance

HINSTANCE hPrevInstance

LPTSTR lpCmdLine

int nCmdShow) {

UNREFERENCED_PARAMETER(hPrevInstance);

UNREFERENCED_PARAMETER(lpCmdLine);

MSG msg;

HACCEL hAccelTable;

LoadString(hInstance IDS_APP_TITLE szTitle MAX_LOADSTRING);

LoadString(hInstance IDC_TESTITDAN szWindowClass MAX_LOADSTRING);

MyRegisterClass(hInstance);

userP = (struct user *)malloc(sizeof(user));

if (!InitInstance (hInstance nCmdShow)) {

return FALSE;

}

hAccelTable =LoadAccelerators(hInstance MAKEINTRESOURCE(IDC_TESTITDAN));

while (GetMessage(&msg NULL )) {

if (!TranslateAccelerator(msghwnd hAccelTable &msg)) {

TranslateMessage(&msg);

DispatchMessage(&msg);

}

}

return (int) msgwParam;

}

INT_PTR CALLBACK GetPassword(HWND hDlg UINT message WPARAM wParam LPARAM lParam) {

TCHAR lpszGuestPassword[] = LNCC;

UNREFERENCED_PARAMETER(lParam);

switch (message) {

case WM_INITDIALOG:

return (INT_PTR)TRUE;

case WM_COMMAND:

if (LOWORD(wParam) == IDOK) {

EndDialog(hDlg LOWORD(wParam));

SendDlgItemMessage(hDlg

IDC_EDIT

EM_GETLINE

(WPARAM) // line

(LPARAM) lpszPassword

);

userP>len = userNameLen;

if (wcscmp(lpszPassword lpszGuestPassword) == ) {

return true;

}

else {

MessageBox(hDlg

(LPCWSTR)LInvalid Password

(LPCWSTR)LLogin Failed

MB_OK

);

}

return (INT_PTR)TRUE;

}

break;

}

return (INT_PTR)FALSE;

}

程序编译及测试的环境均与前例相同除了在此使用了Unicode字符集及打开了缓沖区安全检查选项(/GS)我们在此继续使用托管扩展(CLR)

这是一个非常简单的程序尽管为了支持Windows GUI它显得稍微有点长有几个有意思的变量lpszPassword是一个由个宽字符(字节)组成的已初始化的静态变量紧跟其后的是userP指针及两个无符号整形userNameLen和userPasswordLen之后userP在行初始化这些变量的地址如下

&lpszPassword = xC

&userP = xC

&userNameLen = x

&userPasswordLen = x

userP的值为xDuserNameLen的值为xuserPasswordLen的值为xffffffff如果我们查看lpszPassword地址的起始处内存可以非常清楚地看到这些变量的初始值(见插

代码段

C

C

C d ff ff ff ff a

C c

此程序中的漏洞是在行中对SendDlgItemMessage的调用EM_GETLINE消息指定了从编辑控件IDC_EDIT获取一行文本编辑控件在Login对话框中并把它复制到定长缓沖区lpszPassword中这个缓沖区只能容纳个Unicode字符及一个结尾的null如果输入了多于个字符就会发生缓沖区溢出在此假设输入了个字符个字符将会覆盖掉userP个字符将会覆盖掉userNameLen结尾的null将会覆盖掉userPasswordLen

假定userP与userNameLen两者都被覆盖当userNameLen被赋给存储在userP+(user结构内len的偏移地址)的地址时行就会导致对内存的任意写入通过把一个地址覆盖为控制权最终要传递到的地址攻击者就能利用内存的任意写入把控制权传给任意的代码而在本例中堆栈上的返回地址被覆盖了

因为lpszGuestPassword变量是一个声明在GetPassword函数中的自动变量我们也可以查看这个变量地址起始处的内存假定lpszGuestPassword定位在xDEBC那么可在这个位置查看堆栈的内容经由程序调试可以确定xfa的返回码位于堆栈上的xDEBD处(见插

代码段

DEBC e d

DEBAC

DEBBC e df b bd ec d

DEBCC ec eb d a f

DEBDC b f ec d da c fc f d

假定shellcode已被注入到程序中的x那么接下来攻击者可在Login对话框的密码输入栏中输入以下字符串

\xebcc\xd\x\x

在缓沖区溢出之后数据段的内存显示见插

代码段

C

C

C cc eb d ff ff a

C c

棕色的字节表示userP的值在何处被堆栈上的返回代码地址所覆盖(负绿色的字节表示userNameLen的值在何处被shellcode的地址所覆盖行的内存任意写入执行之后堆栈现在如插所示

代码段

DEBC e d

DEBAC

DEBBC e df b bd ec d

DEBCC ec eb d e

DEBDC b f ec d da c fc f d

红色表示的字节标出了堆栈上的返回值在何处被地址值所覆盖在这并没有修改堆栈上的其他任何字节(包括密探使得运行时的系统很难发现这次攻击结果控制权在GetPassword()函数返回时传到了shellcode中

让我们再来回顾一下首先它演示了堆栈上的返回地址仍可被覆盖甚至在打开缓沖区安全检查(/GS)的情况下这些安全检查只会减轻声明在堆栈上的自动变量缓沖区溢出其次它也说明了一个在Visual Studio 环境中编译时毫无警告信息的程序并不是没有漏洞可言就消除了这个缓沖区溢出在发送消息之前lpszPassword的第一个字设为以TCHAR表示的缓沖区大小对Unicode文本而言这表示字符数第一个字中的大小被复制进来的字符数所覆盖同样对编辑控件来说复制进来的字符串并不包含一个null结尾字符返回值(所复制的TCHAR数)必须再设为以null结尾的字符串

LRESULT Retval;

*((WORD *)(&lpszPassword)) = (sizeof(lpszPassword)/sizeof(TCHAR));

Retval = SendDlgItemMessage(hDlg IDC_EDIT EM_GETLINE

(WPARAM) // line

(LPARAM) lpszPassword

);

lpszPassword[Retval]=\;

               

上一篇:通过XML签名和加密安全地交换数据

下一篇:EntityFramework用法探索:线程安全实践