Первое знакомство с отладчиком Ghidra и взлом игры Spiderman

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


В середине декабря в твиттер-аккаунте NSA было объявлено о релизе новой ветки Ghidra с долгожданной поддержкой отладки. Теперь с помощью GDB-заглушки и прочих механизмов можно будет выполнять ее пошагово внутри самой Ghidra. Желая отпраздновать это событие, которое совпало с моим домашним карантином, я подготовил небольшой обзор сборки этой версии, включая пример использования ее отладчика для интересной цели.

В этой статье мы:

  • научимся собирать последнюю (да и любую) версию Ghidra при помощи Docker Container;
  • настроим плагины Ghidra Eclipse;
  • выполним сборку программного загрузчика для Ghidra;
  • прогоним через отладчик программу, использовав GDB-заглушку;
  • с помощью той же отладки разберемся, как обрабатываются пароли для игры на Game Boy Advance.

Меня очень вдохновила прекрасная работа, которую в этом направлении проделывают stackmashing и LiveOverflow. Советую заглянуть на их канал. В нашем же случае в качестве подопытной программы выступит игра Spiderman: Mysterio’s Menace. В свое время я играл в нее очень много, к тому же всегда приятно снова взглянуть на свои детские увлечения с позиции опыта. Конечная цель – показать, как правильно загружать этот образ ROM через настраиваемый загрузчик и подключать GDB-заглушку эмулятора при помощи отладчика Ghidra.

К сведению: при начале очередного проекта по реверс-инжинирингу важно правильно определить задачи. Например, если мы говорим, что хотим просто разобрать игру, то здесь допустимо огромное число вариантов. Можно, к примеру, разобрать механику обнаружения столкновений, принцип работы ИИ или способ создания карт уровней. В этой же статье конечной целью мы обозначим изучение механизма паролей.

Проект реализуется под Ubuntu 20.04 со всеми последними обновлениями.

Сборка Ghidra


Начнем с основного. Ветка отладчика еще не была включена в официальный релиз, так что мы его будем собирать сами. К нашему везению, уважаемый dukebarman уже создал для этой задачи docker-контейнер, и нам осталось только изменить скрипт build_ghidra.sh для переключения на ветку отладчика:

git clone https://github.com/NationalSecurityAgency/ghidra -b debugger

Мы также настроим для этой версии Ghidra расширения разработки Eclipse, что пригодится нам позже при сборке загрузчика и написании сценариев анализа. Для этого нужно добавить в скрипт build_ghidra.sh следующее:

gradle prepDev
gradle eclipse -PeclipsePDE

Далее следуйте инструкциям в README:

cd ghidra-builder
sudo docker-tpl/build
cd workdir
sudo ../docker-tpl/run ./build_ghidra.sh

Это займет какое-то время, так что можете отвлечься на кофе, а к возвращению вас уже будет ждать свежесобранная Гидра. Готовая сборка находится в workdir/out:

wrongbaud@wubuntu:~/blog/gba-re-gbd/ghidra-builder/workdir$ ls out/
ghidra_9.3_DEV_20201218_linux64.zip

Распакуйте файл и можете запускать Ghidra через скрипт ./ghidraRun. Я распакую содержимое в каталог ghidra-builder/workdir, так как для сборки этой версии мы будем использовать docker-контейнер. Если вы следуете за процессом, то сейчас ваша рабочая директория должна выглядеть так:

wrongbaud@wubuntu:~/blog/gba-re-gbd/ghidra-builder/workdir$ ls
build_ghidra.sh  ghidra  ghidra_9.3_DEV out  set_exec_flag.sh

Сборка плагинов Eclipse


Закончив с Ghidra, можно переходить к сборке плагинов GhidraDev для Eclipse. Эти проекты находятся в каталоге ghidra-builder/workdir/ghidra/GhidraBuild/EclipsePlugins/GhidraDev.

1. Установите Eclipse
  • Выберите Java IDE

2. Установите CDT, PyDev, и Plugin Development Environment.
  • Это можно сделать из маркетплейса Eclipse.

3. Импортируйте проекты GhidraDevFeature и GhidraDevPlugin.
  • Они находятся в каталоге ghidra-builder/workdir/ghidra/GhidraBuild/EclipsePlugins/GhidraDev/
  • File -> Import -> General -> Existing Projects into Workspace
  • Добавьте ghidra-builder/workdir/ghidra/GhidraBuild/EclipsePlugins/GhidraDev
  • Выберите “Search for nested projects”
  • Импортируйте проекты.

