Pull to refresh

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

Reading time5 min
Views3.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 с примерами из жизни можно взять тут.
Tags:
Hubs:
Total votes 17: ↑7 and ↓10-3
Comments14

Articles