Pull to refresh

Once upon a time in Wasmwood или Как я реверсил игру на WASM

Reading time7 min
Views3.4K

Преамбула

Прошел уже месяц с того момента, как я попытал счастья в решении реверс челленджа, заключающегося в поиске пасхалок в игре скомпилированной в WASM, на платформе root-me. С тех пор я подумывал запостить это на хабр, но никак руки не доходили. К счастью, все же удалось перебороть себя. Статью я планирую написать так, будто челлендж еще не решен, т.е с воспроизведением всех умозаключений и рассуждений, которые имели место в процессе его решения.

Суть челленджа

Описание челленджа
Описание челленджа

То есть в мире с 1000 npc есть только один, который может нам помочь. Ну-с, летс гет стартед, как говорится.

Начало

Игра начинается со следующего сетапа

Попробуем повзаимодействовать с кем-нибудь

Заключаем, что ничего полезного этот npc нам не расскажет. Что ж, придется копать глубже - скачаем wasm-бинарник и исследуем его. В качестве инструмента исследования возьмем wabt или WAsm Binary Toolkit. Посмотрим, что у нас там имеется.

Как можно заметить, инструментов здесь предостаточно, но нам понадобится только один, а именно wasm2js, конвертирующий wasm в js код. Конечный продукт пестрит нереальными комбинациями сложно написанного, обфусцированного js кода, состоящего из кучи непонятных функций, но в таком стоге сена мне удалось найти парочку интересных игл. Мой весьма скудный опыт разработки игр подсказывает, что где-то должен быть главный игровой цикл, где и происходит вся логика обновления позиции игрока, рендеринг мира, хендлинг эвентов и т.д. Такая функция в коде есть, и называется она, к моему величайшему удивлению, main_loop.

 function main_loop() {
  var $2 = 0;
  $2 = __stack_pointer - 16 | 0;
  __stack_pointer = $2;
  handle_events();
  HEAP32[($2 + 12 | 0) >> 2] = clock() | 0;
  HEAP32[($2 + 8 | 0) >> 2] = (HEAP32[($2 + 12 | 0) >> 2] | 0) - (HEAP32[(0 + 351648 | 0) >> 2] | 0) | 0;
  HEAP32[(0 + 351648 | 0) >> 2] = HEAP32[($2 + 12 | 0) >> 2] | 0;
  render_game(HEAP32[($2 + 8 | 0) >> 2] | 0 | 0);
  SDL_RenderPresent(HEAP32[(0 + 315092 | 0) >> 2] | 0 | 0);
  __stack_pointer = $2 + 16 | 0;
  return;
 }