К сведению: после импорта вы можете заметить некоторые ошибки сборки. Не обращайте внимания, так как вы просто экспортируете плагин.

4. Теперь перейдем к экспорту:
  • File -> Export
  • Plug-in Development -> Deployable Features
  • ghidradev.ghidradev
  • Выберите местоположение архива для экспорта плагина.
  • Жмите Finish.

Теперь у нас есть плагин для настраиваемой версии Ghidra, который можно скачать через Help->Install New Software.
При этом мы собрали Ghidra из ветки debugger, а также настроили расширения разработки Eclipse, получив возможность создавать плагины для нашей новой версии Ghidra.

К сведению: я хочу подчеркнуть, насколько полезно заглядывать в документацию Ghidra. В ней содержится все необходимое, начиная с мануалов по P-Code и заканчивая инструкциями по сборке и экспорту плагинов.

Создание загрузчика ROM


Для успешного анализа образа ROM нам понадобится определить все области памяти и периферийные устройства GBA. И снова, к нашей удаче, SiD3W4y уже написал для этого решение на GitHub.

Задача загрузчика Ghidra в настройке всех необходимый областей памяти, определении отладочной информации и символов, которые могут присутствовать в файле, а также выдача всей доступной информации о целевом файле. Упомянутый выше загрузчик описывает все основные периферийные устройства GBA и прекрасно подойдет для нашей задачи, так что начнем с его копирования в тот же каталог ghidra-builder/workdir, поскольку для сборки будем использовать тот же контейнер docker, с помощью которого собирали Ghidra.

cd ghidra-builder/workdir
git clone https://github.com/SiD3W4y/GhidraGBA
sudo ../docker-tpl/run /bin/bash
dockerbot@797eb43ce05f:/files/GhidraGBA$ export GHIDRA_INSTALL_DIR=/files/ghidra_9.3_DEV/
dockerbot@797eb43ce05f:/files/GhidraGBA$ gradle
dockerbot@797eb43ce05f:/files/GhidraGBA$ cp dist/ghidra_9.3_DEV_20201218_GhidraGBA.zip ../ghidra_9.3_DEV/Extensions/Ghidra/
dockerbot@797eb43ce05f:/files/GhidraGBA$ exit
exit

Здесь мы:
1. Запускаем docker-контейнер.
2. Собираем расширение GhidraGBA, указывая путь к месту установки.
3. Копируем каталог расширений Ghidra, чтобы он показывался под меню Install Extensions.
4. Выходим из контейнера docker.

Запустите Ghidra командой ghidraRun и перейдите в File-> Install Extensions. Выерите загрузчик GhidraGBA и кликните OK. Для применения изменений потребуется перезапустить Ghidra. Теперь при загрузке GBA ROM должно отображаться следующее:



После выполнения автоматического анализа Ghidra неплохо поняла этот образ ROM. В нем определено много функций и выглядит все достаточно хорошо. Следующим шагом будет найти способ сузить нашу область интереса в этом образе. Говоря условно, нам нужно найти иголку в стоге сена. Начнем с выяснения принципа работы системы паролей, для чего просто попробуем ввести несколько их вариантов.

Анализ ROM


При вводе пароля мы наблюдаем такой экран:



Заметьте, что используются только согласные буквы и цифры от 0 до 9. Сам же пароль состоит из 5 символов. Для реверсинга это будет неплохой отправной точкой. С помощью данной информации можно сузить область интересующих нас функций. Например, давайте просмотрим строки ROM в поиске этих значений. Если открыть окно строк, Window -> Defined Strings, и сделать выборку по пяти первым доступным символам, то мы увидим следующее:



Кое-какой результат имеется – мы обнаружили две точки использования этой строки. Одна расположена в 0x804c11fc, а вторая в 0x84b86f0. При проверке первой строки мы видим, что она передается функции в подпрограмме по адресу 0x8003358:

	undefined4 passwd_1(int param_1,int param_2)

