• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Гостевая статья Обратный инжиниринг Snapchat (часть I): методы запутывания

Обратный инжиниринг Snapchat (часть I): методы запутывания
2020-06-18 \

Когда у вас более 200 миллионов ежедневных пользователей, вы определенно захотите сохранить свой API закрытым от спамеров и других пользователей, поэтому вам придется хранить секрет в двоичном файле, который разрешает ему выполнять вызовы API на вашем сервере. Snapchat (начиная с версии 10.81.6.81) делает это путем включения X-Snapchat-Client-Auth-Tokenзаголовка в каждый запрос, типичный будет выглядеть так:

v8:7841EAFE02CD9DE06AE8E41C6478D504:2B8115D1C5873C8BD5A3A9DDA7F976B21A672A643D8AB2AC91CE223C84BA5F9EB112B65B7C85AFD9CEA86A9DC36D5F6405B8D23B369A94A5657894207F09E432CBD21953F8E4F50E44373B59FB39270360DE5113FA983D1F06FF71A0D540488403A848D1C52A2421AF4341E6BBCD702F4921E5DC134ECCF99EDBD599EAA1AAA8556C6122334A63C86711740E58E453A7049FE94634DEC8FFE2E26C28780FFA46994818F7D0915E6DB3061188784D46D381CE2BF4D15E83BEC1ABFFE29207D2A58906CAC598AD314F368CF41E1892CA032859485DC99882F97D5064D4C7C5C2A4A4975C59530F4D0289EF4BC4E7CFC89FC8279038FB6E623C88A8AB38678F1D2757F7C0914C1A162E4F5B173E694109CD67E73762D8C090D8780714861DB883977D3B85D6F503D8D8CD5167B43A2DB18B79804841FE8064AD1A8078EAEF472698AD482AA77BC5D7EB012F0946DAFB923CFD10BA06675730EF338A96D1D0081B174BE5989B77FD07DCEDCDC635DEF1EE986F65798D87A358742F152AA929800FD5BA2CC29E

Поток контроля потока:
Забудьте о статическом анализе этого двоичного файла. Вот что они делают на высоком уровне: CFG (управляющий граф потока) уничтожается (не выравнивается), мертвый код, вызовы библиотеки в основном динамические, а все символы для функции генерации токена (давайте назовем ее gen_token) и ее вызывающих абонентов удаляются. , По этой причине они реализованы в C, а не в Objective-C, потому что вы всегда можете использовать среду выполнения ObjC против себя 1 (Общая информация: они только начали использовать Swift недавно, но для других задач.)

Косвенные ветви и непрозрачные предикаты
Давайте посмотрим на самый первый блок в gen_token. Блок загружает некоторые значения из разных разделов двоичного файла, а затем:

orr w8,wzr,#0x3
cmp x8,#0xb
orr w9,wzr,#0x6
csel x8,x9,x8,hi
adrp x28,0x106941000
add x28,x28,#0xe40 ; jump table
ldr x8,[x28, x8, LSL #0x3]
br x8


Смотрите первые две инструкции. Почему они сравнивают x8с 0xbправом после хранения 0x3в нем? Непрозрачные предикаты 1 . cselУсловие всегда будет ложным, но это не имеет значения, потому что, насколько дизассемблер обеспокоен, что это условие, и условие должно оцениваться во время выполнения. Замените каждый прыжок (включая законные условия) аналогичным блоком, и вы полностью уничтожили CFG для любого современного дизассемблера. Теперь Ghidra / IDA были бы рады показать то, что, по ее мнению, представляет собой небольшую функцию с хвостовым вызовом, которая на самом деле является огромной функцией. Я дам Гидре, что он может рассчитать адрес вbr x8но только для первого блока (потому что это, где она думает, что функция заканчивается). Теперь это идея плагина: используйте эмуляцию для вычисления всех адресов в косвенных ветвях с непрозрачными предикатами, что потребует эмуляции. На самом деле я немного поработал над реализацией этого, но тогда это даже не полдела для этого двоичного файла.

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

Динамические вызовы библиотеки
Чтобы сделать код максимально простым и не дать вам образованных догадок, когда вы видите вызов, SecItemCopyMatching
например, большинство вызовов библиотеки являются динамическими. Так, например, вместо простого bl SecItemCopyMatching
они будут делать:

adrp x23 <address of SecItemCopyMatching>

Затем в другом блоке они будут:

blr x23
Дизассемблер не знает значение x23
здесь, потому что, как указано выше, он обрабатывает блок так, как если бы он не блокировал текущую функцию.

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

ldr w8,[sp, #0x278]
str w8,[sp, #0x226c]
ldr w8,[sp, #0x27c]
str w8,[sp, #0x2268]
ldr w8,[sp, #0x280]
str w8,[sp, #0x2264]
ldr w8,[sp, #0x284]
str w8,[sp, #0x2260]
ldr w8,[sp, #0x288]
str w8,[sp, #0x225c]
ldr w8,[sp, #0x28c]
str w8,[sp, #0x2258]
ldr w8,[sp, #0x290]
str w8,[sp, #0x2254]
; and so on

Совместные функции
Предположим, у вас есть функция, которая заполняет некоторую структуру нужными данными, а другую - преобразовывает байты в ASCII:

void set_struct_fields(some_struct *p);
void bin2ascii(char *in, char *out, size_t nbytes);
Приложив немного усилий, вы могли бы перехватывать звонки обоим и понимать, что они делают, просто наблюдая за их поведением. Snapchat имеет довольно умный способ помешать этому. Вместо двух приведенных выше будет:

void joint_function(uint64_t function_id, void *retval, void *argv[]) {
switch (function_id) {
case SET_STRUCT_FIELDS_FI:
// Get argument from argv
set_struct_fields(p);
break;
case BINS2ASCII_FI:
bin2sacii(in, out, nbytes);
break;
// etc
}
}
argv будет включать в себя все необходимые аргументы. Теперь удалите все символы, добавьте вышеупомянутые запутывания, и вы получите непонятную мамонту функции. Вы могли бы подумать, что все равно могли бы отследить все вызовы функции соединения и рассматривать их path_keyкак идентификатор функции, которая вас интересует. Но точки останова не будут работать так, как вы ожидаете. Смотри дальше.

Решение: не точки останова (анти-отладочные меры AKA)
Теперь большая часть обфускации потока управления против статического анализа, использование отладчика для преодоления вышеупомянутого сделало бы это. Не так быстро. Большинство функций вызывают анти-отладочную функцию, которую я назвал соответствующим образом и чья подпись:

uint64_t fuckup_debugging(/* some args */, void *func);

Там как минимум 9 таких функций, все одинаковое поведение. Я не нашел время, чтобы обратить их вспять, но их поведение ясно. \ Программные точки останова работают путем исправления инструкции по указанному адресу в памяти. Патч - это инструкция, которая запускает прерывание, которое обрабатывается родительским процессом, отладчиком 3 . Это делает их легко обнаруживаемыми; если у вас есть контрольная сумма того, как выглядит определенная область в памяти, точка останова в этой области аннулирует контрольную сумму. Или вы можете искать brkбайты инструкции прерывания в двоичном файле.

После проверки fuckup_debugging
вернет uint64_t
значение, которое зависит от того, была ли обнаружена точка останова. Так что на самом деле есть только два возможных значения. Разве это не называется bool? Да. Но логическое значение будет тривиальным для исправления. Но с помощью int вы не можете угадать «правильное» значение. fuckup_debugging
Абонент использует возвращаемое значение (я буду называть его в path_key) для загрузки адреса из таблицы переходов, если есть контрольная точка, извлеченной адрес приведет к бесконечному циклу, что приводит приложение просто держать нагрузку, без обратной связи, который является правильным способом сделать это.

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

Смешанно-булева арифметика
Одна из наименее изученных областей в методах запутывания - MBA (поблагодарите Quarkslab за исследование этого и многих других вопросов). Они обычно используются в криптографии, но могут быть использованы для обфускации. В основном это выражения, которые смешивают логические операции с чистой арифметикой. Например,. x = (a ^ b) + (a & b)\ Интересно, что здесь есть тождества, например, x + yможно переписать как (x ^ y) + 2 * (x & y)[7] . А теперь представьте, насколько огромным x + yможет стать простое выражение, если вы рекурсивно замените каждый член его эквивалентом MBA, сумасшедший материал. \ Пример в сборке. Все, что делает этот блок timestamp * 1000:

add x0,sp,#0x1b8 ; struct timeval *tval
mov x1,#0x0 ; struct timezonze *tzone
adrp x8,0x109499000
ldr x8,[x8, #0x1d0]
blr x8 ; gettimeofday(tval, tone)
ldr x8,[sp, #0x1b8] ; tval->tv_sec
mov w9,#0x3e8
mul x8,x8,x9
ldrsw x9,[sp, #0x1c0]
lsr x9,x9,#0x3
mov x10,#0xf7cf
movk x10,#0xe353, LSL #16
movk x10,#0x9ba5, LSL #32
movk x10,#0x20c4, LSL #48
umulh x9,x9,x10
mov x10,#0xe6b3
movk x10,#0x7dba, LSL #16
movk x10,#0xecfa, LSL #32
movk x10,#0xd0e1, LSL #48
add x9,x10,x9, LSR #0x4
orr x11,x9,x8
lsl x11,x11,#0x1
eor x8,x9,x8
sub x8,x11,x8
eor x9,x8,x10
mov x10,#0xe6b3
movk x10,#0x7dba, LSL #16
movk x10,#0xecfa, LSL #32
movk x10,#0x50e1, LSL #48
bic x8,x10,x8
sub x8,x9,x8, LSL #0x1 ; effectively tv_sec *= 1000

Скретч аргументы
Этот не очень распространен в двоичном коде, но все еще интересно упомянуть. Я видел, как это используется в функции, которая читает первые 8 байтов по указателю. Имеет синягатуру:

uint64_t get_first_qword(uint64_t scratch1, void *src, uint64_t scratch2);
scratch1и 2перезаписываются без использования вообще, опять же, чтобы немного замедлить вас.

Умные покупатели дерьма / времени
В доме memmove?
Чтобы сделать вашу жизнь еще более несчастной, Snap иногда лишает вас возможности распознавать некоторые базовые стандартные функции lib, а именно memmove, реализовывать свои собственные или просто копировать исходный код. Вы не будете очень счастливы, потратив день или два на изменение функции, чтобы найти ее memmoveв конце.

Загрузка переполнена
Еще одно почетное упоминание. У этого есть базовый адрес и индекс, и он загружает байты из массива, используя цикл. Вместо простого добавления базового адреса к счетчику для получения байта они выполняют вычисление, которое выдает два больших 64-разрядных целых числа, которые будут переполнены, но сумма которых будет эквивалентна простому вычислению. Так что вместо:

add x10, sp, #0x338 ;base
ldr x9, [sp, #0x270] ;counter
ldrb w9, [x10, x9]

Они делают:

add x10, sp, #0x338 ;base
ldr x9,[sp, #0x270] ;counter
mov x11,#0x5bdd
movk x11,#0x7d38, LSL #16
movk x11,#0x1e74, LSL #32
movk x11,#0x6d7c, LSL #48
add x9,x9,x11
mov x12,#0x3f94
movk x12,#0x7886, LSL #16
movk x12,#0xf6b2, LSL #32
movk x12,#0xb119, LSL #48
add x9,x9,x12
sub x9,x9,x11
add x9,x9,#0x10
mov x11,#0xd943
movk x11,#0xb8b5, LSL #16
movk x11,#0x5fd9, LSL #32
movk x11,#0x6bd2, LSL #48 ; x11 = 0x6bd25fd9b8b5d943
sub x9,x9,x11
sub x9,x9,x12
add x9,x10,x9 ; x9 = 0x942da027b272bb75
ldrb w9,[x9, x11] ; overflowing sum but right stack offset
__mod_init_func

В двоичных файлах Mach-O - функции, чьи указатели __mod_init_funcsиспользуются ранее main. Используя, otoolчтобы увидеть, сколько из них в Snap, мы находим потрясающие 816 функций:

% otool -s __DATA __mod_init_func Snapchat
Snapchat:
Contents of (__DATA,__mod_init_func) section
0000000106819610 0042de58 00000001 0042de58 00000001
0000000106819620 0042de58 00000001 0042de58 00000001
0000000106819630 0042de58 00000001 0042de58 00000001
0000000106819640 0042de58 00000001 0042de58 00000001
0000000106819650 0042de58 00000001 0042de58 00000001
0000000106819660 0042de58 00000001 0042de58 00000001
0000000106819670 0042de58 00000001 0042de58 00000001
0000000106819680 0042de58 00000001 0042de58 00000001
0000000106819690 0042de58 00000001 0042de58 00000001
00000001068196a0 0042de58 00000001 0042de58 00000001
...and a lot more

Хм, кажется, многое посчитать вручную. Давайте wcэто:

% otool -s __DATA __mod_init_func Snapchat | wc -l
410
А так как в каждой строке два указателя на функции, их фактическое число равно 816 (после отбрасывания первых двух строк). Но подождите, все это указывает на одну и ту же функцию? Вероятно, они используют дубликаты для отвлечения внимания и усложнения вашей работы, давайте посмотрим, сколько их там. Сделав некоторое регулярное выражение для получения указателей на функции, я обнаружил, что есть 769 уникальных функций, все еще огромное количество.

% cat mod_init_func | sort -u | wc -l
769

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

sub sp,sp,#0x10
adrp x8,0x10641a000
add x8,x8,#0x340
str x8,[sp, #0x8]
add sp,sp,#0x10
ret

Среди этих 769 функций некоторые определенно будут выполнять некоторые реальные инициализации, а некоторые могут быть там, как еще одно скрытое обнаружение джейлбрейка / отладчика. Отфильтровывать пустышки должно быть легко, но мы все еще говорим о функции 700+, поэтому, чтобы найти те, которые вас интересуют, вы должны иметь некоторое представление о том, как это делает Snap, так что вы можете добраться без необходимость просеять через все эти функции.

Что дальше
Я, вероятно, сделаю часть II о том, как обойти все это.

Источник:
 
Последнее редактирование:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!