Di bagian
pertama artikel, menggunakan Ghidra, kami secara otomatis menganalisis program crack sederhana (yang kami unduh dari crackmes.one). Kami menemukan cara mengubah nama fungsi "tidak dapat dipahami" tepat di daftar dekompiler, dan juga memahami algoritme program "tingkat atas", mis. yang dilakukan oleh
main () .
Pada bagian ini, kami, seperti yang saya janjikan, akan mengambil analisis fungsi
_construct_key () , yang, seperti yang kami
ketahui , bertanggung jawab untuk membaca file biner yang ditransfer ke program dan memeriksa data yang dibaca.
Langkah 5 - Ikhtisar Fungsi _construct_key ()
Mari kita lihat daftar lengkap fungsi ini segera:
Listing _construct_key ()char ** __cdecl _construct_key(FILE *param_1) { int iVar1; size_t sVar2; uint uVar3; uint local_3c; byte local_36; char local_35; int local_34; char *local_30 [4]; char *local_20; undefined4 local_19; undefined local_15; char **local_14; int local_10; local_14 = (char **)__prepare_key(); if (local_14 == (char **)0x0) { local_14 = (char **)0x0; } else { local_19 = 0; local_15 = 0; _text(&local_19,1,4,param_1); iVar1 = _text((char *)&local_19,*(char **)local_14[1],4); if (iVar1 == 0) { _text(local_14[1] + 4,2,1,param_1); _text(local_14[1] + 6,2,1,param_1); if ((*(short *)(local_14[1] + 6) == 4) && (*(short *)(local_14[1] + 4) == 5)) { local_30[0] = *local_14; local_30[1] = *local_14 + 0x10c; local_30[2] = *local_14 + 0x218; local_30[3] = *local_14 + 0x324; local_20 = *local_14 + 0x430; local_10 = 0; while (local_10 < 5) { local_35 = 0; _text(&local_35,1,1,param_1); if (*local_30[local_10] != local_35) { _free_key(local_14); return (char **)0x0; } local_36 = 0; _text(&local_36,1,1,param_1); if (local_36 == 0) { _free_key(local_14); return (char **)0x0; } *(uint *)(local_30[local_10] + 0x104) = (uint)local_36; _text(local_30[local_10] + 1,1,*(size_t *)(local_30[local_10] + 0x104),param_1); sVar2 = _text(local_30[local_10] + 1); if (sVar2 != *(size_t *)(local_30[local_10] + 0x104)) { _free_key(local_14); return (char **)0x0; } local_3c = 0; _text(&local_3c,1,1,param_1); local_3c = local_3c + 7; uVar3 = _text(param_1); if (local_3c < uVar3) { _free_key(local_14); return (char **)0x0; } *(uint *)(local_30[local_10] + 0x108) = local_3c; _text(param_1,local_3c,0); local_10 = local_10 + 1; } local_34 = 0; _text(&local_34,4,1,param_1); if (*(int *)(*local_14 + 0x53c) == local_34) { _text("Markers seem to still exist"); } else { _free_key(local_14); local_14 = (char **)0x0; } } else { _free_key(local_14); local_14 = (char **)0x0; } } else { _free_key(local_14); local_14 = (char **)0x0; } } return local_14; }
Dengan fungsi ini kita akan melakukan hal yang sama seperti sebelumnya dengan
main () - untuk permulaan kita akan membahas panggilan fungsi "terselubung". Seperti yang diharapkan, semua fungsi ini berasal dari pustaka standar C. Saya tidak akan menjelaskan prosedur untuk mengganti nama fungsi lagi - kembali ke bagian pertama artikel, jika perlu. Sebagai hasil dari penggantian nama, fungsi standar berikut โditemukanโ:
- fread ()
- strncmp ()
- strlen ()
- ftell ()
- fseek ()
- menempatkan ()
Kami mengganti nama fungsi pembungkus yang sesuai dalam kode kami (yang dengan tersembunyi disembunyikan di belakang kata
_text ) dengan menambahkan indeks 2 (sehingga tidak akan ada kebingungan dengan fungsi-C asli). Hampir semua fungsi ini untuk bekerja dengan aliran file. Tidak mengherankan - pandangan sekilas pada kode sudah cukup untuk memahami bahwa ia secara berurutan membaca data dari suatu file (deskriptor yang diteruskan ke fungsi sebagai satu-satunya parameter) dan membandingkan data yang dibaca dengan array dua dimensi tertentu dari byte
local_14 byte.
Mari kita asumsikan bahwa array ini berisi data untuk verifikasi kunci. Sebut saja, katakan
key_array . Karena Hydra memungkinkan Anda untuk mengubah nama tidak hanya fungsi, tetapi juga variabel, kami akan menggunakan ini dan mengubah nama
local_14 yang tidak dapat dipahami menjadi
key_array yang lebih dimengerti. Ini dilakukan dengan cara yang sama seperti untuk fungsi: melalui menu tombol kanan mouse (
Ganti nama lokal ) atau dengan tombol
L dari keyboard.
Jadi, segera setelah deklarasi variabel lokal, fungsi tertentu
_prepare_key () dipanggil :
key_array = (char **)__prepare_key(); if (key_array == (char **)0x0) { key_array = (char **)0x0; }
Kami akan kembali ke
_prepare_key () , ini adalah tingkat ke-3 bersarang dalam hierarki panggilan kami:
main () -> _construct_key () -> _prepare_key () . Sementara itu, kami menerima bahwa itu menciptakan dan entah bagaimana menginisialisasi array dua dimensi "uji" ini. Dan hanya jika array ini tidak kosong, fungsi melanjutkan kerjanya, sebagaimana dibuktikan oleh blok
lain segera setelah kondisi di atas.
Selanjutnya, program membaca 4 byte pertama dari file dan membandingkannya dengan bagian yang sesuai dari array
key_array . (Kode di bawah ini setelah penggantian nama, termasuk variabel
local_19, saya
beri nama
pertama_4bytes .)
first_4bytes = 0; fread2(&first_4bytes,1,4,param_1); iVar1 = strncmp2((char *)&first_4bytes,*(char **)key_array[1],4); if (iVar1 == 0) { ... }
Dengan demikian, eksekusi lebih lanjut hanya terjadi jika 4 byte pertama bersamaan (ingat ini). Kemudian kita membaca 2 blok 2-byte dari file (dan
key_array yang sama digunakan sebagai buffer untuk menulis data):
fread2(key_array[1] + 4,2,1,param_1); fread2(key_array[1] + 6,2,1,param_1);
Dan lagi - selanjutnya fungsi hanya berfungsi jika kondisi berikutnya benar:
if ((*(short *)(key_array[1] + 6) == 4) && (*(short *)(key_array[1] + 4) == 5)) {
Sangat mudah untuk melihat bahwa yang pertama dari blok 2-byte yang dibaca di atas harus nomor 5, dan yang kedua harus nomor 4 (tipe data
pendek hanya menempati 2 byte pada platform 32-bit).
Berikutnya adalah ini:
local_30[0] = *key_array;
Di sini kita melihat bahwa array
local_30 (dideklarasikan sebagai char * local_30 [4]) berisi offset dari pointer
key_array . Yaitu,
local_30 adalah larik garis penanda tempat data dari file tersebut mungkin akan dibaca. Berdasarkan asumsi ini, saya mengganti nama
local_30 menjadi
marker . Di bagian kode ini, hanya baris terakhir yang tampak sedikit mencurigakan, di mana penugasan offset terakhir (pada indeks 0x430, mis. 1072) dilakukan bukan oleh elemen
marker berikutnya, tetapi oleh variabel
local_20 yang terpisah (
char * ). Tapi kita akan mengetahuinya, tetapi untuk sekarang - mari kita lanjutkan!
Selanjutnya kita sedang menunggu siklus:
i = 0;
Yaitu Hanya 5 iterasi dari 0 hingga 4 inklusif. Dalam loop, membaca dari file dan memeriksa kepatuhan dengan array
penanda kami segera dimulai:
char c_marker = 0;
Yaitu, byte berikutnya dari file tersebut dibaca ke dalam variabel
c_marker (dalam kode dekompilasi asli -
local_35 ) dan diperiksa untuk kepatuhan dengan karakter pertama dari elemen
penanda ke-i. Dalam kasus ketidakcocokan, array
key_array adalah nol dan pointer ganda kosong dikembalikan. Lebih jauh di sepanjang kode, kita melihat bahwa ini dilakukan setiap kali data yang dibaca tidak cocok dengan data verifikasi.
Tetapi di sini, seperti yang mereka katakan, "anjing dimakamkan." Mari kita perhatikan siklus ini lebih dekat. Ini memiliki 5 iterasi, seperti yang kami ketahui. Anda dapat memeriksa ini jika ingin dengan melihat kode assembler:


Memang, perintah CMP membandingkan nilai variabel
local_10 (kami sudah memiliki
i ) dengan angka 4 dan jika nilainya
kurang dari atau sama dengan 4 (perintah JLE), transisi ke label
LAB_004017eb dibuat , yaitu. mulai dari tubuh siklus. Yaitu kondisi akan terpenuhi untuk
i = 0, 1, 2, 3, dan 4 - hanya 5 iterasi! Semuanya akan baik-baik saja, tetapi
marker juga diindeks oleh variabel ini dalam satu lingkaran, dan bagaimanapun, array ini dideklarasikan dengan hanya 4 elemen:
char *markers [4];
Jadi, seseorang jelas-jelas berusaha menipu seseorang :) Ingat, saya mengatakan bahwa kalimat ini meragukan?
local_20 = *key_array + 0x430;
Seperti itu saja! Lihat saja seluruh daftar fungsi dan coba temukan setidaknya satu referensi lagi ke variabel
local_20 . Dia tidak ada di sana! Kami menyimpulkan dari ini: offset ini juga harus disimpan dalam array
markers , dan array itu sendiri harus mengandung 5 elemen. Mari kita perbaiki. Pergi ke deklarasi variabel,
tekan Ctrl + L (ketik ulang variabel) dan dengan berani mengubah ukuran array ke 5:

Selesai Gulir ke bawah ke kode untuk menetapkan offset pointer ke
spidol , dan - lihatlah! - variabel tambahan yang tidak bisa dipahami menghilang dan semuanya jatuh pada tempatnya:
markers[0] = *key_array; markers[1] = *key_array + 0x10c; markers[2] = *key_array + 0x218; markers[3] = *key_array + 0x324; markers[4] = *key_array + 0x430;
Kembali ke
loop sementara kami (dalam kode sumber ini kemungkinan besar akan
untuk , tetapi kami tidak peduli). Selanjutnya, byte dari file tersebut dibaca lagi dan nilainya diperiksa:
byte n_strlen1 = 0;
OK,
n_strlen1 ini pasti bukan nol. Mengapa Anda akan melihat sekarang, tetapi pada saat yang sama Anda akan mengerti mengapa saya memberi variabel ini nama berikut:
*(uint *)(markers[i] + 0x104) = (uint)n_strlen1; fread2(markers[i] + 1,1,*(size_t *)(markers[i] + 0x104),param_1); n_strlen2 = strlen2(markers[i] + 1);
Saya menambahkan komentar yang semuanya harus jelas.
N_strlen1 byte dibaca dari file dan disimpan sebagai urutan karakter (yaitu string) ke dalam array
penanda [i] - yaitu, setelah "stop-simbol" yang sesuai, yang sudah ditulis di sana dari
key_array . Menyimpan nilai
n_strlen1 dalam
marker [i] pada offset 0x104 (260) tidak memainkan peran apa pun di sini (lihat baris pertama dalam kode di atas). Bahkan, kode ini dapat dioptimalkan sebagai berikut (dan tentu saja ini terjadi dalam kode sumber):
fread2(markers[i] + 1, 1, (size_t) n_strlen1, param_1); n_strlen2 = strlen2(markers[i] + 1); if (n_strlen2 != (size_t) n_strlen1) { ... }
Itu juga memeriksa bahwa panjang dari baris baca adalah
n_strlen1 . Ini mungkin tampak tidak perlu, mengingat bahwa parameter ini diteruskan ke fungsi
ketakutan , tetapi
ketakutan membaca
tidak lebih dari begitu banyak byte yang ditentukan dan dapat membaca kurang dari yang ditunjukkan, misalnya, dalam hal memenuhi penanda akhir file (EOF). Artinya, semuanya ketat: panjang garis (dalam byte) ditunjukkan dalam file, kemudian garis itu sendiri berjalan - dan tepat 5 kali. Tapi kita maju dari diri kita sendiri.
Lebih lanjut menyirami kode ini (yang saya juga segera berkomentar):
uint n_pos = 0;
Ini masih lebih sederhana di sini: kita mengambil byte berikutnya dari file, menambahkan 7 dan membandingkan nilai yang dihasilkan dengan posisi kursor saat ini dalam aliran file yang diperoleh oleh fungsi
ftell () . Nilai
n_pos harus tidak kurang dari posisi kursor (mis. Offset dalam byte dari awal file).
Baris terakhir dalam loop:
fseek2(param_1,n_pos,0);
Yaitu mengatur ulang kursor file (dari awal) ke posisi yang ditunjukkan oleh
n_pos oleh fungsi
fseek () . OK, kami melakukan semua operasi ini dalam loop 5 kali. Fungsi
_construct_key () diakhiri dengan kode berikut:
int i_lastmarker = 0;
Dengan demikian, blok data terakhir dalam file harus berupa nilai integer 4-byte dan harus sama dengan nilai pada
key_array [0] [1340] . Dalam hal ini, kami akan menerima pesan ucapan selamat di konsol. Jika tidak, array kosong masih kembali tanpa pujian :)
Langkah 6 - Ikhtisar fungsi __prepare_key ()
Kami hanya memiliki satu fungsi yang belum dirakit
tersisa -
__prepare_key () . Kami sudah menduga bahwa di dalamnya data verifikasi dihasilkan dalam bentuk array
key_array , yang kemudian digunakan dalam fungsi
_construct_key () untuk memverifikasi data dari file. Masih mencari tahu data seperti apa di sana!
Saya tidak akan menganalisis fungsi ini secara terperinci dan segera memberikan daftar lengkap dengan komentar setelah semua penggantian nama variabel yang diperlukan:
__Prepare_key () daftar fungsi void ** __prepare_key(void) { void **key_array; void *pvVar1; key_array = (void **)calloc2(1,8); if (key_array == (void **)0x0) { key_array = (void **)0x0; } else { pvVar1 = calloc2(1,0x540); *key_array = pvVar1; pvVar1 = calloc2(1,8); key_array[1] = pvVar1; *(undefined4 *)key_array[1] = 0x404024; *(undefined2 *)((int)key_array[1] + 4) = 5; *(undefined2 *)((int)key_array[1] + 6) = 4; *(undefined *)*key_array = 0x62; *(undefined4 *)((int)*key_array + 0x104) = 3; *(undefined *)((int)*key_array + 0x218) = 0x57; *(undefined *)((int)*key_array + 0x324) = 0x70; *(undefined *)((int)*key_array + 0x10c) = 0x6c; *(undefined *)((int)*key_array + 0x430) = 0x98; *(undefined4 *)((int)*key_array + 0x53c) = 0x462; } return key_array; }
Satu-satunya tempat yang layak dipertimbangkan adalah baris ini:
*(undefined4 *)key_array[1] = 0x404024;
Bagaimana saya mengerti bahwa di sini terletak garis "BATAL"? Faktanya adalah 0x404024 adalah alamat di ruang alamat program yang mengarah ke bagian
.rdata . Mengklik dua kali pada nilai ini memungkinkan kita untuk melihat dengan jelas apa yang ada di sana:

Omong-omong, hal yang sama dapat dipahami dari kode assembler untuk baris ini:
004015da c7 00 24 MOV dword ptr [EAX], .rdata = 56h V
40 40 00
Data yang sesuai dengan baris VOID ada di bagian paling awal
.rdata data (pada titik nol offset dari alamat yang sesuai).
Jadi, saat keluar dari fungsi ini, array dua dimensi harus dibentuk dengan data berikut:
[0] [0]:'b' [268]:'l' [536]:'W' [804]:'p' [1072]:152 [1340]:1122
[1] [0-3]:"VOID" [4-5]:5 [6-7]:4
Langkah 7 - Siapkan biner untuk crack
Sekarang kita dapat memulai sintesis file biner. Semua data awal ada di tangan kita:
1) data verifikasi ("simbol berhenti") dan posisinya dalam array verifikasi;
2) urutan data dalam file
Mari kita kembalikan struktur file yang kita cari sesuai dengan algoritma fungsi
_construct_key () . Jadi, urutan data dalam file adalah sebagai berikut:
Struktur file- 4 byte == key_array [1] [0 ... 3] == "VOID"
- 2 byte == key_array [1] [4] == 5
- 2 byte == key_array [1] [6] == 4
- 1 byte == key_array [0] [0] == 'b' (token)
- 1 byte == (panjang baris berikutnya) == n_strlen1
- n_strlen1 byte == (sembarang string) == n_strlen1
- 1 byte == (+7 == token berikutnya) == n_pos
- 1 byte == key_array [0] [0] == 'l' (token)
- 1 byte == (panjang baris berikutnya) == n_strlen1
- n_strlen1 byte == (sembarang string) == n_strlen1
- 1 byte == (+7 == token berikutnya) == n_pos
- 1 byte == key_array [0] [0] == 'W' (token)
- 1 byte == (panjang baris berikutnya) == n_strlen1
- n_strlen1 byte == (sembarang string) == n_strlen1
- 1 byte == (+7 == token berikutnya) == n_pos
- 1 byte == key_array [0] [0] == 'p' (token)
- 1 byte == (panjang baris berikutnya) == n_strlen1
- n_strlen1 byte == (sembarang string) == n_strlen1
- 1 byte == (+7 == token berikutnya) == n_pos
- 1 byte == key_array [0] [0] == 152 (token)
- 1 byte == (panjang baris berikutnya) == n_strlen1
- n_strlen1 byte == (sembarang string) == n_strlen1
- 1 byte == (+7 == token berikutnya) == n_pos
- 4 byte == (key_array [1340]) == 1122
Untuk lebih jelasnya, saya membuat di Excel tablet seperti itu dengan data file yang diinginkan:

Di sini, di baris ke-7 - data itu sendiri dalam bentuk karakter dan angka, di baris ke-6 - representasi heksadesimal mereka, di baris ke-8 - ukuran setiap elemen (dalam bytes), di baris ke-9 - offset relatif ke awal file. Pandangan ini sangat nyaman karena memungkinkan Anda untuk memasukkan baris apa pun di file yang akan datang (ditandai dengan isian kuning), sementara nilai-nilai panjang garis-garis ini, serta offset posisi simbol pemberhentian berikutnya dihitung dengan formula secara otomatis, sesuai dengan algoritma program. Di atas (dalam baris 1-4), struktur array cek
key_array ditampilkan .
Unggul itu sendiri plus bahan sumber lainnya untuk artikel dapat diunduh di
sini .
Pembuatan dan validasi file biner
Satu-satunya yang tersisa adalah membuat file yang diinginkan dalam format biner dan mengumpankannya dengan crack kami. Untuk menghasilkan file, saya menulis skrip Python sederhana:
Script untuk menghasilkan file import sys, os import struct import subprocess out_str = ['!', 'I', ' solved', ' this', ' crackme!'] def write_file(file_path): try: with open(file_path, 'wb') as outfile: outfile.write('VOID'.encode('ascii')) outfile.write(struct.pack('2h', 5, 4)) outfile.write('b'.encode('ascii')) outfile.write(struct.pack('B', len(out_str[0]))) outfile.write(out_str[0].encode('ascii')) pos = 10 + len(out_str[0]) outfile.write(struct.pack('B', pos - 6)) outfile.write('l'.encode('ascii')) outfile.write(struct.pack('B', len(out_str[1]))) outfile.write(out_str[1].encode('ascii')) pos += 3 + len(out_str[1]) outfile.write(struct.pack('B', pos - 6)) outfile.write('W'.encode('ascii')) outfile.write(struct.pack('B', len(out_str[2]))) outfile.write(out_str[2].encode('ascii')) pos += 3 + len(out_str[2]) outfile.write(struct.pack('B', pos - 6)) outfile.write('p'.encode('ascii')) outfile.write(struct.pack('B', len(out_str[3]))) outfile.write(out_str[3].encode('ascii')) pos += 3 + len(out_str[3]) outfile.write(struct.pack('B', pos - 6)) outfile.write(struct.pack('B', 152)) outfile.write(struct.pack('B', len(out_str[4]))) outfile.write(out_str[4].encode('ascii')) pos += 3 + len(out_str[4]) outfile.write(struct.pack('B', pos - 6)) outfile.write(struct.pack('i', 1122)) except Exception as err: print(err) raise def main(): if len(sys.argv) != 2: print('USAGE: {this_script.py} path_to_crackme[.exe]') return if not os.path.isfile(sys.argv[1]): print('File "{}" unavailable!'.format(sys.argv[1])) return file_path = os.path.splitext(sys.argv[1])[0] + '.dat' try: write_file(file_path) except: return try: outputstr = subprocess.check_output('"{}" -f "{}"'.format(sys.argv[1], file_path), stderr=subprocess.STDOUT) print(outputstr.decode('utf-8')) except Exception as err: print(err) if __name__ == '__main__': main()
Script mengambil path ke crack sebagai parameter tunggal, kemudian menghasilkan file biner dengan kunci di direktori yang sama dan memanggil crack dengan parameter yang sesuai, menerjemahkan output program ke konsol.
Untuk mengonversi data teks menjadi biner, gunakan paket
struct . Metode
pack () memungkinkan Anda untuk menulis data biner dalam format yang menunjukkan tipe data ("B" = "byte", "i" = int, dll.), Dan Anda juga dapat menentukan urutan (">" = "Besar -endian "," <"=" Little-endian "). Urutan default adalah Little-endian. Karena kami sudah menentukan dalam artikel pertama bahwa ini persis kasus kami, maka kami hanya menunjukkan jenisnya.
Semua kode secara keseluruhan mereproduksi algoritma program yang kami temukan. Sebagai garis yang akan dicetak jika berhasil, saya tentukan "Saya memecahkan crackme ini!" (Anda dapat memodifikasi skrip ini sehingga memungkinkan untuk menentukan baris apa pun).
Periksa output:

Hore, semuanya bekerja! Jadi, setelah sedikit berkeringat dan memilah-milah beberapa fungsi, kami dapat sepenuhnya memulihkan algoritma program dan "memecahkannya". Tentu saja, ini hanya retakan sederhana, program pengujian, dan bahkan tingkat kesulitan ke-2 (dari 5 yang ditawarkan di situs itu). Pada kenyataannya, kita akan berurusan dengan hierarki panggilan yang kompleks dan lusinan - ratusan fungsi, dan dalam beberapa kasus - bagian terenkripsi data, kode sampah dan teknik kebingungan lainnya, hingga penggunaan mesin virtual internal dan kode-P ... Tapi ini, seperti yang mereka katakan, sudah cerita yang sama sekali berbeda.
Bahan untuk artikel.