Исходный код также доступен на GitHub здесь. Задача - всего два файла: main.c и challenge_shell.html. Код C компилируется до WASM (index.wasm) и Javascript (index.js). Компилятор также преобразует challenge_shell.html в index.html.

Для компиляции требуется набор инструментов Emscripten. К счастью, Emscripten поддерживает отличные инструкции по установке. После того, как набор инструментов установлен, задача может быть скомпилирована с использованием предоставленного make-файла.

Давайте погрузимся в код и обсудим несколько интересных мне вещей.

Выполнение перед main ()

WASM, как и ELF, поддерживает выполнение функций, определенных программистом до main(). Первоначально я предполагал, что в WASM есть init_array, поэтому я мог просто использовать традиционный атрибут функции конструктор.

void __attribute__((constructor)) hello()

Хотя это технически работает, hello() действительно вызывается до main(), но оказалось, что это не совсем то, что я хотел. При использовании атрибута конструктора сгенерированный Javascript создает массив функций и перебирает их.

При нормальных обстоятельствах можно было бы вызвать hello() из Javascript. Но hello() содержит основную часть логики защиты от отладки, и я бы предпочел, чтобы функция не была очевидна для поиска простым чтением index.js.

Кроме того, я выполняю ключевое слово Javascript debugger из hello(), чтобы уловить использование консоли разработчика. Это трассировка стека, которую реализует конструктор Emscripten, когда веб-консоль Firefox обрабатывает оператор отладчика.

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

Это должно исправить переключение hello() с конструктора на начальный узел. Единственная проблема в том, что я понятия не имею, как заставить Emscripten генерировать начальный узел. Однако формат начального узла Текст WebAssembly (WAT) прост:

(start $start_function)
 - or -
(start 42)

Не зная, сможет ли Emscripten создать начальный узел, я решил добавить его сам. Для начала я переопределил hello(), чтобы он больше не использовал атрибут конструктора и его нельзя было экспортировать.

static void hello()

Имя функции больше не отображается в index.wasm или index.js. Но теперь, чтобы создать начальный узел, мне нужно знать индекс функции hello(). Чтобы понять это, я преобразовал index.wasm в более удобный для человека формат WAT, используя wasm2wat.

albinolobster@ubuntu:~/wasm_challenge$ wasm2wat ./build/index.wasm -o ./build/index.wat

Затем я открыл index.wat и отследил экспортированные функции, которые написал на C.

(export "___syscall12" (func 30))
(export "___syscall18" (func 27))
(export "___syscall188" (func 31))
(export "___syscall42" (func 26))
(export "___syscall72" (func 25))
(export "___syscall80" (func 24))

Каждая из этих функций вызывает hello() в конце функции. Например, __syscall72() в C выглядит так:

void EMSCRIPTEN_KEEPALIVE __syscall72(int p_value)
{
    ... compute result ...
    if (result == 1)
    {
        EM_ASM(
        {
            window['console']['log'] = function(param)
            {
                var result = Module.ccall(
                 '__syscall42', 'void', ['number'], [param]);
            }
        });
    }
    else
    {
        hello();
    }
}

Когда вы смотрите на __syscall72() или индекс функции 25, вы должны найти вызов hello().

(func (;25;) (type 0) (param i32)
  i32.const 2
  local.get 0
  call 14
  i32.const 1
  i32.eq
  if  ;; label = @1
    i32.const 3
    call 13
    drop
  else
    call 33
  end)

call 33 в самом конце должен быть вызовом hello(). Это означает, что начальный узел должен быть записан как: (start 33). Вам просто нужно вставить его в index.wat и преобразовать в index.wasm.

Сейчас он не совсем готов к производству или, откровенно говоря, даже отдаленно не стабилен, но я добавил следующее в make-файл, чтобы автоматически установить hello() в качестве функции запуска.

wasm2wat $(OUTPUT_FOLDER)/index.wasm -o $(OUTPUT_FOLDER)/index.wat
truncate -s -2 $(OUTPUT_FOLDER)/index.wat
echo "\n(start 33))" >> $(OUTPUT_FOLDER)/index.wat
wat2wasm $(OUTPUT_FOLDER)/index.wat -o $(OUTPUT_FOLDER)/index.wasm

Теперь консоль разработчика генерирует следующую трассировку стека.

Такой подход дает злоумышленнику меньше контекста и сохраняет логику вне Javascript, которую людям, по-видимому, легче читать.

Не очень косвенный вызов функции

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

