Информация [Reverse-Engineering] Разбор протектора Matreshka Mobile

mansurov

Новичок
Автор темы
16
12

Всем привет, читатели, в этой "статье" будет разбор проты либки в топ 2 по популярности КРМП Мобайл (Матрешка РП)​


Впервые пишу статейку на бластхаке. Решил я чот подергать клиент Матрешки Мобайл (пакет com.matreshkarp.game), а точнее их либку lib/arm64-v8a/libmatreshka.so. Игруха на флаттере (там libflutter.so + libapp.so на 22 мега дартового кода), а вся сердцевина клиента спрятана вот в этом чуде. Название крмпшки будто придумывали под либку (хотя первая версия вышла в 21 году, а такая защитка примерно с 23),


Использовал IDA Pro 9.4, плюс по мелочи idapython и разок питон для энтропии, т.к. в иде ее из коробки нет. Ушло на все часов пять +-, потому что либка жирная и наглухо обфусцирована.


По сегментам:​


Кидаю libmatreshka.so в иду, несколько минут жевала либку. Первый сюрприз от разрабов - 49 мегабайт веса либки. Для либки это многовато, уже попахивало что внутри что-то запаковано.

Смотрю сегменты смотреть как оно разложено, и вот тут первые непонятки:

Два сегмента LOAD, и оба исполняемые (r-x). Обычно .text один. А тут два куска кода, причём у второго другое выравнивание. сегмент с именем .__data1, оно нестандартное. Флаг X (executable), виртуалка 0x110000, а вот файловый оффсет 0x1850000, то есть в самом конце файла. Размер +- 23 мегабайта.


Прикол в том, что реальный андроидный линкер грузит по тем самым load, а ида и прочие тулы больше верят таблице секций. И вот .__data1 говорит дизассемблеру, чтобы код для адреса 0x110000 брало из хвоста либки. Чуть посравнял регион 0x110000 и хвост 0x1850000 совпадают один в один, ровно 0x1680000 байт, то есть в файле лежат две одинаковые копии образа кода. Вот тебе и +23 мегабайта к весу.

Зачем копия хз конечн, но по логике одну копию (ту что в LOAD) стаб дешифрует и правит прямо в памяти, а вторая в хвосте, нетронутый оригинал, чтоб было откуда перечитать / чем сверить целостность. Ну или просто исходный запакованный блоб.


Экспорты либки​


С этим смирился, и полез в экспорты, и там мне тоже впихнули сюрприз:

Код:
FgljZF
EfuDv
TU_IRRKSbIHXfGKLaOde
STxSxXETASFdVJRLIbO
NOsNsSxzHxKEKE
... ещё пара тыщ таких

Имена рандомизированы, читать нечего. Но между этой шелухой торчат вменяемые JNI-функи:

Код:
JNI_OnLoad
Java_com_native_Main_load / preInit / appEvent / touchEvent / windowSize / setSinglePlayer / hasFlutter
Java_com_native_RenderQueue_Lock / Unlock
Java_com_native_libLoader_base / getSym / execEnd

43184318431843.png


Отдельно зацепил libLoader_getSym / base / execEnd. Это походу свой лоадер, ну или то есть либка умеет сама подтягивать и разворачивать какие-то куски.



Где ваще точка входа?​


Логично начать с .init_array, ну и перешел туда, а оно из 141 нуля, хм.

85319913851313.png


Подумал сначала, что я не туда полез, дальше глянул .rela.dyn, и картина проясняется, почти все релокации это тупо R_AARCH64_NONE (штук 7000+), а нормальных RELATIVE всего т четыре. Для такой либки это чушь, если обычно там тысячи. Ну и вот одна из этих четырех как раз пишет в .init_array:

Код:
0x17b21a0   R_AARCH64_RELATIVE   ->  0x101ca28

То есть на диске инит пустой, а живой поинтер дорисовывается релокацией уже в памяти. Один-единственный. И ведёт он на 0x101ca28, это начало второго (открытого) сегмента, точнее спрятанной точки входа. Прикольн, глазами в статике инит пустой, старта не видно.