{
  int iVar1;
  uint uVar2;
  uint uVar3;
  undefined4 in_lr;
  undefined auStack52 [36];
  undefined4 uStack4;
  
  uStack4 = in_lr;
  FUN_080231f4(auStack52,"BCDFGHJKLMNPQRSTVWXYZ0123456789-",0x21);
  *(uint *)(param_1 + 0x8c) = 0;
  FUN_080025f8(param_1);
  FUN_08002674(param_1);
  FUN_08002714(param_1);
  FUN_0800282c(param_1);
  iVar1 = 0;
  uVar3 = *(uint *)(param_1 + 0x8c);
  uVar2 = 0;
  do {
    *(undefined *)(param_2 + iVar1) = auStack52[uVar3 >> (uVar2 & 0xff) & 0x1f];
    uVar2 = uVar2 + 5;
    iVar1 = iVar1 + 1;
  } while (iVar1 < 5);
  return uStack4;
}

Обратите внимание на цикл, продолжающий выполнение при переменной < 5. Это говорит о том, что данная функция может оказаться полезной, поскольку пароль как раз содержит именно 5 символов. Давайте отметим ее как passwd_1 и перейдем к остальным местам использования нашей строки символов. Далее она встречается в функции по адресу 0x8002CEC. Вот декомпилированный вариант:

	undefined8 passwd_2(void)

{
  int iVar1;
  int iVar2;
  uint uVar3;
  undefined4 in_lr;
  undefined local_98 [5];
  undefined local_93;
  undefined auStack144 [36];
  undefined auStack108 [8];
  undefined auStack100 [72];
  undefined4 uStack4;
  
  uStack4 = in_lr;
  FUN_08000b0c(0,1,0,0);
  DAT_03001fd0._0_2_ = 0x1444;
  DISPCNT = 0x1444;
  FUN_0801e330(&DAT_0838277c);
  iVar1 = DAT_03001fe0;
  FUN_080231f4(auStack144,"BCDFGHJKLMNPQRSTVWXYZ0123456789-",0x21);
  *(uint *)(iVar1 + 0x8c) = 0;
  FUN_080025f8(iVar1);
  FUN_08002674(iVar1);
  FUN_08002714(iVar1);
  FUN_0800282c(iVar1);
  iVar2 = 0;
  uVar3 = 0;
  do {
    local_98[iVar2] = auStack144[*(uint *)(iVar1 + 0x8c) >> (uVar3 & 0xff) & 0x1f];
    uVar3 = uVar3 + 5;
    iVar2 = iVar2 + 1;
  } while (iVar2 < 5);
  local_93 = 0;
  FUN_0801d1bc(auStack108,local_98);
  FUN_0801d92c(DAT_03001ff0,0x10,0);
  FUN_08000b0c(1,1,0,0);
  *(undefined4 *)(DAT_03002028 + 0xc) = 0x200;
  FUN_08000f1c();
  iVar1 = FUN_0801d26c(auStack108);
  *(undefined4 *)(DAT_03002028 + 0xc) = 0;
  FUN_08000f1c();
  FUN_0801dcac(DAT_03001ff0,0);
  FUN_08000b0c(0,1,0,0);
  FUN_08004408(auStack100,2);
  return CONCAT44(uStack4,(uint)(iVar1 == 0));
}

И снова мы видим передачу этой строки в функцию, а также очередной цикл, выполняющий 5 итераций – отметим его как passwd_2 и перейдем далее. Следующая строка встречается по адресу 0x84b86f0 и также используется в двух подпрограммах. Вот первая, расположенная в FUN_0801c37c:

	undefined4 render_pw_screen(int param_1)