Рассмотрим глобальный указатель на функцию, определенный в main.c.

// Stored indirect call here to be annoying
static void (*g_func_ptr)() = 0;

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

int main(int p_argc, char** p_argv)
{
    // the javascript glue invokes the script this way.
    if (p_argc != 1 || strcmp(p_argv[0], "./this.program") != 0)
    {
        EM_ASM(
        {
            delete window['console']['log'];
            window['console']['log'] = window['console']['assert'];
            exit(0);
        });
    }
    else
    {
        // "call_me_indirectly" should be at address 1 currently.
        g_func_ptr = 1;
    }
    return EXIT_SUCCESS;
}

Где g_func_ptr = 1; указывает на индекс или «адрес» функции с именем call_me_indirectly(). По крайней мере, согласно выводу, который я получил от printf("%p\n", call_me_indirectly);

Указатель на функцию g_func_ptr()в конце концов вызывается в __syscall80().

/*
 * This is the first digit handler. Indirectly call 
 * call_me_indirectly. Just to be annoying. p_value is the value
 * passed into "console.log"
 */
void EMSCRIPTEN_KEEPALIVE __syscall80(int p_value)
{
    ... some anti debug stuff ...
    // call call_me_indirectly based on index in the lookup table
    g_func_ptr(p_value);
}

Но когда я посмотрел на WAT для __syscall80(), я был весьма озадачен.

local.get 0
i32.const 58
call_indirect (type 0)
return

Мало того, что в моем «косвенном вызове» использовалось жестко закодированное значение, я даже не осознавал это значение. Что случилось с g_func_ptr = 1; ?!

Немного почесав голову, я наткнулся на эту таблицу в WAT.

(elem (;0;) (global.get 0) 115 40 44 56 51 51 51 115 116 69 67 70 74 78 79 116 117 41 42 45 46 55 61 63 68 68 91 92 93 94 95 96 97 98 99 117 117 117 117 117 117 117 117 117 117 117 117 117 118 119 80 81 119 120 72 89 120 121 29 59 73 85 88 121 121)

Это таблица глобальных функций. Функция, на которую должен указывать g_func_ptr(), call_me_indirectly(), расположена в индексе 58. Компилятор проделал всю работу по статическому анализу! Никому не нужно выслеживать место установки g_func_ptr(), потому что она уже решена! Просто проиндексируйте глобальную таблицу и продолжайте.

Хотя я думаю, что это отрывочно, так как я установил g_func_ptr() в условной ветке.

Так или иначе. Дело до сих пор не закрыто! Почему g_func_ptr = 1; генерирует call_indirect для 58-й записи в глобальной таблице функций !? Что ж, оказывается, все функции в таблице сгруппированы по типу. Вспомните, как используется g_func_ptr():

// call call_me_indirectly based on index in the lookup table
g_func_ptr(p_value);

WASM очень внимательно относится к типам функций. В задаче определены следующие типы, выраженные в WAT:

(type (;0;) (func (param i32)))
(type (;1;) (func (param i32) (result i32)))
(type (;2;) (func (param i32 i32 i32 i32) (result i32)))
(type (;3;) (func (param i32 i32 i32) (result i32)))
(type (;4;) (func (param i32 i32) (result i32)))
(type (;5;) (func (param i32 i32 i32 i32 i32) (result i32)))
(type (;6;) (func))
(type (;7;) (func (result i32)))
(type (;8;) (func (param i32 i32)))
(type (;9;) (func (param i32 i32 i32 i32 i32 i32)))
(type (;10;) (func (param i32 i32 i32 i32)))
(type (;11;) (func (param f64 f64) (result f64)))
(type (;12;) (func (param f64) (result f64)))
(type (;13;) (func (param i32 i32 i32 i32 i32 i32) (result i32)))

В таблице глобальных функций все функции 57–63 относятся к типу 0 (один параметр i32 и без возвращаемого значения). call_me_indirectly() находится в индексе 58, который является второй записью в группе типа 0. Или, другими словами, call_me_indirectly() - это индекс 1 группы типа 0. Вот почему g_func_ptr = 1; работает в коде C.

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

Компиляция и выполнение во время выполнения

Javascript может компилировать и выполнять байтовые массивы WASM во время выполнения. Вот очень простой пример из __syscall72().

