Переполнение буфера в C ++ - PullRequest
3 голосов
/ 09 января 2012

Я пытаюсь научить себя переполнению буфера и его эксплуатации в 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 << String и т. Д. Во всяком случае, когда я ввожу слишком много букв (например, тонну A) в cUser и cPass Я только что получил нарушение прав доступа из Visual Studio. Однако, если я набираю 20ish A, затем пробел, а затем еще один A в cUser, он пропускает запрос на cPass (при условии, что он был заполнен после того, как символ пробела вызвал предыдущий вызов cin, чтобы вернуться) и просто дает мне доступ.

В какой момент и почему данные перетекают в «аутентификацию» и почему это происходит только тогда, когда у меня есть место, а не когда у меня миллион A ... Я никогда не получаю «Нарушение прав доступа» «когда я использую пробел для ввода cUser.

Ответы [ 6 ]

22 голосов
/ 09 января 2012

Я немного изменил вашу программу, чтобы сделать ее более наглядной:

#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 );
}

Я скомпилировал ее с помощью компилятора MS x64 для командной строки, без оптимизаций.Так что теперь у нас есть 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-байтовой границе: например, мы попросили выделить 10 байт для cUsername, но мы получили 16 байтов - x64 битный стек выровнен на 8-байтовой границе, естественно.Это означает, что для записи в authentication нам нужно записать в cUsername MORE эти 16 байтов (символов).Также обратите внимание, что компилятор поставил cPassword выше authentication - мы не можем перезаписать authentication, используя cPassword, только cUsername.

Так что теперь мы запускаем нашу программу и вводим Username: 0123456789abcdef1.0123456789abcdef = 16 байтов, следующий 1 будет помещен в младший байт authentication - достаточно для нас:

Username: 0123456789abcdef1
Pass: whatever
Access granted
1
2 голосов
/ 09 января 2012

Перезаписывает вашу переменную authentication. Это означает, что authentication является положительным даже до того, как ваш код проверит имя пользователя и пароль. Чтобы проверить это, распечатайте аутентификацию перед проверками.

Я расширю немного дальше: когда вы вводите очень длинное имя пользователя, это длинное имя пользователя копируется вашим strcpy в cUsername. Эта переменная cUsername сразу после authentication и, следовательно, перезаписывается слишком длинным именем пользователя.

Если вы введете очень очень длинное имя пользователя, то (снова) переменная аутентификации будет перезаписана. Но также теперь элементы, находящиеся дальше по стеку, такие как возвращаемое значение, будут перезаписаны. Если ваша программа слишком сильно перезаписывает стек, то она будет очень сильно повреждена и все может произойти. Вы по сути выполняете случайный код на этом этапе.

1 голос
/ 07 октября 2016

Предлагаемое решение для обнаружения указателя NULL и переполнения буфера в memcpy, memset, strcpy перед рукой и распечатки местоположения (файл: строка), где возникает проблема:

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

1 голос
/ 09 января 2012

Если вы используете 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";
  }      
}

Edit:

Что касается вашего недавнего вопроса, я думаю, что по умолчанию cin >> string прекратит чтение с первого символа пробела (то есть пробела), поэтому, если вы введете пробел, cin остановится, прежде чем он повредит какие-либо данные, и так что вы не получите нарушение доступа. Если вы хотите иметь возможность читать пробелы, вам нужно использовать getline, как у меня выше, чтобы он читал всю строку текста, включая пробелы.

0 голосов
/ 09 января 2012

Поскольку ваш символ установлен на 10 мест (включая символ NULL), все, что больше будет переливаться на Authentication. Есть множество способов исправить это, и, очевидно, так просто сделать символ больше. Другими способами было бы ограничить количество писем, которые пользователь вводит при регистрации (при условии, что это было на сервере сайта). Вы также можете использовать strlen(cUsername) для подсчета длины массива char и запроса повторного ввода имени пользователя с меньшим количеством символов.
EDIT:
Хорошо. Так что вы хотите вместо этого использовать getline(cin,cUser). cin прекращает чтение при первом появлении пробела. getline() прочитает всю строку с пробелами или без них.

0 голосов
/ 09 января 2012

Когда вы компилируете программу, компилятор решает, как разместить ваши данные в памяти. Если программа содержит неконтролируемый доступ к массиву, она может быть использована, поскольку злонамеренный пользователь со знанием расположения данных в памяти может выяснить, как перезаписать критические переменные.

Однако C ++ не дает вам полного контроля над тем, как все расположено в стеке. Локальные переменные могут появляться в памяти в любом порядке.

Чтобы понять эксплойты переполнения буфера, вам придется разобрать вашу программу и вникнуть в машинный код. Это даст вам макет стека, включая все важные адреса возврата.

Кстати, "Нарушение прав доступа" исходит от вашей программы, а не от Visual Studio. Вероятно, вам понадобится больше опыта в области «форвардного» инжиниринга, прежде чем переходить в реверс-инжиниринг.

...