Кста, про none релокации, это не просто так, раз настоящие релокации выпилены в none, значит привязку поинтеров стаб делает сам из своей спрятанной таблицы. Практический вывод на будущее, тупо снять дамп памяти и переиспользовать не выйдет, без ручного релокатора он развалится, дефолтный анти-дамп



Поломанный граф​


Перехожу на 0x101CA28, пролог нормальный, регистры сохраняет, а дальше иду плющит, то есть граф не строится, сплошные красные стрелки и куски undefined. Ну и и видно за что цепляется:

96456694316531.png


Короче обфускация потока трамплинами. Схема всегда одна: adr reg берёт свой же адрес, add reg, #0x14, br reg, и прыжок ровно на +20, перескакивая два мертвых слова. А в этих словах либо левый бранч, либо редкая sve-хрень (uaddlb, ld1rqb), на которой дизасм рассыпается. И так по всей либке, не только в стабе я потом посчитал, таких add reg,#0x14; br reg под 800 штук.

Руками это читать нереально, поэтому накидал скриптик на idapython, чисто пройтись по трамплинам и вычислять реальную цель (adr запомнили базу, на add прибавили, на br прыгнули). После этого поток разворачивается в нормальную линию.

Python:
import idc, ida_ua

def follow(ea, limit=200):
    reg = {}
    seen = set()
    while limit and ea not in seen:
        limit -= 1; seen.add(ea)
        mnem = idc.print_insn_mnem(ea)
        print(hex(ea), idc.GetDisasm(ea))
        if mnem in ("ADR", "ADRP"):
            reg[idc.print_operand(ea,0)] = idc.get_operand_value(ea,1)
        elif mnem == "ADD":
            d, s = idc.print_operand(ea,0), idc.print_operand(ea,1)
            if d == s and d in reg:
                reg[d] += idc.get_operand_value(ea,2)
        elif mnem == "BR":
            r = idc.print_operand(ea,0)
            if r in reg:
                ea = reg[r]; continue
            break
        elif mnem == "B":
            ea = idc.get_operand_value(ea,0); continue
        elif mnem == "RET":
            break
        ea = idc.next_head(ea)

follow(0x101CA28)



Что в стабе вообще, черт возьми​


Пошаговые действия стаба:

Ищет свои маркеры первым делом. Гоняет ldrb-ом по памяти и сверяет байты: сначала сигнатуру FF FF BE D9 (начало блока), потом C0 03 5F D6 (а это, если развернуть, ret конец блока):

