Как стать автором
Поиск
Написать публикацию
Обновить

Эффективный способ защиты от пиратства

Время на прочтение5 мин
Количество просмотров3.7K
Если вы разработчик ios приложений, то скорее всего, тема пиратства вам знакома, болезненна и малоприятна. Надеюсь, вам будет интересно разобраться как ей препятствовать и что нужно делать, что бы не увидеть свое приложение в репозитории hackulo.us через час после релиза в appstore.

В поисках легкой наживы


Для начала, рассмотрим, что предприняла Apple для нашей с вами защиты. Называется этот DRM — FairPlay и в его компетенцию входит полное шифрование исполняемого файла, что по замыслу яблока, должно препятствовать реверс-инжинирингу и несанкционированному копированию. В целом, со своими задачами система справляется, но только до того момента, пока наше приложение не попадет на iУстройство. Дело в том, что процессор не умеет выполнять шифрованные инструкции, поэтому операционная система после загрузки файла в память и непосредственно перед его исполнением, производит его полную расшифровку. Вот именно эту особенность и эксплуатирует весь кракерский софт. Дожидаясь когда приложение будет полностью расшифровано в памяти и готово к исполнению, кракерская утилита дампит его образ в файл.

Для наглядности, покажу как это можно сделать с помощью gdb:
break main                   # установим точку прерывания в самое начало
command 1               # и когда она будет достигнута, дампим расшифрованное содержимое в файл output.bin
    dump memory output.bin 0x2000 cryptsize # cryptsize берется через otool -l в секции LC_ENCRYPTION_INFO
    kill
    quit
end
start

Затем, меняем пару служебных полей в заголовке исполняемого файла mach-o и Info.plist. Вот и все. Наше приложение готово выйти в свободное плавание. Но, самое неприятное, что с релизом полностью автоматизированных утилит, таких как Crackulous, проделать эту операцию может любой школьник, даже не имея малейшего представления о том, как все устроено.

Естественно, такое положение вещей не могло оставить сообщество разработчиков равнодушными. Можно легко нагуглить с десяток решений, которые помогут справиться со школьниками, но совершенно беспомощны против даже начинающего кракера. Почему? Давайте рассмотрим подробнее. Основная идея проверок заключается в том, что бы отследить изменения в бандле приложения. Например, разное время создания исполняемого файла и Info.plist, или размер Info.plist отличается от эталонного, или отсутствует директория _CodeSignature. Как и положено приложениям для ios, все эти проверки написаны на ObjC c использованием фреймворков.

Попробуйте открыть исполняемый файл вашего приложения в текстовом редакторе, а еще лучше через консольную утилиту strings. Видите? Все что вы используете, лежит как на ладони. Достаточно загрузить бинарник в IDA и посмотреть, кто и где в коде использует все эти NSBundle, NSDate и NSFileSize. В каком месте используются «грозные» предупреждения Crack Detected и т.д. Обнаружив проверку, взломщику совсем не составит труда изменить 1-2 байта так, что бы проверка всегда возвращала удобное значение.

Недостаток всех этих способов заключается в том, что они очень легко находятся статическим анализом, потому что используют фреймворки, методы и классы ObjC. Значит, нам нужен такой способ, который нельзя было бы найти по общим шаблонам, написанный на чистом С, без использования классов и фреймворков. Вот лучшее из того, что мне показал гугл:
#import <mach-o/dyld.h>
#define LC_ENCRYPTION_INFO 0x21

struct encryption_info_command {
    uint32_t cmd;
    uint32_t cmdsize;
    uint32_t cryptoff;
    uint32_t cryptsize;
    uint32_t cryptid;
};

static BOOL is_encrypted () {
    // в оригинальном коде этот адрес искали с помощью dladdr(main, &dlinfo), но на iphone это константа,
    // так что, нет смысла лишний раз светиться использованием dl-функций
    struct mach_header *header = 0x1000;

    struct load_command *cmd = (struct load_command *) (header+1);

    for (uint32_t i = 0; cmd != NULL && i < header->ncmds; i++) {
        /* Encryption info segment */
        if (cmd->cmd == LC_ENCRYPTION_INFO) {
            struct encryption_info_command *crypt_cmd = (struct encryption_info_command *) cmd;
            /* Check if binary encryption is enabled */
            if (crypt_cmd->cryptid < 1) {
                /* Disabled, probably pirated */
                return NO;
            }
            
            /* Probably not pirated? */
            return YES;
        }
        
        cmd = (struct load_command *) ((uint8_t *) cmd + cmd->cmdsize);
    }
    /* Encryption info not found */
    return NO;
}

