【问题标题】:C++ Buffer OverflowC++ 缓冲区溢出
【发布时间】:2012-02-05 15:49:13
【问题描述】:

我正在尝试自学 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;
}

我知道cin &lt;&lt; String 等这里有各种不好的juju...无论如何,当我在cUsercPass 中输入太多字母(例如大量A)时,我刚刚从 Visual Studio 收到访问冲突。但是,如果我输入 20ish A's,然后是空格,然后是另一个 AcUser,它会跳过询问我 cPass(假设因为在空格字符导致上一次调用cin 返回)并授予我访问权限。

数据在什么时候以及为什么会溢出到“身份验证”中,为什么它只在我有空间而不是当我有一百万 A 时才会发生......我从来没有收到“访问冲突” " 当我在 cUser 的输入中使用空格时。

【问题讨论】:

  • 系统(“暂停”);开始;不,很确定你是初学者。
  • 您是否正在尝试获取溢出并想知道为什么会这样?还是您不想溢出并寻求解决方案?你也不应该 system().对于黑客来说,这是一个巨大的后门。
  • 另外,我的系统上有一个名为pause 的程序,用于发射核导弹。所以我可能不应该运行你的程序。
  • 为什么在包含字符串标题时使用 char 数组???还有 goto 和 system("pause")... 我不能比你更惩罚你自己。
  • @codekiddy system("command") 是 C 标准所要求的,并由 C++ 在所有平台上继承。但它的行为完全是特定于平台的。在某些平台上,黑客可以安装一个会被发现并执行的文件,而不是预期的文件。

标签: c++ overflow buffer exploit


【解决方案1】:

当你编译一个程序时,编译器决定如何在内存中排列你的数据。如果程序包含未经检查的数组访问,它可能会被利用,因为知道内存中数据排列的恶意用户可以弄清楚如何覆盖关键变量。

但是,C++ 不能让您完全控制堆栈上的布局方式。局部变量可以按任意顺序出现在内存中。

要了解缓冲区溢出漏洞,您必须反汇编程序并深入研究机器代码。这将为您提供堆栈的布局,包括所有重要的返回地址。

顺便说一句,“访问冲突”来自您的程序,而不是 Visual Studio。在进入逆向工程之前,您可能需要更多的“正向”工程经验。

【讨论】:

  • "forward" 工程:D 我必须记住这一点。
  • 我一定会在我的前沿工程上工作......欣赏俏皮话。我试图理解为什么我在一个实例中而不是在另一个实例中得到“访问冲突”(有空格)。
  • @sraboy 不打算作为俏皮话或刺拳......无论如何,结果是不可预测的,因为你没有足够的信息来做出预测。你是在黑暗中拍摄,你真的期望得到明确的结果吗?
  • 我不知道会发生什么。这对我来说是全新的。你的回答没有完全解释清楚。我有一个实际问题;你的回答很抽象。结合 AzzA,它会有所帮助,尤其是知道您对“局部变量可能以任何顺序出现”的引用得到了更好的解释,他告诉我编译器只是选择将 cPassword 置于身份验证之上。我对堆栈了解不多,只是编程直到它起作用。这是我第一次尝试从实际角度弄清楚这些东西。我是一个 .NET(和 QBASIC)的人,所以内存管理等对我来说是新的。
【解决方案2】:

它正在覆盖您的 authentication 变量。这意味着即使在您的代码检查用户名和密码之前,authentication 也是肯定的。要检查这一点,请在检查之前打印出身份验证。

我将进一步扩展:当您键入一个很长的用户名时,该长用户名将由您的strcpy 复制到cUsername。该变量cUsername 紧跟在authentication 之后,因此它会被过长的用户名覆盖。

如果您键入一个非常长的用户名,那么(再次)身份验证变量将被覆盖。但是现在堆栈中更靠前的项目,例如返回值,将被覆盖。如果您的程序在堆栈上覆盖太高,那么它将被严重破坏并且任何事情都可能发生。此时您实际上是在执行随机代码。

【讨论】:

  • 为什么有一个空格可以防止识别“访问冲突”而不是简单地覆盖身份验证?
  • 我不认为这个空间是相关的。 cin 对所有空格进行相同处理。我认为您的问题是“当我在用户名中有大约 20 个字符时,程序会进行身份验证,当我有更多字符(比如 100 个)时,我会遇到访问冲突。为什么会有这种差异?”
  • 好吧。感谢您的回复......自学成才,我可以从这里编写代码到王国来,但我无法调试我的生活并且对堆栈和内存分配不太了解。欣赏信息。
  • @sraboy,您使用的是哪个编译器?我做了实验和 g++ 和 clang。你在哪里使用clang?哪个编译器的哪个版本?
  • 我刚刚使用了 Visual Studio 2010,所以 MSVC++...无论我现在使用哪种编译器,都不确定我目前有哪些更新/服务包。我通常只为我的应用程序使用 C# 或 VB.NET/GDI,所以这个带有内存管理等的 C++ 东西对我来说是相当新的。我已经习惯了所有的托管代码。
【解决方案3】:

因为您将 char 设置为 10 个位置(包括 NULL 字符),所以任何更长的字符都会溢出到 Authentication。有很多方法可以解决这个问题,最明显的是让字符变大。其他方法是限制用户在注册时输入的字母数量(假设这是在网站服务器上)。您还可以使用strlen(cUsername) 来计算 char 数组的长度,并要求以更少的字符重新输入用户名。
编辑:
好的。所以你想做的是改用getline(cin,cUser)cin 在第一次出现空格时停止读取。 getline() 将读取带或不带空格的整个字符串。

【讨论】:

  • 我知道有很多方法可以避免这种情况;我只是想理解这里的概念。为什么在没有空间的情况下会出现“访问冲突”,但如果我有空间,它只会允许将额外的数据写入身份验证中?
  • 使用strlen(cUsername)来检查用户是否输入了太多字符是行不通的,因为到那时已经太晚了。当cUsername已满时,您想停止阅读,而不是检查之后是否已满。
【解决方案4】:

如果你使用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 &gt;&gt; string 将在第一个空白字符(即空格)处停止读取,因此如果您输入空格,cin 将在它损坏任何数据之前停止,并且所以你不会得到访问冲突。如果您希望能够读取空格,那么您需要像上面一样使用getline,以便它可以读取整行文本,包括空格。

【讨论】:

  • 谢谢。我知道如何完全避免这个问题,但我正试图找出向 cin 提供空间的差异背后的细节。没有空间,我会遇到访问冲突。有了空间,我得到了我的利用。
  • 啊!谢谢。我没有意识到这一点。所以 cin 只是在空白处停止读取,当我尝试 cin >> uPass 时,这可以防止它读入过多的内存。我知道它停在空白处,但没有将 2 和 2 放在一些东西上。通过调试器运行,我可以看到现在发生了什么。谢谢!
【解决方案5】:

我稍微修改了您的程序以使其更具说明性:

#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+28hcUsernameRSP+38hauthenticationRSP+40hcPassword,其中 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 更高的位置——我们不能使用cPassword 覆盖authentication,只能使用cUsername

所以现在我们运行我们的程序并输入Username: 0123456789abcdef10123456789abcdef = 16 个字节,下一个 1 将被放入 authentication 的低字节 - 对我们来说已经足够了:

Username: 0123456789abcdef1
Pass: whatever
Access granted
1

【讨论】:

  • 非常感谢您不厌其烦地解释这个显然是一个非常简单的问题。我很欣赏 ASM 突破和示例。你真是个教练!
  • @sraboy 如果您对编译器如何生成代码、内存布局等感兴趣,那么像您这样的小型测试程序是不错的选择。只需阅读一点关于最常见的汇编指令和 CPU 堆栈布局的信息,并获得一个允许您反汇编的调试器。然后您将能够看到您的 C/C++ 代码如何转换为实际的机器指令。它不仅允许使用一些“技巧”,还可以让您了解编译器能够优化的内容等。
  • 嗨,我是 C++ 新手。我有一个愚蠢的问题要问,我们如何将用户输入限制为我们声明的可变大小?例如,cUsername 只接受 10 个字符,但用户可以输入更多,对吗?可以防止这个变量溢出吗?
  • @overshadow 是的。例如,您可以使用std::setw()std::cin &gt;&gt; std::setw(9) &gt;&gt; cUsername;
  • 令人惊讶的是,像 C++ 这样古老/令人讨厌的语言如何存活这么久。也许是时候结束它的统治,为现代语言腾出空间(不太容易出现缓冲区溢出等错误)。
【解决方案6】:

提前检测memcpy、memset、strcpy中的NULL指针和缓冲区溢出并打印出问题发生的位置(文件:行)的建议解决方案:

http://htvdanh.blogspot.com/2016/09/proposed-solution-to-detect-null.html

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-09-07
    • 1970-01-01
    • 2015-12-16
    • 1970-01-01
    • 2010-11-11
    • 1970-01-01
    • 2020-10-02
    • 2017-06-26
    相关资源
    最近更新 更多