{
  int iVar1;
  int iVar2;
  uint uVar3;
  undefined4 uVar4;
  uint uVar5;
  undefined4 in_lr;
  char local_1c [8];
  undefined4 uStack4;
  
  uStack4 = in_lr;
  iVar2 = FUN_0801b834(DAT_03001ffc,"@ - Accept   & - Backspace");
  iVar1 = DAT_03001ffc;
  *(uint *)(DAT_03001ffc + 0x90) = 0xf0U - iVar2 >> 1;
  *(undefined4 *)(iVar1 + 0x94) = 0x96;
  FUN_0801b764(iVar1,"@ - Accept   & - Backspace");
  uVar3 = *(uint *)(param_1 + 0x51c);
  if (uVar3 != 0) {
    uVar5 = 0;
    if (uVar3 != 0) {
      do {
        local_1c[uVar5] = "BCDFGHJKLMNPQRSTVWXYZ0123456789-"[*(byte *)(param_1 + 0x520 + uVar5)];
        uVar5 = uVar5 + 1;
      } while (uVar5 < uVar3);
    }
    local_1c[*(int *)(param_1 + 0x51c)] = '\0';
    iVar2 = FUN_0801b834(DAT_03002000,local_1c);
    iVar1 = DAT_03002000;
    *(uint *)(DAT_03002000 + 0x90) = 0xf0U - iVar2 >> 1;
    *(undefined4 *)(iVar1 + 0x94) = 0x3f;
    iVar2 = FUN_0800118c(DAT_03001fdc,5);
    *(byte *)(iVar1 + 5) = *(byte *)(iVar1 + 5) & 0xf | (byte)(iVar2 << 4);
    FUN_0801b764(DAT_03002000,local_1c);
  }
  if (*(int *)(param_1 + 0x51c) != 5) {
    uVar4 = FUN_0801a6d4(*(undefined4 *)(param_1 + 0x18));
    *(undefined4 *)(param_1 + 4) = uVar4;
  }
  return uStack4;
}

В этой функции мы видим, что FUN_0801b764 вызывается со строкой @ — Accept & — Backspace. Несколько далее та же функция вызывается с переменной, содержащей интересующую нас строку. При дальнейшем рассмотренииFUN_0801b764 мы узнаем, что она копирует данные из второй переменной (строки ASCII) в адрес памяти первого аргумента. Здесь уже нельзя сказать уверенно, но меня кажется, что конкретно эта подпрограмма служит для отрисовки текста на экране, поэтому пока что я ее пропущу и перейду к следующему месту использования строки символов, которое привожу ниже:

	undefined8 FUN_0801c454(int param_1)

{
  int iVar1;
  int iVar2;
  undefined4 in_lr;
  char local_14 [8];
  undefined4 uStack4;
  
  iVar2 = 1;
  uStack4 = in_lr;
  FUN_080231f4(local_14,"CRDT5",6);
  iVar1 = 0;
  do {
    if (local_14[iVar1] != "BCDFGHJKLMNPQRSTVWXYZ0123456789-"[*(byte *)(param_1 + 0x520 + iVar1)]) {
      iVar2 = 0;
    }
    iVar1 = iVar1 + 1;
  } while ((iVar1 < 5) && (iVar2 != 0));
  return CONCAT44(uStack4,iVar2);
}

Что у нас здесь? Во-первых, здесь мы видим FUN_080231f4, по сути являющуюся операцией memcpy:

	undefined4 * memcpy_1(undefined4 *dest,undefined4 *src,uint count)

{
  undefined4 uVar1;
  undefined4 *puVar2;
  undefined4 *puVar3;
  
  puVar2 = dest;
  if ((0xf < count) && ((((uint)src | (uint)dest) & 3) == 0)) {
    do {
      *puVar2 = *src;
      puVar2[1] = src[1];
      puVar3 = src + 3;
      puVar2[2] = src[2];
      src = src + 4;
      puVar2[3] = *puVar3;
      puVar2 = puVar2 + 4;
      count = count - 0x10;
    } while (0xf < count);
    while (3 < count) {
      uVar1 = *src;
      src = src + 1;
      *puVar2 = uVar1;
      puVar2 = puVar2 + 1;
      count = count - 4;
    }
  }
  while (count = count - 1, count != 0xffffffff) {
    *(undefined *)puVar2 = *(undefined *)src;
    src = (undefined4 *)((int)src + 1);
    puVar2 = (undefined4 *)((int)puVar2 + 1);
  }
  return dest;
}

Ее задача – копирование строки CRDT5 в указатель ячейки памяти в local_14. Далее мы видим, что в цикле while это значение используется в сравнении:

	if (local_14[iVar1] != "BCDFGHJKLMNPQRSTVWXYZ0123456789-"[*(byte *)(param_1 + 0x520 + iVar1)])