Идея заключается в том, что в заголовке mach-o исполняемого файла есть флаг cryptid в секции LC_ENCRYPTION_INFO, отображающий зашифрован бинарник или нет. На основе его значения делается вывод о происхождении данного приложения.

На этом месте, все хорошие идеи в паблике заканчиваются, дальше только приват


Несмотря на то, что приведенный выше код достаточно сложно обнаружить статическим анализом, злоумышленник может запустить наше приложение на своем джейлбрейкнутом телефоне под gdb. И в режиме пошагового выполнения он будет в состоянии распутать любую нашу защиту в рекордный промежуток времени!
К счастью, Apple позволяет нам запрещать такую трассировку:
ptrace(PT_DENY_ATTACH, 0, 0, 0);

После ее исполнения, любые попытки отлаживать наше приложение, будут сегфолтить, «посягнувший на святое», процесс. Можно сказать, что на этом фронте мы одержали полную и безоговорочную победу!
Но не спешите радоваться, поскольку ptrace — это функция, по сути, врапер, а не настоящий системный вызов, ее легко найти и обезвредить, например так:
break ptrace    # останавливаемся в функции ptrace
commands 1
   return       # и просто возвращаемся из нее, естественно без выполнения самого вызова
   continue
end
run

Как же быть? Отказываться от fancy api и делать системный вызов своими руками!
Адепты С могут вспомнить о:
#include <sys/syscall.h>
syscall(SYS_ptrace, PT_DENY_ATTACH, 0, 0, 0);

Но, во-первых syscall — функция, с которой можно поступить так же как и с ptrace, а во-вторых, это часть private api и за ее использование программу могут не пропустить цензоры.
Так что, нам придется делать системный вызов без посредников, прямиком на ассемблере, благо это совсем не страшно:
asm {
    mov r0, #31     // PT_DENY_ATTACH
    mov r1, #0
    mov r2, #0
    mov r3, #0
    mov ip, #26     // SYS_ptrace
    svc #0x80       // собственно сам вызов
}

А вот найти и обезвредить эти инструкции будет несоизмеримо сложнее, особенно если таких вызовов будет 5-10 и они будут раскиданы по всему коду программы.

Венцом нашей защиты должна быть проверка целостности исполняемого файла, необходимо проверять контрольную сумму секции с кодом (__text). Смысл в том, что даже изменив один байт в этой секции, изменится и контрольная сумма, соответственно мы отследим попытку модифицировать файл! Поскольку в штатных условиях наш файл зашифрован и нет смысла пытаться прочитать его содержимое, мы поступим как взломщики и прочитаем секцию, после того как операционная система загрузит и расшифрует ее сама. Необходимую информацию о смещении и длине можно узнать с помощью:
otool -l myPrecious.app/binary
...
Section
  sectname __text
   segname __TEXT
      addr 0x00002000
      size 0x00096980
    offset 4096
     align 2^2 (4)
    reloff 0
    nreloc 0
     flags 0x80000400
 reserved1 0
 reserved2 0
...

Интересующий нас код находится начиная с уже знакомого нам адреса 0x2000, а длина соответственно 0x96980.
Шаблон этой проверки может быть, например, таким:
__attribute__((always_inline)) void my_secret_checksum() {
    u_char *buf = 0x2000;
    u_int res;

    for (int i = 0; i < 0x96980; i++) {
        res += buf[i] .....  //алгоритм подсчета контрольной суммы crc/adler и тд
    }

    if (res != ...) {
        // Achtung! Partizanen!
    }
}

Обратите внимание на атрибут always_inline, он позволяет в разных местах программы размещать не просто вызов к этой функции, а непосредственно ее код. Таким образом, если мы разместим эту проверку в нескольких местах, то взломщику придется найти и обезвредить их все. И пока он этого не сделает, любая попытка модифицировать исполняемый файл будет пресекаться. В идеале, стоит разместить ее в каждом вашем методе или функции. Чтение из памяти — очень быстрый процесс, так что, на производительности ее частое использование никак не отразится. А перспектива выпиливать ее из всего кода должна оттолкнуть даже самого целеустремленного взломщика.

Послесловие


Защита — это противодействие нападению. Противодействуя одной атаке, мы не защищенны от другой. Поэтому, эффективной защитой может быть только комплекс проверок. Я постарался осветить необходимый минимум. И надеюсь, этот материал вдохновит читателя к собственным поискам и исследованиям. Удачи вам! И пусть уровень ваших продаж не омрачают романтики с большой дороги!

Литература


хорошую инструкцию на тему Patching Iphone Application с примерами из жизни можно взять тут.
Теги:
Хабы:
Всего голосов 17: ↑7 и ↓10-3
Комментарии14

Публикации

Ближайшие события