Код:
LDRB W10, [X8], #1  ; CMP #0xFF
LDRB W10, [X8]      ; CMP #0xFF
LDRB W10, [X9, #2]  ; CMP #0xBE
LDRB W10, [X9, #3]  ; CMP #0xD9   -> FF FF BE D9

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

83145895318951.png


Ну и затирает маркеры

Код:
MOV  W11, #0x201F
MOVK W11, #0xD503, LSL #16   ; W11 = 0xD503201F == NOP
STP  W11, W11, [X9]          ; два nop поверх сигнатуры

Ну и дергает ядро мимо libc, то есть память оно готовит не через mmap/mprotect из libc, а сырыми svc:

Код:
MOV X8, #0xDE   ; 222 = mmap
SVC #0
MOV X8, #0xE2   ; 226 = mprotect   (RW чтоб записать расшифрованное, потом обратно R-X)
SVC #0
MOV X8, #0xD7   ; 215 = munmap
SVC #0

То есть, если кто повесил хук на libcшные mmap/mprotect (фрида, LD_PRELOAD и т.п.), он нихера не увидит, потому что вызов идет напрямую в ядро, ну а всего таких прямых сисколов я насчитал десяточку : mprotect - 4, mmap - 2, munmap - 2 и один gettimeofday (169), последний удобен как замер времени, палить отладчик/эмуль по тормозам.

Декодер инструкций и проверка себя, оно читает по 32-битному слову и делает сдвиги на 21/27/25, это ровно поля класса инструкции в arm64, прогоняет через simd и мешает с магией:

Код:
LDR   W4, [X8]              ; слово кода
DUP   V17.4S, W4
LSR   W23, W4, #0x15        ; >>21  \
LSR   W24, W4, #0x1B        ; >>27   > поля опкода
LSR   W25, W4, #0x19        ; >>25  /
BFXIL W24, W4, #0x1E, #2    ; тасует поля
...
EOR   X3, X3, X12           ; X12 = 0xDEADBEEF
ADD   X3, X3, X13           ; X13 = 0x0BADCAFE
AND   X3, X3, X14           ; X14 = 0x55555555

Тут расшифровка тела (см. ниже про энтропию). Во-вторых checksum, оно нормализует адресные поля у bl/adrp (они же плавают от релокаций) и сворачивает все в хэш, ну и пропатчил инструкцию = сумма не сойдётся, в простонародье антитампер, ну и константы 0xDEADBEEF / 0x0BADCAFE / 0x55555555, по ним, кстати, потом легко найти само сравнение хэша.

713587517875843.png




Что же с телом игры? (зашифровано)​


Пошел искать обычные функции, типа, Java_com_native_Main_load (по экспортам это 0x10B56A4). Перехожу, а там вообще хз пойми что, иде дизассемблирить нечего, сплошные dcb/undefined и рандомные байты:

Код:
0x10B56A4:  6F 8A 66 F7 8B 04 B2 41 EA 03 5F D6 9F 09 EC 0E ...

Пошел в дамп хекса, листаю первый сегмент 0x110000..0xF00000, и это хз что вообще, ни одного осмысленного пролога, ничего, а у иды крыша едет, само собой.

В иде энтропии нет, поэтому быстро посчитал питоном по окошкам. Вышло:

Код:
первый сегмент  (0x110000..0xF00000)   H ≈ 7.9 .. 8.0   - рандом, то есть шифр
дыра            (0xF00000..0x1000000)  H = 0            - одни нули (место под bss)
второй сегмент  (0x1000000..0x1791BD4) H ≈ 5.5 .. 7.4   <- нормальный код (загрузчик)

15388135853114.png
15385138583113.png


Ну, значит тут реально матрешка, если первый сегмент (+- 15 мегабайт) зашифрованное тело клиента, энтропия почти 8.0, нормальный код так не выглядит (у него 5.5-6.5). Сюда указывают все "настоящие" экспорты, но в статике они мертвые, а второй сегмент (+- 7.8 мегабайт) открытый (хоть и обфусцированный трамплинами) лоадер, вот его и его одного мы реально видим в иде: стаб 0x101CA28, JNI_OnLoad на 0x1412170 (декодер)

При dlopen порядок такой: линкер мапнул оба сегмента → сработал тот единственный инит 0x101CA28 - стаб своими сисколами дешифрует и релоцирует первый сегмент, попутно сверяя целостность → и уже живому телу отдаёт руль. Пока это не отработало, реверсить в первом сегменте нечего, там шум.



Что же еще наверху?​


pac + bti, прологи функций собраны с поинтер-аутентификацией (ARMv8.3) и BTI (8.5). Ида в новых версиях их прям так и показывает PACIASP в начале и BTI C на целях переходов. Я прикинул по первым словам функций: PACIASP стоит у +-748 функций, BTI C у +-595. Против ROP и грубого перебивания переходов самое то, ну и наивный анализ прологов сбивает.

153883158185131.png


Строки зашифрованы, ни ptrace, ни TracerPid, ни frida, ни /proc/... в открытую нет, только огрызки, энтропия .rodata тоже +-8 , значит строки крутятся расшифрованными в рантайме, в статике их не почитать.

Импорты при этом сами по себе говорящие, даже если строки спрятаны:

Код:
syscall, mmap, mprotect (через svc), open, read, stat, access,
opendir / readdir / fdopendir     шарится по /proc (TracerPid, maps)
getpid, gettid, pthread_self      сверка потоков
sigaction, raise, strsignal       сигнальные хендлеры (SIGTRAP/SIGSEGV):
setjmp, longjmp                       ловля брейков, восстановление потока
dl_iterate_phdr, dladdr           сам себя находит, сверяет свои сегменты





(я не декриптал либку, а анализировал в ida pro)​
 

Вложения

  • 15388135853114.png
    15388135853114.png
    25.5 KB · Просмотры: 18
  • 153883158185131.png
    153883158185131.png
    15.6 KB · Просмотры: 172
Последнее редактирование: