Меня попросили взломать программу на собеседовании. Часть 2

в 14:00, , рубрики: c++, crackme, gdb, информационная безопасность, отладка

Это перевод второй части публикации «Меня попросили взломать программу на собеседовании». Оригинальный текст можно найти здесь.

Предисловие

Привет, ребята. Если вы не знаете, что означает «Часть 2», пожалуйста прочитайте Часть 1.
Для начала я хотел бы поблагодарить всех прочитавших первую часть, поскольку в итоге я получил массу отличных отзыв.

Так же я бы хотел устранить некоторые недопонимания:

  1. Я более не работаю на данную компанию, я переехал в Барселону;
  2. Я проходил данное интервью почти год назад;
  3. Программы я взламывал в облаке ($5 тариф, да, вы угадали компанию), поэтому я не считаю, что использование root@'a является проблемой — я могу пересоздать новую среду за пару секунд. В итоге я все же переключился на пользователя eren@, так как gdb не принимал рутовые инит файлы.
  4. Не забудьте прочитать окончание статьи — вам обязательно понравится!

Поехали

На этот раз мы будем работать не с дверью, а с ядерной ракетой.

eren@lisa:~$ ./CrackTheNuke 

        *** NUKE CONTROL SYSTEM  ***

PASSWORD: giveMeNuke

        *** ACCESS DENIED ***

PASSWORD: iwantanexplosion

        *** ACCESS DENIED ***

PASSWORD: knockknockitsme 

        *** ACCESS DENIED ***

        *** SYSTEM LOCKED ***

        *** SHUTTING DOWN ***

eren@lisa:~$

Я создам дамп всего бинарника с intel asm синтаксисом, как образец:

eren@lisa:~$ objdump -M intel -D CrackTheNuke > staticDis 
eren@lisa:~$

Этот файл нам понадобится позже. Если вы загляните в файл staticDis , вы сможете найти полный дамп с intel'овским синтаксисом.

Давайте на этот раз попробуем кое-что другое: для начала я запущу процесс, а после подцеплю на него дебаггер.

eren@lisa:~$ ./CrackTheNuke 

        *** NUKE CONTROL SYSTEM  ***

PASSWORD:

Теперь мы можем переключиться в другой шелл и запустить из него отладчик:

eren@lisa:~$ ps aux | grep Crack
eren      4741  0.0  0.0   1724   252 pts/0    S+   14:54   0:00 ./CrackTheNuke
eren      4845  0.0  0.1   7832   832 pts/1    S+   14:56   0:00 grep Crack
eren@lisa:~$ gdb --pid 4741
GNU gdb (GDB) 7.4.1-debian
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Catchpoint 1 (syscall 'ptrace' [26])
Attaching to process 4741
Reading symbols from /home/eren/CrackTheNuke...(no debugging symbols found)...done.
Reading symbols from /lib32/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib32/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
0xf7726430 in __kernel_vsyscall ()
=> 0xf7726430 <__kernel_vsyscall+16>:   5d  pop    ebp
(gdb)

Теперь вы можете ввести любые 16 символов в предыдущем окне и вернуться сюда. Сейчас мы находимся в функции scanf, которая покоится в библиотеке glibc (crackme будет вызывать scanf 16 раз, но мы сэкономим здесь немного времени).

В gdb вы можете набрать si (аббр. от single step). Вводите si, пока не доберетесь до адреса: 0x80495ed. Или же можете просто ввести команду: b * 0x80495ed и нажать с, чтобы добраться до необходимого адреса.

В любом случае, теперь мы на месте:

0x80495ed <main+195>:

Здесь мы можем увидеть операцию сравнения:

0x80495ed <main+195>:    cmp    DWORD PTR [esp+0x1c],0x0

В gdb вы можете ввести: p/x $esp для просмотра содержимого $esp.
Также вы можете провести некоторые вычисления с использованием регистров и адресов: p/x $esp+0x1c. Или же просмотреть содержимое адреса после разименования: p/x *0xff811bac.

Здесь вы можете ввести si, что приведет нас к моменту, когда crackme получил наши 16 символов и ожидает символ окончания строки n.

Советую вам поставить брейкпоинт по адресу 0x804962d : b * 0x804962d, если вы не хотите долго и мучительно ждать.

А вот теперь начинается веселье:

=> 0x804962d <main+259>:   push   eax
   0x804962e <main+260>:  push   ebx
   0x804962f <main+261>:  rdtsc  
   0x8049631 <main+263>:  and    eax,0xfffff
   0x8049636 <main+268>:  test   eax,eax
   0x8049638 <main+270>:  je     0x8049646 <g99>
   0x804963a <main+272>:  xor    ebx,0xe
   0x804963d <main+275>:  add    ebx,0xe
   0x8049640 <main+278>:  sub    ebx,0xe
   0x8049643 <main+281>:  dec    eax

Слышали ли вы когда-нибудь об инструкции rdtsc? Её основная задача — подсчет количества циклов процессора. После вызова rdtsc счётчик TSC будет помещен в регистры edx и eax:

0x8049636 <main+268>:        test   eax,eax
0x8049638 <main+270>:        je     0x8049646 <g99>

То же самое на С выглядело бы примерно так:

if(eax == 0)
{
    goto 0x8049646
}

Поскольку eax не равен 0, мы продолжим копать. Как вы вероятнее всего заметили, нижеприведенный код — полный треш: мы добавляем 0xe к ebx, а затем вычитаем его. Похоже, нас пытаются запутать.

xor ebx,0xe
add ebx,0xe
sub ebx,0xe
dec eax 

Пока eax равен 0, продолжаем данный цикл.
Поставьте брейкпоинт: b * 0x8049646 и нажмите c.
Окей, ничего интересного — идем дальше.

=> 0x80494db <nkc1qpE2L6f6AyqaendA>:   push   ebp
   0x80494dc <nkc1qpE2L6f6AyqaendA+1>:    mov    ebp,esp
   0x80494de <nkc1qpE2L6f6AyqaendA+3>:    sub    esp,0x14
   0x80494e1 <nkc1qpE2L6f6AyqaendA+6>:    mov    DWORD PTR [ebp-0x4],0x0
   0x80494e8 <nkc1qpE2L6f6AyqaendA+13>:   mov    DWORD PTR [esp],0x0
   0x80494ef <nkc1qpE2L6f6AyqaendA+20>:   call   0x804944b <qEWL8Jl0zdpmTbwhziDv>
   0x80494f4 <nkc1qpE2L6f6AyqaendA+25>:   mov    eax,DWORD PTR [ebp+0x8]
   0x80494f7 <nkc1qpE2L6f6AyqaendA+28>:   mov    DWORD PTR [esp],eax
   0x80494fa <nkc1qpE2L6f6AyqaendA+31>:   call   0x8048604 <fjDKIzPtGuE8ZdfSL8vq>
   0x80494ff <nkc1qpE2L6f6AyqaendA+36>:   mov    DWORD PTR [esp],0x2
   0x8049506 <nkc1qpE2L6f6AyqaendA+43>:   call   0x804944b <qEWL8Jl0zdpmTbwhziDv>
   0x804950b <nkc1qpE2L6f6AyqaendA+48>:   mov    eax,DWORD PTR [ebp+0x8]
   0x804950e <nkc1qpE2L6f6AyqaendA+51>:   mov    DWORD PTR [esp],eax
   0x8049511 <nkc1qpE2L6f6AyqaendA+54>:   call   0x8048ab1 <W0ElBw5Smo9TPiWOeK8c>
   0x8049516 <nkc1qpE2L6f6AyqaendA+59>:   mov    DWORD PTR [ebp-0x4],eax
   0x8049519 <nkc1qpE2L6f6AyqaendA+62>:   mov    DWORD PTR [esp],0x1
   0x8049520 <nkc1qpE2L6f6AyqaendA+69>:   call   0x804944b <qEWL8Jl0zdpmTbwhziDv>
   0x8049525 <nkc1qpE2L6f6AyqaendA+74>:   mov    eax,DWORD PTR [ebp-0x4]
   0x8049528 <nkc1qpE2L6f6AyqaendA+77>:   leave  
   0x8049529 <nkc1qpE2L6f6AyqaendA+78>:   ret

nkc1qpE2L6f6AyqaendA — эта функция и есть основой всего процесса.

Давайте попробуем исследовать все функции, к которым обращается nkc1qpE2L6f6AyqaendA: qEWL8Jl0zdpmTbwhziDv , fjDKIzPtGuE8ZdfSL8vq и W0ElBw5Smo9TPiWOeK8c:

(gdb) x/10i qEWL8Jl0zdpmTbwhziDv
   0x804944b <qEWL8Jl0zdpmTbwhziDv>:  push   ebp
   0x804944c <qEWL8Jl0zdpmTbwhziDv+1>:    mov    ebp,esp
   0x804944e <qEWL8Jl0zdpmTbwhziDv+3>:    mov    eax,DWORD PTR [ebp+0x8]
   0x8049451 <qEWL8Jl0zdpmTbwhziDv+6>:    cmp    eax,0x0
   0x8049454 <qEWL8Jl0zdpmTbwhziDv+9>:    je     0x80494b9 <hzdhp>
   0x8049456 <qEWL8Jl0zdpmTbwhziDv+11>:   cmp    eax,0x1
   0x8049459 <qEWL8Jl0zdpmTbwhziDv+14>:   je     0x8049499 <qEWL8Jl0zdpmTbwhziDv+78>
   0x804945b <qEWL8Jl0zdpmTbwhziDv+16>:   call   0x8047b71
   0x8049460 <qEWL8Jl0zdpmTbwhziDv+21>:   add    DWORD PTR [eax+0x48604bf],0x5eb9008
   0x804946a <qEWL8Jl0zdpmTbwhziDv+31>:   add    DWORD PTR [eax-0x4608ea13],0x8048ab1

(gdb) x/10i fjDKIzPtGuE8ZdfSL8vq
   0x8048604 <fjDKIzPtGuE8ZdfSL8vq>:  call   0xb027:0xaf72c78c
   0x804860b <fjDKIzPtGuE8ZdfSL8vq+7>:    cmp    esi,DWORD PTR ds:0xe4dfbbf1
   0x8048611 <fjDKIzPtGuE8ZdfSL8vq+13>:   (bad)  
   0x8048612 <fjDKIzPtGuE8ZdfSL8vq+14>:   and    al,BYTE PTR [ebp+edi*2-0x8]
   0x8048616 <fjDKIzPtGuE8ZdfSL8vq+18>:   push   ebx
   0x8048617 <fjDKIzPtGuE8ZdfSL8vq+19>:   push   esi
   0x8048618 <fjDKIzPtGuE8ZdfSL8vq+20>:   inc    edx
   0x8048619 <fjDKIzPtGuE8ZdfSL8vq+21>:   mov    WORD PTR [ebp+0x76],ss
   0x804861c <fjDKIzPtGuE8ZdfSL8vq+24>:   xchg   edx,eax
   0x804861d <fjDKIzPtGuE8ZdfSL8vq+25>:   mov    al,ds:0x45fd3fbb
(gd

(gdb) x/10i W0ElBw5Smo9TPiWOeK8c
   0x8048ab1 <W0ElBw5Smo9TPiWOeK8c>:  call   0xb023:0x1c72c78c
   0x8048ab8 <W0ElBw5Smo9TPiWOeK8c+7>:    cmp    esi,DWORD PTR ds:0xe4dfbbf1
   0x8048abe <W0ElBw5Smo9TPiWOeK8c+13>:   jmp    0xf86e358
   0x8048ac3 <W0ElBw5Smo9TPiWOeK8c+18>:   xchg   ax,ax
   0x8048ac5 <W0ElBw5Smo9TPiWOeK8c+20>:   out    dx,eax
   0x8048ac6 <W0ElBw5Smo9TPiWOeK8c+21>:   dec    ebp
   0x8048ac7 <W0ElBw5Smo9TPiWOeK8c+22>:   xchg   edi,eax
   0x8048ac8 <W0ElBw5Smo9TPiWOeK8c+23>:   popa   
   0x8048ac9 <W0ElBw5Smo9TPiWOeK8c+24>:   test   DWORD PTR [ecx-0x7e],esp
   0x8048acc <W0ElBw5Smo9TPiWOeK8c+27>:   test   DWORD PTR [edi],esi

Как мы можем увидеть, основной алгоритм находится внутри функции nkc1qpE2L6f6AyqaendA , а цепочка вызовов выглядит следующим образом: qEWL8Jl0zdpmTbwhziDv -> fjDKIzPtGuE8ZdfSL8vq -> qEWL8Jl0zdpmTbwhziDv -> W0ElBw5Smo9TPiWOeK8c -> qEWL8Jl0zdpmTbwhziDv.

Просмотрев первые 10 строк каждой функции, смогли ли вы найти нечто необычное? Посмотрите внимательно на первые строки fjDKIzPtGuE8ZdfSL8vq и W0ElBw5Smo9TPiWOeK8c , они абсолютно бессмысленны.

Я ни разу в жизни (от переводчика: в оригинале: life:) — вероятнее всего отсыл к небезизвестному мобильному оператору) не встречался с чем-либо подобным: call 0xb023:0x1c72c78c. А все дело в том, что обе эти функции зашифрованы и gdb попытался их дизассемблить.

Итак, qEWL8Jl0zdpmTbwhziDv занимается расшифровкой функций (поэтому её вызов и стоит перед ними).

Я попробую поменять алгоритм выполнения программы, заменив зашифрованные функции их расшифрованными соответствиями и уберу вызов qEWL8Jl0zdpmTbwhziDv.

Исходя из этого, новый алгоритм будет выглядеть следующим образом: fjDKIzPtGuE8ZdfSL8vq -> W0ElBw5Smo9TPiWOeK8c — и всё.

Тупик 1. Начало

Работая над этим crackme, я попытался отключить TimeStampCounter или как-нибудь его контролировать. В данном случае rdtsc используется для проверки интервала времени между выполнениями инструкций. Соответственно, если вы попытаетесь прогнать программу через gdb, то данный интервал будет намного больше такого же, но при нормальной работе кода. Поэтому я попытался найти способ управления счетчиком tsc, но, к сожалению, он управляется процессором — и поэтому я ничего не могу сделать из-под ОС.
Но все же я попытался написать модуль для ядра, который бы сбивал счетчик, устанавливая его значение равным 0:

#include <linux/module.h>    // included for all kernel modules
#include <linux/kernel.h>    // included for KERN_INFO
#include <linux/init.h>      // included for __init and __exit macros
#include <linux/kthread.h>  // for threads
#include <linux/sched.h>  // for task_struct
#include <linux/time.h>   // for using jiffies 
#include <linux/timer.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("m00dy");
MODULE_DESCRIPTION("A Fake rdtsc emulation");

static struct task_struct *thread1;

int thread_fn(){

uint32_t hi,lo;
unsigned long j0,j1;
int delay = HZ / 250;
hi=0; lo=0xb;
printk(KERN_INFO "In thread1");
j0 = jiffies;
j1 = j0 + delay;


asm volatile("wrmsr"::"c"(0x10),"a"(lo),"d"(hi));

while(1){
    if(time_before(jiffies,j1))
        schedule();
    else
    {
      j1 = jiffies + delay;
      asm volatile("wrmsr"::"c"(0x10),"a"(lo),"d"(hi));
    }
}

}

static int __init hello_init(void)
{

    char  our_thread[8]="thread1";
    printk(KERN_INFO "in init");
    thread1 = kthread_create(thread_fn,NULL,our_thread);
    if((thread1))
        {
        printk(KERN_INFO "in if");
        wake_up_process(thread1);
        }

    return 0;
}

static void __exit hello_cleanup(void)
{
    printk(KERN_INFO "Fake RDTSC end n");
}

module_init(hello_init);
module_exit(hello_cleanup);=

К сожалению, данный метод не сработал, так, как мне было необходимо, и я продолжил поиски.

Тупик 1. Конец

У меня появилась идея остановить программу в тот момент, когда обе функции будут в расшифрованном состоянии. Например, 0x8048ab0 — очень хорошее место, поскольку это конец функции fjDKIzPtGuE8ZdfSL8vq.

Давайте откроем .gdbinit и запишем:

set disassembly-flavor intel
set disassemble-next-line on
handle SIGTRAP noprint pass nostop
b * 0x8048ab0

Перезапускаем crackme и снова цепляем gdb. Вводим 16 символов и жмем c.

=> 0xf7706430 <__kernel_vsyscall+16>:  5d  pop    ebp
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x08048ab1 in W0ElBw5Smo9TPiWOeK8c ()
=> 0x08048ab1 <W0ElBw5Smo9TPiWOeK8c+0>: 9a 8c c7 72 1c 23 b0  call   0xb023:0x1c72c78c
(gdb) x/10i fjDKIzPtGuE8ZdfSL8vq
   0x8048604 <fjDKIzPtGuE8ZdfSL8vq>:  push   ebp
   0x8048605 <fjDKIzPtGuE8ZdfSL8vq+1>:    mov    ebp,esp
   0x8048607 <fjDKIzPtGuE8ZdfSL8vq+3>:    call   0x8047b08
   0x804860c <fjDKIzPtGuE8ZdfSL8vq+8>:    xor    eax,0x20ec8390
   0x8048611 <fjDKIzPtGuE8ZdfSL8vq+13>:   call   0x8047b08
   0x8048616 <fjDKIzPtGuE8ZdfSL8vq+18>:   xor    eax,0x32ff45c6
   0x804861b <fjDKIzPtGuE8ZdfSL8vq+23>:   call   0x8047b08
   0x8048620 <fjDKIzPtGuE8ZdfSL8vq+28>:   xor    eax,0xdafe45c6
   0x8048625 <fjDKIzPtGuE8ZdfSL8vq+33>:   call   0x8047b08
   0x804862a <fjDKIzPtGuE8ZdfSL8vq+38>:   xor    eax,0xdbfd45c6
(gdb)

Вуаля. Теперь у нас есть чистая функция fjDKIzPtGuE8ZdfSL8vq . Но у нас все еще есть проблема с gdbfalse assembly (доступно для прочтения на английском тут).

Давайте сохраним нашу функцию во временный файл (параметры: имя_файла, начальный_адрес и конечный_адрес):

dump ihex memory fjDKIzPtGuE8ZdfSL8vq_dump 0x8048604 0x8048ab0

Теперь сделаем то же самое для второй функции — ставим брейкпоинт по адресу: 0x08048e14 и создаем дамп:

dump ihex memory W0ElBw5Smo9TPiWOeK8c_dump W0ElBw5Smo9TPiWOeK8c g999+3

Теперь, когда у нас есть обе функции, давайте попробуем поменять алгоритм выполнения программы. Для этого очищаем файл .gdbinit и ставим брейкпоинт: 0x80494db:

set disassembly-flavor intel
set disassemble-next-line on

break * 0x80494ef
commands
set($eip) = 0x80494f4
continue
end

break * 0x80494fa
commands
restore fjDKIzPtGuE8ZdfSL8vq_dump
restore W0ElBw5Smo9TPiWOeK8c_dump
continue
end

break * 0x08049506
commands
set($eip) = 0x804950b
continue
end

break * 0x8049520
commands
set($eip) = 0x8049525
continue
end

Ну а теперь, когда мы изменили алгоритм — все достаточно просто. Следуем инструкциям, описанным в первой части данной статьи.

Введенные нами символы ксорятся (XOR) c некими константами, после чего результат проверяется на правильность: Inputs ^ FirstConstants == SecondConstants, соответственно: Inputs = SecondConstants ^ FirstConstants

А вот и наш генератор ключа:

#!/usr/bin/python
firstConst = [0x32,0xda,0xdb,0x1,0xf3,0x77,0x4c,0x57,0xbe,0x49,0xec,0x5f,0xab,0x7f,0xed,0x9f]
secondConst = [0x0d,0xef,0xf1,0x4d,0xb6,0x4c,0x69,0x20,0xf9,0x20,0xdd,0x7c,0xda,0x3b,0xc9,0xaf]
ret =""
for x in range(16):
        ret+=chr(firstConst[x] ^ secondConst[x])
print ret

Поехали проверять:

eren@lisa:~$ ./CrackTheNuke 

        *** NUKE CONTROL SYSTEM  ***

PASSWORD: ?5*LE;%wGi1#qD$0

        ***  ACCESS GRANTED  ***

        *** THE NUKE STOPPED ***

eren@lisa:~$

Все работает.

Заключение

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

После этого я стал J2ee разработчиком. Мне приходилось использовать eclipse, svn и даже операционную систему под названием Windows *. Но, как оказалось в последствии, это было не самое страшное. Позже они заставили меня писать css

Но теперь я живу в Барселоне и у меня прекрасная жизнь.

Автор: WhiteAngel

Источник

* - обязательные к заполнению поля