Что же происходит здесь? В каждой итерации символ из local_14 сравнивается со значением из нашей строки доступных символов BCDFGHJKLMNPQRSTVWXYZ0123456789-. Такое поведение вполне соответствует предполагаемым действиям функции проверки пароля. Но мы знаем, что iVar1 при каждой итерации увеличивается на 1. Значит ли это, что пароли должны состоять из смежных символов в BCDFGHJKLMNPQRSTVWXYZ0123456789-? Это бы было очень глупо, к тому же строка CRDT5 никогда бы не прошла такую проверку. Если еще раз взглянуть на условие сравнения, то можно заметить, что в нем присутствует переменная param_1, которая тоже используется в качестве индекса, к которому прибавляются iVar1 и 0x520 – затем эти значения используются как INDEX в доступных для набора символах.

О чем это говорит? Переменная param_1 скорее всего указывает на массив смещений, представляющих введенные на экране пароля символы. Например, если мы введем GHDRR, то массив будет содержать [0x4,0x5,0x2,0xd,0xd].

Но давайте не будем забегать вперед и для начала попробуем пароль CRDT5:



Интересно! Мы попали в сцену с титрами!

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

Отладка ROM


Те, кто повторяет процесс, должны были заметить появление нового инструмента в менеджере проектов:




Обратите внимание на иконку жука – с ее помощью открывается отладчик. Кликнув по ней, вы увидите следующее окно:



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



Под ним располагается окно “Objects”, показывающее находящиеся в режиме отладки “Objects”. Отсюда можно делать паузу, выполнять шаги и т.д.



В самом низу находится представление трех вкладок: Regions (области памяти), Stack (стек) и Console (консоль).



Справа мы видим окно для показа двух других вкладок: Threads (потоки) и Time (время). Для нашей задачи отладки однопоточной ARM-системы эти окна не пригодятся.



И наконец, оставшаяся справа часть экрана выделена под еще несколько вкладок, которые обычно представлены в разделе анализатора Ghidra. Здесь у нас вкладка Breakpoints, отображающая заданные точки останова:



Вторая вкладка Registers будет обновляться значениями регистра при достижении точек останова:



Последняя же вкладка – это представление Modules, где при необходимости отображаются загруженные модули. Мы же в случае нашего простого приложения ничего в ней не увидим:



Подключение к эмулятору


Для этого проекта я использую эмулятор mGBA, главным образом потому, что он может представлять удаленную GDB-заглушку. Подключаться к нему мы будем с помощью gdb-multiarch. Чтобы выполнить это из представления отладчика нужно в окошке Debugger Targets кликнуть по зеленой вилке (Connect), что вызовет следующее окно:



Здесь есть много опций для удаленной отладки. В целях данной статьи я использую IN-VM GNU gdb local debugger.
Я добавил gdb-multiarch в путь команды запуска gdb. После нажатия Connect появится стандартное диалоговое окно:



Теперь нужно запустить сервер. Загрузите образ ROM в mGBA и выберите Tools -> Start GDB Server, всплывет такое окно:



Кликните Start и возвращайтесь в окно отладчика Ghidra. В диалоговом окне gdb выполните следующие команды:

	set architecture arm
set arm fallback-mode thumb
set arm force-mode thumb
target remote localhost:2345
break *0x801c470
c

