Введение.
Данная статья - это не критика современного подхода к "упаковыванию" талбеток, а, скорее, желание предложить еще один подход. Так что целью данной статьи является сравнение традиционных методик упаковывания кряков и методик основанных на Java технологии. Небольшое отступление от темы. Никогда, даже под угрозой расстрела не произносите слово "Java" как "Ява". Это признак не дальновидности в отношении java-технологий и языка непосредственно. Так что, при чтении данной статьи, увидев слово java, бурчите под нос "джава", а не "ява" =) Итак, поехали.

В данной статье я буду рассматривать два самых простых типа "лекарств": это патчи и кейгены. Под патчем (patch) принято понимать исполняемый файл, который заменяет/удаляет/добавляет несколько байт в файл, который мы собственно "патчим".
Часть первая. Патчи.
Примером будет для нас Lingvo 9.0.2.76 (ER). Итак, проведя бессонные ночи над дизассемблированием Лингвы и въезжания в алгоритм того, как ЛингвА проверяет, что она не одна такая в локалке с таким серийником, мы поняли, что нужно всего навсего поменять несколько байт в экзешнике (пропатчить) и все будет работать как по маслу. Мы выяснили, что менять нужно следующее:


OFFSET
OLD BYTE
NEW BYTE
0x00050C45
0x84
0x33
0x000797B6
0xFF
0xE8
0x000797B7
0x52
0x29
0x000797B8
0x78
0x94
0x000797B9
0x8B
0xBD
0x000797BA
0xF0
0x38
0x000797BB
0x3B
0x8B
0x000797BC
0xF3
0xF0
0x000797C0
0x75
0xEB
0x0010C3E2
0x0F
0xB3
0x0010C3E3
0x95
0x01
0x0010C3E4
0xC3
0x90
0x00121DF3
0x85
0x33

Так же как и в любом, уважающем себя кряке мы должны реализовать проверку на длину файла и CRC. В нашем случае длина оригинального файла составляет 4120576 байт, а CRC32 равно 1927184142.


Ну что же, примерный алгоритм таков:
- Прочитать файл Lingvo.exe;
- Проверить длину и CRC32;
- Сделать бэкап;
- Пропатчить файл;

Приведу сразу же получившийся код патчера и ниже приведу объяснения.

1    import java.io.*; 
2    import java.nio.channels.FileChannel; 
3    import java.nio.MappedByteBuffer; 
4    import java.util.zip.Checksum; 
5    import java.util.zip.CRC32; 
6     
7    class Patch { 
8        static long   FILE_SIZE  = 4120576; 
9        static long   FILE_CRC32 = 1927184142; 
10       static String FILE_NAME  = "Lingvo.exe"; 
11       static int  BYTES_TO_CHANGE = 13; 
12       static long[] offsets   = new long[BYTES_TO_CHANGE]; 
13       static byte[] newBytes  = new byte[BYTES_TO_CHANGE]; 
14    
15       static { 
16           int i = 0; 
17           offsets[i] = 0x00050C45; newBytes[i++] = (byte)0x33; 
18           offsets[i] = 0x000797B6; newBytes[i++] = (byte)0xE8; 
19           offsets[i] = 0x000797B7; newBytes[i++] = (byte)0x29; 
20           offsets[i] = 0x000797B8; newBytes[i++] = (byte)0x94; 
21           offsets[i] = 0x000797B9; newBytes[i++] = (byte)0xBD; 
22           offsets[i] = 0x000797BA; newBytes[i++] = (byte)0x38; 
23           offsets[i] = 0x000797BB; newBytes[i++] = (byte)0x8B; 
24           offsets[i] = 0x000797BC; newBytes[i++] = (byte)0xF0; 
25           offsets[i] = 0x000797C0; newBytes[i++] = (byte)0xEB; 
26           offsets[i] = 0x0010C3E2; newBytes[i++] = (byte)0xB3; 
27           offsets[i] = 0x0010C3E3; newBytes[i++] = (byte)0x01; 
28           offsets[i] = 0x0010C3E4; newBytes[i++] = (byte)0x90; 
29           offsets[i] = 0x00121DF3; newBytes[i++] = (byte)0x33; 
30       } 
31    
32       public static void main(String[] args) { 
33           try { 
34               File source = new File(FILE_NAME); 
35               File dest = new File(FILE_NAME + ".bak"); 
36    
37               if (source.length() != FILE_SIZE) { 
38                   System.out.println("Incorrect file size"); 
39                   return; 
40               } 
41    
42               if (FILE_CRC32 != getChecksumValue(new CRC32(), FILE_NAME)) { 
43                   System.out.println("CRC32 Error! Version missmatch or file is already patched"); 
44                   return; 
45               } 
46    
47               copyFile(source, dest); 
48    
49               RandomAccessFile fis = new RandomAccessFile(source, "rw"); 
50               for (int i = 0; i < BYTES_TO_CHANGE; i++) { 
51                   fis.seek(offsets[i]); 
52                   fis.write(newBytes[i]); 
53               } 
54               fis.close(); 
55               System.out.println(FILE_NAME + " - successfully patched"); 
56           } catch (Exception e) { 
57               e.printStackTrace(); 
58           } 
59       } 
60    
61       public static void copyFile(File source, File dest) throws IOException { 
62           FileChannel in = null, out = null; 
63           try { 
64               in = new FileInputStream(source).getChannel(); 
65               out = new FileOutputStream(dest).getChannel(); 
66    
67               long size = in.size(); 
68               MappedByteBuffer buf = in.map(FileChannel.MapMode.READ_ONLY, 0, size); 
69    
70               out.write(buf); 
71    
72           } finally { 
73               if (in != null) in.close(); 
74               if (out != null) out.close(); 
75           } 
76       } 
77    
78       static long getChecksumValue(Checksum checksum, String filename) { 
79           try { 
80               BufferedInputStream is = new BufferedInputStream(new FileInputStream(filename)); 
81               byte[] bytes = new byte[1024]; 
82               int len = 0; 
83    
84               while ((len = is.read(bytes)) >= 0) checksum.update(bytes, 0, len); 
85    
86               is.close(); 
87           } catch (IOException e) { 
88               System.out.println("Can't read from " + filename); 
89           } 
90           return checksum.getValue(); 
91       } 
92    
93   }

Итак, что же мы видим.Строки 1-5, как обычно, импорт классов, которыми мы будем пользоваться. Далее (7) объявление класса. Строки с 8 по 13 - объявление глобальных переменных. Все, что нам нужно знать: размер файла, его CRC, имя и кол-во байт которое мы хотим изменить. За эти значения отвечают поля FILE_SIZE, FILE_CRC32, FILE_NAME и BYTES_TO_CHANGE соответственно.

С 15 по 30 строку следует статический блок, в котором происходит инициализация смещений, по которым нужно будет писать, и собственно те байты, которые нужно писать. Т.е. это представление таблицы 1 с отсутствующим за ненадобностью средним столбцом.

32-59 основная часть программы. Итак. Для начала (34-35) создаем ссылки на файлы (который хотим пропатчить и на бэкап). 37-45 проверяем совпадают ли размер и CRC с теми что у нас есть? Если да то продолжаем работу. Итак остался последний шаг, пропатчить файл, что мы успешно (50-53) и делаем, не забывая (47) сделать бэкап, на всякий пожарный.

Ниже 61 - 91 представлены утилитные функции для выполнения таких стандартных действий, как копирование файлов и вычисление CRC32 для файла. Не буду останавливаться на особенностях их реализации.

Написав, сей код и сохранив его в файле Patch.java его нужно скомпилировать.
>javac Patch.java
Мы получили Patch.class - готовый патч для Лингво, который весит 3226 байт. Помещаем его в директорию Лингвы и запускаем
>java Patch
Lingvo.exe - successfully patched


Ну что же. Хороший результат. Но мы его улучшим. Дело в том, что обычно Java классы распространяются в виде одного или нескольких Джава Архивов или попросту JAR'ов (Java ARchive). Jar формат абсолютно ничем не отличается от zip файлов. В принципе, можно зазиповать наш Patch.class обычным зипом и потом поменять расширение с zip на jar. Но мы воспользуемся стандартной утилитой поставляемой вместе с JDK. Которая так и называется jar.

Для начала создадим файл манифест. Он необходим для решения ряда задач, связанных с инициализацией приложения. В частности, в файле манифеста прописывается какой класс является главным (main) - в него и будут переданы все параметры, если таковые имеются.

Т.к. класс у нас один, ему и быть главным. Создаем файл MANIFEST.MF с одной единственной строчкой
Main-Class: Patch
Сохраняем и в командной строке выполняем команду
>jar cvfm patch.jar MANIFEST.MF Patch.class
added manifest
adding: Patch.class(in = 3226) (out= 1835)(deflated 43%)


Получаем файл patch.jar размером 2313 байт. Давайте его запустим
>java -jar patch.jar
Lingvo.exe - successfully patched


Ну что же, работает! Да и в весе стало полегче. P.S. стандартный кряк к данной версии весит порядка 17Кб. Безусловно, даже начинающий ассемблерщик напишет подобный патч весом менее 300байт, а гуру сделают этот патч еще в два раза легче. Однако, выше описанный способ тоже имеет право на жизнь =)

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

Для примера возьмем абстрактную программу, которая при регистрации просит Вас ввести: Имя, Фамилию, Название Вашей Организации и серийный номер продукта. Вы выяснили, что программа считает, что серийник правильный, если он совпадает с CRC, взятым от конкатенации строк: Имени, Фамилии и Названия Предприятия. Т.е. если Вы введете серийник, равеный CRC32("ВасяПупкинЛарек"), то программа посчитает Вас честным пользователем, если Вы укажете соответствующие параметры в Имени, Фамилии и Организации.

Итак, наша задача написать простенький кейген, в котором будут присутствовать поля для ввода Имени, Фамилии, Организации. А так же большая кнопка "Сгенерировать серийник", для получения серийного номера на основе введенных Вами данных.

Сразу приведу исходный код Кейгена для разбора:

1    import java.awt.*; 
2    import java.awt.event.*; 
3    import javax.swing.*; 
4    import java.util.zip.CRC32; 
5     
6    public class Keygen  extends JFrame { 
7      String fields[] = {"First name", "Last name", "Organization", "Serial number"}; 
8      JLabel[] labels = new JLabel[fields.length]; 
9      JTextField[] textfields = new JTextField[fields.length]; 
10    
11     public Keygen() { 
12       super("Mazafaka.Ru - keygen"); 
13       addWindowListener(new WindowAdapter() { 
14           public void windowClosing(WindowEvent e) {System.exit(0);} 
15       }); 
16    
17      JPanel mainPanel = new JPanel(false); 
18      mainPanel.setLayout(new GridLayout(1,0)); 
19    
20      JPanel namePanel = new JPanel(false); 
21      namePanel.setLayout(new GridLayout(0, 1)); 
22    
23      JPanel fieldPanel = new JPanel(false); 
24      fieldPanel.setLayout(new GridLayout(0, 1)); 
25    
26      for (int i = 0; i < fields.length; i++) { 
27        labels[i] = new JLabel(fields[i] + ": ", JLabel.RIGHT); 
28        namePanel.add(labels[i]); 
29    
30        textfields[i] = new JTextField(); 
31        fieldPanel.add(textfields[i]); 
32      } 
33    
34      mainPanel.add(namePanel); 
35      mainPanel.add(fieldPanel); 
36    
37      JPanel bottom = new JPanel(); 
38      JButton generateButton = new JButton("Generate"); 
39      generateButton.addActionListener(new ActionListener() { 
40                   public void actionPerformed(ActionEvent e) { 
41                     String[] params = new String[textfields.length]; 
42                     for (int i=0; i < params.length - 1; i++) 
43                         params[i] = textfields[i].getText(); 
44                     generate(params); 
52      setBounds(300,200,300,21*fields.length + 65); 
53      setVisible(true); 
54     } 
55    
56     public void generate(String[] params) { 
57       String crc32 = ""; 
58       for (int i=0; i < params.length - 1; i++) crc32 += params[i]; 
59       CRC32 crc = new CRC32(); 
60       crc.update(crc32.getBytes()); 
61       textfields[textfields.length - 1].setText("" + crc.getValue()); 
62     } 
63    
64     public static void main(String[] args) { 
65       Keygen kg = new Keygen(); 
66     } 
67   }

Ну что же, давайте разберем, что мы тут понаписали.

Основная задача при написании кейгена была такая: написать такой классик, чтобы на его основе можно было легко "клепать" в будущем кейгены, не заботясь о том, что надо нарисовать окошки и т.п. А что из себя обычно представляет кейген? В сущности, это алгоритм, который на основании N введенных строчек, выдает еще одну (две, три) строчки. Каждая строчка - это поле которое должен заполнить пользователь (или получить результат).

Ок, наш класс будет разделен на несколько частей:
GUI - независимая от алгоритма часть и никогда не изменяемая
Алгоритм - тут крякер будет реализовывать алгоритм
Список полей - собственно то, что пользователь должен заполнить/получить

Итак, поехали!
Строки 1-4 импортирование нужных для работы классов
6 - наследуемся от JFrame т.к. наша программа будет представлять из себя окошко (невашно какое - виндовое или гнилуксовое)
7 - очень важная строка. Тут мы перечисляем все поля, которые хотим представить пользователю. Как уже говорилось, у нас будут Имя, Фамилия, Организация. Ну и конечно поле для серийника - надо же куда-то результат выводить =) Чтобы изменить (добавить/удалить) поля в кейгене, достаточно изменить массивчик fields. Для людей, разбирающихся в джаве, сразу скажу, что я не уделял внимание архитектуре самого кода, дабы не загромождать читателя. Поэтому Вы не увидете final static перед полем fields =)

Далее (8-9) объявляем массивы графических компонентов: меток и контролов для ввода текста. Их колличество, конечно же, равно кол-ву строк в массиве fields

Конструктор (11-54) очень рутинная работа с гуйней. Определяем специфические менеджеры компоновки, расставляем метки и ТекстФилды (26-32). Наконец, создаем кнопку (38) и создаем для нее обработчик события на нажатие. Обработчик таков, что обходит все текстовые поля и собирает значения, которые в них содержаться. Далее, передает все это в метод generate, который уже решает, что делать со всем этим добром. В нашем случае, последний элемент в массиве параметров, будет всегда пустым, т.к. поле Serial Number пользователь заполнять не будет.

Ну и сердцевина нашего кейгена - метод generate (56). Как и говорилось выше, мы делаем конкатенацию всех строк, которые ввел пользователь (57-58) и вычисляем CRC32 получившейся строки (59-60). Далее, заполняем получившимся значением поле Serial Number (61). Теперь, проделаем операцию по компилированию и упаковке аналогично тому, как я делал это в первой части. Вот и все. Смотрим, что из этого всего получилось.

Различие в упаковке в jar файл только в том, что теперь нам нужно засунуть целых три класса, получившихся после компиляции. Но это не проблема
>jar cvfm keygen.jar MANIFEST.MF Keygen.class Keygen$1.class Keygen$2.class

Итого, имеем jar файл размером 2,850 байт. Неплохо для мультиплатформенной, гуевой апликухи =)

back content next