Хмм, посмотрим: выделение стековой памяти под переменные, вызов функций. Думаю, что следующим на очереди у нас будет handle_events.

 function handle_events() {
  var $20 = 0, $2 = 0, $90 = 0;
  $2 = __stack_pointer - 64 | 0;
  __stack_pointer = $2;
  label$1 : {
   label$2 : while (1) {
    if (!(SDL_PollEvent($2 + 8 | 0 | 0) | 0)) {
     break label$1
    }
    label$3 : {
     if (!((HEAP32[($2 + 8 | 0) >> 2] | 0 | 0) == (768 | 0) & 1 | 0)) {
      break label$3
     }
     label$4 : {
      label$5 : {
       if ((HEAPU8[(0 + 315112 | 0) >> 0] | 0) & 1 | 0) {
        break label$5
       }
       $20 = HEAP32[($2 + 28 | 0) >> 2] | 0;
       label$6 : {
        label$7 : {
         label$8 : {
          if (($20 | 0) == (32 | 0)) {
           break label$8
          }
          label$9 : {
           label$10 : {
            if (($20 | 0) == (97 | 0)) {
             break label$10
            }
            if (($20 | 0) == (100 | 0)) {
             break label$9
            }
            if (($20 | 0) == (113 | 0)) {
             break label$10
            }
            label$11 : {
             if (($20 | 0) == (115 | 0)) {
              break label$11
             }
             label$12 : {
              if (($20 | 0) == (119 | 0)) {
               break label$12
              }
              if (($20 | 0) == (122 | 0)) {
               break label$12
              }
              if (($20 | 0) == (1073741886 | 0)) {
               break label$7
              }
              if (($20 | 0) == (1073741903 | 0)) {
               break label$9
              }
              if (($20 | 0) == (1073741904 | 0)) {
               break label$10
              }
              if (($20 | 0) == (1073741905 | 0)) {
               break label$11
              }
              if (($20 | 0) != (1073741906 | 0)) {
               break label$6
              }
             }
             label$13 : {
              if (!(HEAP32[(0 + 315136 | 0) >> 2] | 0)) {
               break label$13
              }
              move(0 | 0, -1 | 0);
             }
             HEAP32[(0 + 315136 | 0) >> 2] = 3;
             break label$6;
            }
            label$14 : {
             if (!((HEAP32[(0 + 315136 | 0) >> 2] | 0 | 0) != (3 | 0) & 1 | 0)) {
              break label$14
             }
             move(0 | 0, 1 | 0);
            }
            HEAP32[(0 + 315136 | 0) >> 2] = 0;
            break label$6;
           }
           label$15 : {
            if (!((HEAP32[(0 + 315136 | 0) >> 2] | 0 | 0) != (2 | 0) & 1 | 0)) {
             break label$15
            }
            move(-1 | 0, 0 | 0);
           }
           HEAP32[(0 + 315136 | 0) >> 2] = 1;
           break label$6;
          }
          label$16 : {
           if (!((HEAP32[(0 + 315136 | 0) >> 2] | 0 | 0) != (1 | 0) & 1 | 0)) {
            break label$16
           }
           move(1 | 0, 0 | 0);
          }
          HEAP32[(0 + 315136 | 0) >> 2] = 2;
          break label$6;
         }
         interact();
         break label$6;
        }
        emscripten_run_script(9447 | 0);
       }
       break label$4;
      }
      $90 = HEAP32[($2 + 28 | 0) >> 2] | 0;
      label$17 : {
       label$18 : {
        label$19 : {
         if (($90 | 0) == (32 | 0)) {
          break label$19
         }
         if (($90 | 0) == (1073741886 | 0)) {
          break label$18
         }
         break label$17;
        }
        HEAP8[(0 + 315112 | 0) >> 0] = 0;
        break label$17;
       }
       emscripten_run_script(9447 | 0);
      }
     }
    }
    continue label$2;
   };
  }
  __stack_pointer = $2 + 64 | 0;
  return;
 }

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

 function interact() {
  var $2 = 0, $3 = 0, $4 = 0;
  $2 = __stack_pointer - 16 | 0;
  __stack_pointer = $2;
  $3 = 0;
  HEAP32[($2 + 12 | 0) >> 2] = $3;
  $4 = HEAP32[($3 + 315136 | 0) >> 2] | 0;
  label$1 : {
   label$2 : {
    switch ($4 | 0) {
    case 3:
     HEAP32[($2 + 12 | 0) >> 2] = get_entity_at(HEAP32[(0 + 315120 | 0) >> 2] | 0 | 0, (HEAP32[(0 + 315124 | 0) >> 2] | 0) - 1 | 0 | 0) | 0;
     label$6 : {
      if (!((HEAP32[($2 + 12 | 0) >> 2] | 0 | 0) != (0 | 0) & 1 | 0)) {
       break label$6
      }
      HEAP32[((HEAP32[($2 + 12 | 0) >> 2] | 0) + 20 | 0) >> 2] = 0;
     }
     break label$1;
    case 0:
     HEAP32[($2 + 12 | 0) >> 2] = get_entity_at(HEAP32[(0 + 315120 | 0) >> 2] | 0 | 0, (HEAP32[(0 + 315124 | 0) >> 2] | 0) + 1 | 0 | 0) | 0;
     label$7 : {
      if (!((HEAP32[($2 + 12 | 0) >> 2] | 0 | 0) != (0 | 0) & 1 | 0)) {
       break label$7
      }
      HEAP32[((HEAP32[($2 + 12 | 0) >> 2] | 0) + 20 | 0) >> 2] = 3;
     }
     break label$1;
    case 2:
     HEAP32[($2 + 12 | 0) >> 2] = get_entity_at((HEAP32[(0 + 315120 | 0) >> 2] | 0) + 1 | 0 | 0, HEAP32[(0 + 315124 | 0) >> 2] | 0 | 0) | 0;
     label$8 : {
      if (!((HEAP32[($2 + 12 | 0) >> 2] | 0 | 0) != (0 | 0) & 1 | 0)) {
       break label$8
      }
      HEAP32[((HEAP32[($2 + 12 | 0) >> 2] | 0) + 20 | 0) >> 2] = 1;
     }
     break label$1;
    case 1:
     break label$2;
    default:
     break label$1;
    };
   }
   HEAP32[($2 + 12 | 0) >> 2] = get_entity_at((HEAP32[(0 + 315120 | 0) >> 2] | 0) - 1 | 0 | 0, HEAP32[(0 + 315124 | 0) >> 2] | 0 | 0) | 0;
   label$9 : {
    if (!((HEAP32[($2 + 12 | 0) >> 2] | 0 | 0) != (0 | 0) & 1 | 0)) {
     break label$9
    }
    HEAP32[((HEAP32[($2 + 12 | 0) >> 2] | 0) + 20 | 0) >> 2] = 2;
   }
  }
  label$10 : {
   if (!((HEAP32[($2 + 12 | 0) >> 2] | 0 | 0) != (0 | 0) & 1 | 0)) {
    break label$10
   }
   show_quote(HEAP32[($2 + 12 | 0) >> 2] | 0 | 0);
  }
  __stack_pointer = $2 + 16 | 0;
  return;
 }