Здесь мы устанавливаем gdb архитектуру, подключаемся к удаленному серверу и в завершении определяем точку останова у функции, которая, как мы считаем, проверяет, нужно ли показывать сцену с титрами. Говоря конкретнее, устанавливаем ее у сегмента, сравнивающего переданный нами символ с извлеченным из строки доступных символов. Рассматривать мы будем этот фрагмент ассемблера:

	                             LAB_0801c470                                    XREF[1]:     0801c48c(j)  
        0801c470 69 46           mov        r1,sp
        0801c472 88 18           add        r0,r1,r2 
        0801c474 a1 18           add        r1,r4,r2 ; Обновление указателя на введенный пароль текущим индексом
        0801c476 09 78           ldrb       r1,[r1,#0x0]; r1 содержит значение индекса переданного символа пароля. Например, "B" == 0, "C"==1, и т.д.
        0801c478 c9 18           add        r1,r1,r3; r3 содержит указатель на строку доступных символов. Мы добавляем к этому указателю индекс текущего символа пароля.
        0801c47a 00 78           ldrb       r0=>local_14,[r0,#0x0] ; Загрузка r0 из стека со значением строки "CRDT5" по индексу, указанному r2 
        0801c47c 09 78           ldrb       r1,[r1,#0x0]=>s_BCDFGHJKLMNPQRSTVWXYZ012345678   = "BCDFGHJKLMNPQRSTVWXYZ01234567 ; Загрузка представления символа на основе введенного для пароля значения
        0801c47e 88 42           cmp        r0,r1 ; Сравнение!
        0801c480 00 d0           beq        LAB_0801c484
        0801c482 00 25           mov        r5,#0x0
                             LAB_0801c484                                    XREF[1]:     0801c480(j)  
        0801c484 01 32           add        r2,#0x1; Увеличение счетчика индекса
        0801c486 04 2a           cmp        r2,#0x4
        0801c488 01 dc           bgt        LAB_0801c48e
        0801c48a 00 2d           cmp        r5,#0x0
        0801c48c f0 d1           bne        LAB_0801c470

Введя все вышеприведенные команды, посмотрим, сработает ли точка останова…



Превосходно! Мы не только достигли точки останова, но и зафиксировали все регистры. Теперь проверим, врены ли были все наши предположения в отношении проверки пароля. Прошагаем через несколько инструкций до позиции 0801c474. Здесь мы предполагаем, что r1 будет указывать на массив индексов, представляющих введенные нами символы. Для выяснения этого заглянем в память:

К сведению: если вы делаете отладку удаленно при помощи gdb-multiarch, и при этом некоторые точки останова не срабатывают, попробуйте использовать команду stepi вместо c. Такую проблему я встречал в mGBA ранее, и она не связана с сервером GDB.

(gdb)x/10x $r1
0x2005998:  0x01  0x0d  0x02  0x0f  0x1a  0x00  0x00  0x00
0x20059a0:  0x00  0x4f

Вот оно! Что и следовало ожидать – вместо сохранения фактических символов ascii, вводимых в качестве пароля, сохраняются значения их индексов в таблице доступных символов:



Просто ради проверки, давайте посмотрим, что произойдет, если ввести в качестве пароля CGHDR и установить те же точки останова:

Breakpoint 3, 0x0801c476
Can't determine the current process's PID: you must name one.
(gdb)x/10x $r1
0x2005998:  0x01  0x04  0x05  0x02  0x0d  0x00  0x00  0x00
0x20059a0:  0x00  0x60




Все так и есть! Теперь мы знаем, как сохраняются пароли, и как они выглядят в памяти, а также умеем делать отладку из Ghidra. Думаю, что для данной статьи на этом можно прерваться – в следующей же мы исследуем другие особенности пароля при помощи той же Ghidra и возможностей удаленной отладки GDB.

Заключение


Сегодня мы познакомились с инструментами, позволяющими собрать Ghidra, рассмотрели некоторые из заявленных возможностей отладчика, с помощью которых смогли произвести удаленную отладку игры на Game Boy Advance. Многое из проделанного вы можете выполнить и без Ghidra, используя только gdb-multiarch, но я хотел познакомиться с этими возможностями и попутно поделиться с вами опытом.

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

Дополнительная информация / Примечания





Основные выводы здесь: gvba не работает ни с какими современными GDB. По какой-то причине gdb-multiarch пропускает точки останова, а gdb из devkitarm не отвечает должным образом ghidra для предоставления регистров.

Источник: https://habr.com/ru/company/ruvds/blog/535564/


Интересные статьи

Интересные статьи

История сегодня пойдёт про автосервис в Москве и его продвижении в течении 8 месяцев. Первое знакомство было ещё пару лет назад при странных обстоятельствах. Пришёл автосервис за заявками,...
Одна из самых распространенных тем запросов, с которыми приходят к детским специалистам, это речь. Самая очевидная проблема — это когда ребенок не говорит. Вот ему год, а он молчит. Вот ему уже т...
В интернет-магазинах, в том числе сделанных на готовых решениях 1C-Битрикс, часто неправильно реализован функционал быстрого заказа «Купить в 1 клик».
Привет! Я хотел сказать, что вчера вышла третья наша книга, и тоже очень помогли посты с Хабра (а частично и вошли). История такая: на протяжении примерно 5 лет к нам подходили люди, которые ...
Фабиан известен тем, что уничтожает программы-вымогатели – вирусы, которые криминальные группировки рассылают с целью вымогания денег. Из-за этого ему приходится вести уединённый образ жизни,...