int result = EM_ASM_INT(
{
    /**
     * int oh_no(int p_pressed_key) {
     *     if (p_pressed_key == 9) {
     *       return 1;
     *     }
     *     return 0;
     * }
     */
    var wasm = new Uint8Array([
        0,97,115,109,1,0,0,0,1,134,128,128,128,0,1,96,1,127,1,127,
        3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,
        128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,146,128,128,128,
        0,2,6,109,101,109,111,114,121,2,0,5,111,104,95,110,111,0,0,
        10,141,128,128,128,0,1,135,128,128,128,0,0,32,0,65,9,70,11
    ]);
    var module = new WebAssembly.Module(wasm);
    var module_instance = new WebAssembly.Instance(module);
    var result = module_instance.exports.oh_no($0);
    return result;
}, p_value);

В этом примере WASM в массиве байтов сравнивает переданное значение с числом 9.

Когда я узнал об этом, у меня был вопрос: Хорошо, но как мне на самом деле сгенерировать массив байтов? Я объясню два способа. Первый способ - это WASM Fiddle.

WASM Fiddle, как и многие другие скрипты, является хорошей платформой для обмена фрагментами кода. Также бывает, что для WASM предусмотрены разные варианты вывода. Один из вариантов - это Javascript Uint8Array, который вы можете быстро скопировать и вставить в свой код.

Другой способ, конечно же, - использовать ваш компилятор. В моем случае это emcc Enscriptem.

Как видно из приведенного выше, я использовал следующее для компиляции main.c:

emcc -O1 -s ONLY_MY_CODE=1 -s WASM=1 main.c -o main.html

Затем я преобразовал скомпилированный WASM в массив C с помощью xxd, чтобы я мог легко скопировать и вставить его в код C.

/*
 * int oh_no(int p_pressed_key) { 
 *  if (p_pressed_key == 4) {
 *      return 1;
 *  }
 *  return 0;
 * }
 */
const char wasm[43] =
{
    0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x06,
    0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00,
    0x07, 0x0a, 0x01, 0x06, 0x5f, 0x6f, 0x68, 0x5f, 0x6e, 0x6f,
    0x00, 0x00, 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x41, 
    0x04, 0x46, 0x0b
};
int result = EM_ASM_INT(
{
    // read the C array into a uint8array
    var wasm_array = new Uint8Array($2);
    for (var i = 0; i < $2; i++)
    {
        wasm_array[i] = getValue($1 + i);
    }
    // compile and execute
    var module = new WebAssembly.Module(wasm_array);
    var module_instance = new WebAssembly.Instance(module);
    var result = module_instance.exports._oh_no($0);
    return result;
}, p_value, wasm, 43);

В двух только что приведенных примерах есть одна интересная особенность: имеет значение, где определен массив WASM (например, C или Javascript). Все, что определено в EM_ASM, интерфейсе Javascript в C, появится в index.js. Однако массив, определенный в C, хранится в index.wasm, что затрудняет поиск и чтение.

Самое интересное в этих массивах заключается в том, что вы можете изменять их как хотите, прежде чем передавать их компилятору. Например, в __syscall18() я сохранил байт-код WASM как данные в кодировке xor.

/*
 * int oh_no(int p_pressed_key) { 
 *  if (p_pressed_key == 7) {
 *      return 1;
 *  }
 *  return 0;
 * }
 */
char wasm[97] =
{
    170,203,217,199,171,170,170,170,171,44,42,42,42,170,171,202,
    171,213,171,213,169,40,42,42,42,170,171,170,174,46,42,42,
    42,170,171,218,170,170,175,41,42,42,42,170,171,170,171,172,
    43,42,42,42,170,170,173,56,42,42,42,170,168,172,199,207,
    199,197,216,211,168,170,175,197,194,245,196,197,170,170,160,39,
    42,42,42,170,171,45,42,42,42,170,170,138,170,235,173,236,
    161,
};
for (int i = 0; i < 97; i++)
{
    wasm[i] = (wasm[i] ^ 0xaa) & 0xff;
}
int result = EM_ASM_INT(
{
    // read the C array into a uint8array
    var wasm_array = new Uint8Array($2);
    for (var i = 0; i < $2; i++)
    {
        wasm_array[i] = getValue($1 + i);
    }
    // compile and execute
    var module = new WebAssembly.Module(wasm_array);
    var module_instance = new WebAssembly.Instance(module);
    var result = module_instance.exports.oh_no($0);
    return result;
}, p_value, wasm, 97);

Уже много лет мы видим обфусцированный Javascript. Должно быть интересно посмотреть, что мир делает с запутанным WASM.

Заключение

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