Строчка 7 демонстрирует доступ к полю какой-то структуры. Можно догадаться, что структура представляет собой стейт игрока, а поле - направление, в которое игрок смотрит в данный момент. На основании направления происходит доступ к npc путем вызова функции get_entity_at. Ну и в завершении вызывается функция show_quote, в которую прокидывается npc в качестве аргумента. Но все же меня очень интересует функция show_quote. Вдруг там и спрятан флаг.

 function show_quote($0) {
  $0 = $0 | 0;
  var $3 = 0, $17 = 0, $39 = 0;
  $3 = __stack_pointer - 48 | 0;
  __stack_pointer = $3;
  HEAP32[($3 + 44 | 0) >> 2] = $0;
  HEAP32[($3 + 12 | 0) >> 2] = 0;
  label$1 : {
   label$2 : while (1) {
    if (!(((HEAPU8[((HEAP32[($3 + 12 | 0) >> 2] | 0) + 9424 | 0) >> 0] | 0) & 255 | 0 | 0) != (0 & 255 | 0 | 0) & 1 | 0)) {
     break label$1
    }
    $17 = 24;
    HEAP8[(($3 + 16 | 0) + (HEAP32[($3 + 12 | 0) >> 2] | 0) | 0) >> 0] = (((HEAPU8[((HEAP32[($3 + 12 | 0) >> 2] | 0) + 9424 | 0) >> 0] | 0) << $17 | 0) >> $17 | 0) ^ ((((HEAP32[(HEAP32[($3 + 44 | 0) >> 2] | 0) >> 2] | 0) - 1 | 0) + (HEAP32[($3 + 12 | 0) >> 2] | 0) | 0 | 0) % (256 | 0) | 0) | 0;
    HEAP32[($3 + 12 | 0) >> 2] = (HEAP32[($3 + 12 | 0) >> 2] | 0) + 1 | 0;
    continue label$2;
   };
  }
  HEAP8[($3 + 38 | 0) >> 0] = 0;
  $39 = HEAP32[((HEAP32[($3 + 44 | 0) >> 2] | 0) + 12 | 0) >> 2] | 0;
  HEAP32[$3 >> 2] = $3 + 16 | 0;
  snprintf(315152 | 0, 300 | 0, $39 | 0, $3 | 0) | 0;
  HEAP8[(0 + 315112 | 0) >> 0] = 1;
  __stack_pointer = $3 + 48 | 0;
  return;
 }

Хмм, интересный цикл. В цикле вычисляется, по всей видимости, адрес строки, которую нужно показать. И все же нет смысла глядеть в статичный код, нужно что-нибудь динамическое в виде какого-нибудь дебаггера по типу cheat engine, но для браузера. Поиски натолкнули меня к интересному расширению под названием Cetus.

Посмотрю-ка я в раздел строк для начала.

Не, ничего примечательного. Придется пойти сложным путем - копать. Перейдем в раздел Memory View. Для начала я хочу подтвердить свою гипотезу о том, что по адресу 315136 располагается структура игрока. Сделаем так

Действительно, при перемещении игрока можно заметить меняющееся значение первого байта, что есть направление. Но мне все-таки интересно посмотреть на поведение игры во время взаимодействия с npc. В этот момент мне в голову приходит только один вопрос: есть ли какое-то уникальное свойство у npc, не меняющееся в течение всей игры? Конечно, позиция! Они ведь всегда статичны. Тогда если ввести хотя бы x координату, то можно и на структуру npc напороться.

Я ушел очень далеко, чтобы сузить пространство поиска. Попробуем найти что-нибудь в памяти со значением -546 (x-координата мадмуазель). Для этого я перейду в раздел Search.

Супер, только 2 ячейки памяти с таким значением. Путем перебора я быстро выяснил, что то, что мне нужно располагается по адресу 0x0004f2b8. Что ж, сходим туда.

Несложно заключить, что по адресу 0x0004f2bc располагается y-координата. Мне интересно, если есть что-нибудь до x-координаты. Вычтем 4 и посмотрим.

Хм, тут число 0x000000f6. Мне не совсем понятно, что оно значит, но предположим, что это какой-нибудь entityID по совместительству являющееся началом структуры. Анализируя функцию show_quote я нашел фрагмент, вычисляющий адрес строки.

$39 = HEAP32[((HEAP32[($3 + 44 | 0) >> 2] | 0) + 12 | 0) >> 2] | 0;

Упростим строчку до

$39 = HEAP32[(0x0004f2b4 + 12)]

Вычислив эффективный адрес, можно поглядеть, что находится в этой ячейке.

Число 0x000020cc. Просто число или адрес какой-нибудь штуки? Я больше склоняюсь ко второму. Что ж, продолжим копать.

Бинго! Да это же наша строка! В этот момент передо мной предстает выбор:
A) Я бегу по всей карте и взаимодействую с каждым npc, которого вижу, до тех пор, пока не найду нужного

Б) Найду строку, хоть сколько-нибудь напоминающую строку с флагом, ну и на основании уже имеющихся знаний реверсну процесс.

Полагаю, что выбор очевиден, господа.

Для поисков флага пришлось использовать wasm-objdump с параметром -s, ну и греппать слово flag. Нашлась форматирующая строка "You found me! Your flag is %s". Далее остается только ввести это в Cetus и выяснить, что адресом строки является 0x00002588.

И вот почти весь пазл собран, но остается одна недостающая деталь - нам нужно найти позицию npc, который держит флаг. Для этого снова введем в строку поиска значение 0x00002588.

Лишь один результат - 0x00050b5c. Теперь лишь остается пойти в память и ввести туда значение 0x00050b5c - 12, так как изначально к началу структуры прибавлялось число 12. До флага осталось всего ничего, но мне хочется оставить это для тебя, дорогой читатель.

Заключение

Конечно, мне не удалось воспроизвести полный ход мыслей во время решения этого челленджа, так как цепочка мыслей и действий очень велика по той причине, что на его решение я потратил 2 дня. Тот факт, что в то время я занимался исследованием самого формата WebAssembly, только увеличил дозу дофамина в 5x раз, когда мне удалось наконец-таки решить его. Ну и я очень рад, что появилась тема для первого поста на Хабре :)

Tags:
Hubs:
+21
Comments5

Articles

Change theme settings