我正在尝试自学C ++中的缓冲区溢出和利用。我是一个中级C ++家伙,充其量,所以请耐心等待。我已经学过几个教程,但这里有一些示例代码来说明我的问题:
#include <string>
#include <iostream>
using namespace std;
int main()
{
begin:
int authentication = 0;
char cUsername[10], cPassword[10];
char cUser[10], cPass[10];
cout << "Username: ";
cin >> cUser;
cout << "Pass: ";
cin >> cPass;
strcpy(cUsername, cUser);
strcpy(cPassword, cPass);
if(strcmp(cUsername, "admin") == 0 && strcmp(cPassword, "adminpass") == 0)
{
authentication = 1;
}
if(authentication)
{
cout << "Access granted\n";
cout << (char)authentication;
}
else
{
cout << "Wrong username and password\n";
}
system("pause");
goto begin;
}
我知道这里有各种各样的坏juju与cin << String
等等...无论如何,当我输入太多字母(例如A
一吨)到{{1和cUser
,我只是从Visual Studio获得访问冲突。但是,如果我输入20个cPass
,然后是空格,然后另一个A
输入A
,它会跳过询问我cUser
(假设因为它已被填充后空格字符导致前一次调用cPass
返回)并授予我访问权限。
在什么时候,以及为什么,数据溢出到“身份验证”中,为什么它只发生在我有空间而不是我有一百万cin
时...我永远不会得到“访问违规“当我在A
的输入中使用空格时。
答案 0 :(得分:22)
我稍微修改了你的程序,使其更具说明性:
#include <iostream>
int main( void )
{
int authentication = 0;
char cUsername[ 10 ];
char cPassword[ 10 ];
std::cout << "Username: ";
std::cin >> cUsername;
std::cout << "Pass: ";
std::cin >> cPassword;
if( std::strcmp( cUsername, "admin" ) == 0 && std::strcmp( cPassword, "adminpass" ) == 0 )
{
authentication = 1;
}
if( authentication )
{
std::cout << "Access granted\n";
std::cout << ( char )authentication;
}
else
{
std::cout << "Wrong username and password\n";
}
return ( 0 );
}
我用x64编译器命令行MS编译器编译它,没有优化。所以现在我们有一个我们想要“破解”的exe。我们使用WinDbg(非常好的调试器)加载程序并查看反汇编(注意,为了清楚起见,我提供了完整的调试信息):
00000001`3f1f1710 4883ec68 sub rsp,68h
00000001`3f1f1714 488b0515db0300 mov rax,qword ptr [Prototype_Console!__security_cookie (00000001`3f22f230)]
00000001`3f1f171b 4833c4 xor rax,rsp
00000001`3f1f171e 4889442450 mov qword ptr [rsp+50h],rax
00000001`3f1f1723 c744243800000000 mov dword ptr [rsp+38h],0 // This gives us address of "authentication" on stack.
00000001`3f1f172b 488d156e1c0300 lea rdx,[Prototype_Console!std::_Iosb<int>::end+0x78 (00000001`3f2233a0)]
00000001`3f1f1732 488d0d47f00300 lea rcx,[Prototype_Console!std::cout (00000001`3f230780)]
00000001`3f1f1739 e8fdf9ffff call Prototype_Console!ILT+310(??$?6U?$char_traitsDstdstdYAAEAV?$basic_ostreamDU?$char_traitsDstd (00000001`3f1f113b)
00000001`3f1f173e 488d542428 lea rdx,[rsp+28h] // This gives us address of "cUsername" on stack.
00000001`3f1f1743 488d0df6f00300 lea rcx,[Prototype_Console!std::cin (00000001`3f230840)]
00000001`3f1f174a e823faffff call Prototype_Console!ILT+365(??$?5DU?$char_traitsDstdstdYAAEAV?$basic_istreamDU?$char_traitsDstd (00000001`3f1f1172)
00000001`3f1f174f 488d153e1c0300 lea rdx,[Prototype_Console!std::_Iosb<int>::end+0x6c (00000001`3f223394)]
00000001`3f1f1756 488d0d23f00300 lea rcx,[Prototype_Console!std::cout (00000001`3f230780)]
00000001`3f1f175d e8d9f9ffff call Prototype_Console!ILT+310(??$?6U?$char_traitsDstdstdYAAEAV?$basic_ostreamDU?$char_traitsDstd (00000001`3f1f113b)
00000001`3f1f1762 488d542440 lea rdx,[rsp+40h] // This gives us address of "cPassword" on stack.
00000001`3f1f1767 488d0dd2f00300 lea rcx,[Prototype_Console!std::cin (00000001`3f230840)]
00000001`3f1f176e e8fff9ffff call Prototype_Console!ILT+365(??$?5DU?$char_traitsDstdstdYAAEAV?$basic_istreamDU?$char_traitsDstd (00000001`3f1f1172)
00000001`3f1f1773 488d15321c0300 lea rdx,[Prototype_Console!std::_Iosb<int>::end+0x84 (00000001`3f2233ac)]
00000001`3f1f177a 488d4c2428 lea rcx,[rsp+28h]
00000001`3f1f177f e86c420000 call Prototype_Console!strcmp (00000001`3f1f59f0)
00000001`3f1f1784 85c0 test eax,eax
00000001`3f1f1786 751d jne Prototype_Console!main+0x95 (00000001`3f1f17a5)
00000001`3f1f1788 488d15291c0300 lea rdx,[Prototype_Console!std::_Iosb<int>::end+0x90 (00000001`3f2233b8)]
00000001`3f1f178f 488d4c2440 lea rcx,[rsp+40h]
00000001`3f1f1794 e857420000 call Prototype_Console!strcmp (00000001`3f1f59f0)
00000001`3f1f1799 85c0 test eax,eax
00000001`3f1f179b 7508 jne Prototype_Console!main+0x95 (00000001`3f1f17a5)
00000001`3f1f179d c744243801000000 mov dword ptr [rsp+38h],1
00000001`3f1f17a5 837c243800 cmp dword ptr [rsp+38h],0
00000001`3f1f17aa 7426 je Prototype_Console!main+0xc2 (00000001`3f1f17d2)
00000001`3f1f17ac 488d15151c0300 lea rdx,[Prototype_Console!std::_Iosb<int>::end+0xa0 (00000001`3f2233c8)]
00000001`3f1f17b3 488d0dc6ef0300 lea rcx,[Prototype_Console!std::cout (00000001`3f230780)]
00000001`3f1f17ba e87cf9ffff call Prototype_Console!ILT+310(??$?6U?$char_traitsDstdstdYAAEAV?$basic_ostreamDU?$char_traitsDstd (00000001`3f1f113b)
00000001`3f1f17bf 0fb6542438 movzx edx,byte ptr [rsp+38h]
00000001`3f1f17c4 488d0db5ef0300 lea rcx,[Prototype_Console!std::cout (00000001`3f230780)]
00000001`3f1f17cb e825f9ffff call Prototype_Console!ILT+240(??$?6U?$char_traitsDstdstdYAAEAV?$basic_ostreamDU?$char_traitsDstd (00000001`3f1f10f5)
00000001`3f1f17d0 eb13 jmp Prototype_Console!main+0xd5 (00000001`3f1f17e5)
00000001`3f1f17d2 488d15ff1b0300 lea rdx,[Prototype_Console!std::_Iosb<int>::end+0xb0 (00000001`3f2233d8)]
00000001`3f1f17d9 488d0da0ef0300 lea rcx,[Prototype_Console!std::cout (00000001`3f230780)]
00000001`3f1f17e0 e856f9ffff call Prototype_Console!ILT+310(??$?6U?$char_traitsDstdstdYAAEAV?$basic_ostreamDU?$char_traitsDstd (00000001`3f1f113b)
00000001`3f1f17e5 33c0 xor eax,eax
00000001`3f1f17e7 488b4c2450 mov rcx,qword ptr [rsp+50h]
00000001`3f1f17ec 4833cc xor rcx,rsp
00000001`3f1f17ef e8bc420000 call Prototype_Console!__security_check_cookie (00000001`3f1f5ab0)
00000001`3f1f17f4 4883c468 add rsp,68h
00000001`3f1f17f8 c3 ret
现在,既然我们知道x64堆栈如何工作,我们可以开始“黑客攻击”。 RSP
是堆栈指针,函数堆栈是高于RSP
值的地址(堆栈增长到较小的地址)。因此,我们发现RSP+28h
位于cUsername
,RSP+38h
为authentication
,RSP+40h
为cPassword
,其中28h,38h和40h为十六进制偏移量。这里有一些小图片来说明:
-----> old RSP value // Stack frame of caller of `main` is above, stack frame of main is below
16 bytes of
"cPassword"
+40h
8 bytes of "authentication"
+38h
16 bytes of
"cUsername"
+28h
-----> RSP value = old RSP-68h
我们从这里看到了什么?我们看到编译器在8字节边界上对齐数据:例如,我们要求为cUsername
分配10个字节,但我们得到16个字节 - 自然地,x64位堆栈在8字节边界上对齐。这意味着为了写入authentication
,我们需要写入cUsername
更多16字节(符号)。另请注意,编译器将cPassword
置于authentication
之上 - 我们无法使用authentication
覆盖cPassword
,仅cUsername
。
现在我们运行我们的程序并输入Username: 0123456789abcdef1
。 0123456789abcdef
= 16个字节,下一个1
将被放入authentication
的低位字节 - 对我们来说足够好了:
Username: 0123456789abcdef1
Pass: whatever
Access granted
1
答案 1 :(得分:2)
它会覆盖您的authentication
变量。这意味着即使在您的代码检查用户名和密码之前authentication
也是肯定的。要检查这一点,请在检查之前打印出身份验证。
我会进一步扩展:当您输入的是一个非常长的用户名时,您的strcpy
会将该长用户名复制到cUsername
。该变量cUsername
紧跟在authentication
之后,因此会被过长的用户名覆盖。
如果您键入一个非常长的用户名,那么(再次)将覆盖身份验证变量。但是现在堆栈中的项目(例如返回值)将被覆盖。如果你的程序覆盖了太高的堆栈,那么它将被严重破坏,任何事情都可能发生。你基本上执行随机代码。
答案 2 :(得分:1)
如果您使用std::string
,您会发现您的程序会更简单:
int main()
{
bool authenticated = false;
while(!authenticated)
{
string username;
string password;
cout << "Username: ";
getline(cin, username); // you may want to read input differently
cout << "Pass: ";
getline(cin, password); // same as above
// you'll need to check cin.fail() to see whether the stream
// had failed to read data, and exit the loop with "break".
if(username == "admin" && password == "adminpass")
{
authenticated = true;
}
else
{
cout << "Wrong username and password, try again\n";
}
}
if(authenticated)
{
cout << "Access granted\n";
}
}
关于你最近的问题,我认为默认情况下,cin >> string
将停止在第一个空格字符(即空格)处读取,因此如果输入空格,cin
将在其损坏之前停止任何数据,因此您不会获得访问冲突。如果你想能够阅读空格,那么你需要像我上面那样使用getline
,这样它就会读取整行文字,包括空格。
答案 3 :(得分:1)
建议的解决方案是在手中检测memcpy,memset,strcpy中的NULL指针和缓冲区溢出,并打印出问题发生的位置(文件:行):
&#xA;&#xA;&#XA; &#XA;&#XA;
答案 4 :(得分:0)
编译程序时,编译器决定如何在内存中排列数据。如果一个程序包含未经检查的数组访问,它可能被利用,因为了解内存中数据排列的恶意用户可以弄清楚如何覆盖关键变量。
但是,C ++并不能让您完全控制堆栈上的布局。局部变量可以在内存中以任何顺序出现。
要了解缓冲区溢出漏洞,您必须反汇编程序并深入研究机器代码。这将为您提供堆栈的布局,包括所有重要的返回地址。
顺便说一句,“访问冲突”来自您的程序,而不是Visual Studio。在进入逆向工程之前,您可能需要更多的“前进”工程经验。
答案 5 :(得分:0)
因为你的char设置为10个位置(包括NULL
字符),所以更长的时间会溢出到Authentication
。有很多方法可以解决这个问题,最明显的是如此简单地使char更大。其他方法是限制用户在注册时输入的字母数(假设这是在网站服务器上)。您还可以使用strlen(cUsername)
来计算char数组的长度,并要求重新输入较少字符的用户名。
编辑:
好。所以你要做的是改用getline(cin,cUser)
。 cin
在空白的第一次出现时停止读取。 getline()
将使用或不使用空格读取整个字符串。