Tài liệu Hướng dẫn Crackme 1 của Mer

Công cụ Chúng ra sẽ cần Stripper, IDA, và một chương trình biên dịch ngôn ngữC. Việc giải nén khá là đơn giản. Ta sẽ dùng Stripper v2.0.7 để giải nén crackme này. Bài viết này không chú trọng vào việc giải nén.

pdf12 trang | Chia sẻ: haohao89 | Lượt xem: 2292 | Lượt tải: 1download
Bạn đang xem nội dung tài liệu Tài liệu Hướng dẫn Crackme 1 của Mer, để tải tài liệu về máy bạn click vào nút DOWNLOAD ở trên
Hướng dẫn Crackme 1 của Merc Introduction: Giới thiệu Bài viết này sẽhướng dẫn người đọc cách đọc code crackme đầu tiên của Merc đểtừđó viết chương trình keygen. Tools: Công cụChúng ra sẽcần Stripper, IDA, và một chương trình biên dịch ngôn ngữC. Việc giải nén khá là đơn giản. Ta sẽdùng Stripper v2.0.7 đểgiải nén crackme này. Bài viết này không chú trọng vào việc giải nén. Guess Phỏng đoán: Chạy thửcrackme, ta thấy crackme đòi nhập một chuỗi nào đó rồi nhấn nút Check. Nhấn nút Check thì crackme tựđộng thoát ra. Việc kết thúc chương trình một cách ép buộc này làm ta nghĩngay đến các hàm ExitProcess, TerminateProcess, SendMessage, PostMessage và các hàm tương tự. Disasm: Dịch ngược và hiểu mã Ta dùng IDA đểdịch ngược. Sau khi đã dịch ngược xong, chọn cửa sổNames window, tìm hàm ExitProcess hoặc PostQuitMessage. Tại sao? Vì khi ởcông ty thì tác giảbài viết phát hiện ra từhàm ExitProcess có thểlần ngược lại hàm kiểm tra chuỗi. Nhưng đến khi về nhà thì tác giảkhông còn lần ngược lại được từExitProcess nữa mà phải từPostQuitMessage. Quay trởlại cửa sổIDA View. Cuộn lên trên cho tới khi thấy đoạn mã sau. .text:00401440 83+ sub esp, 7Ch .text:00401443 53 push ebx .text:00401444 8B+ mov ebx, ecx .text:00401446 33+ xor ecx, ecx .text:00401448 56 push esi .text:00401449 89+ mov [esp+9], ecx .text:0040144D 57 push edi .text:0040144E 89+ mov [esp+11h], ecx .text:00401452 A0+ mov al, byte_418148 .text:00401457 89+ mov [esp+15h], ecx .text:0040145B BE+ mov esi, offset aGoodJobTheSolu ; "Good Job! The Solution Is : " .text:00401460 66+ mov [esp+19h], cx .text:00401465 8D+ lea edi, [esp+2Ch] .text:00401469 88+ mov [esp+1Bh], cl]]> Đưa con trỏtới dòng .text:00401440, nhấn phím P đểkhai báo một hàm mới bắt đầu từđịa chỉđó. Tại sao chúng ta biết? Vì ngay trên đó ta thấy align 10h chia cách các hàm. Cũng đặt con trỏtại đây, nhấn N, nhập vào check vì ta sẽthấy trong vài đoạn sau đây chính là hàm kiểm tra chuỗi. Lần theo mã đến một đoạn có rất nhiều lệnh mov sau. .text:00401482 B8+ mov eax, 58h .text:00401487 89+ mov [esp+88h+var_67], edx .text:0040148B 89+ mov [esp+88h+var_30], eax .text:0040148F 89+ mov [esp+88h+var_20], eax .text:00401493 89+ mov [esp+88h+var_63], edx .text:00401497 8D+ lea eax, [esp+88h+String] .text:0040149B 6A+ push 3Ch ; nMaxCount .text:0040149D 66+ mov [esp+8Ch+var_5F], dx .text:004014A2 A4 movsb .text:004014A3 50 push eax ; lpString .text:004014A4 68+ push 3E8h ; nIDDlgItem .text:004014A9 8B+ mov ecx, ebx .text:004014AB 88+ mov [esp+94h+var_5D], dl .text:004014AF C7+ mov [esp+94h+var_3C], 31h .text:004014B7 C7+ mov [esp+94h+var_38], 47h .text:004014BF C7+ mov [esp+94h+var_34], 23h .text:004014C7 C7+ mov [esp+94h+var_2C], 38h .text:004014CF C7+ mov [esp+94h+var_28], 74h .text:004014D7 C7+ mov [esp+94h+var_24], 27h .text:004014DF C7+ mov [esp+94h+var_1C], 39h .text:004014E7 C7+ mov [esp+94h+var_18], 73h .text:004014EF C7+ mov [esp+94h+var_14], 76h .text:004014FA C7+ mov [esp+94h+var_10], 43h .text:00401505 C7+ mov [esp+94h+var_C], 60h .text:00401510 C7+ mov [esp+94h+var_8], 2Fh .text:0040151B C7+ mov [esp+94h+var_4], 79h .text:00401526 33+ xor esi, esi .text:00401528 E8+ call sub_40E21E]]> Đầu tiên, eax được gán giá trị0x58. Sauđó var_30 và var_20 được gán giá trịcủa eax. Ởcác dòng mov sau, ta thấy một loạt liên tiếp từ var_3C tới var_4 được gán các giá trị"cứng". Chú ý kỹta thấy rằng từvar_3C tới var_4 là một mảng 15 biến kiểu int tất cả. Nhấn kép chuột vào var_3C, sau đó nhấn chuột vào chuỗi var_3C, nhấn * và nhập 15 vào ô Array size. Ta có thểđổi tên biến var_3C thành target bằng cách nhấn chuột vào chuỗi var_3C, nhấn N và nhập vào target. Nhấn kép vào sub_40E21E tại dòng lệnh call ởcuối ta thấy rằng hàm này thực chất là hàm gói GetDlgItemTextA. Đổi tên sub_40E21E thành getInputText. Vì đây là hàm nhận dữliệu nhập, ta cũng nên đổi tên biến String thành input. Đoạn mã C tương tựnhưsau. int target[] = {0x31, 0x47, 0x23, 0x58, 0x38, 0x74, \ 0x27, 0x58, 0x39, 0x73, 0x76, 0x43, 0x60, 0x2F, 0x79}; char input[60]; getInputText(input, 0x3C); // đây chỉlà hàm giảđểnhận dữliệu nhập ]]> Chúng ta sẽgặp đoạn mã kếtiếp thường xuyên nên bài viết sẽchỉ nói qua một lần.Đoạn mã kếbao gồm bốn lệnh lea edi, X, tới repe scasb, rồi not ecx, và dec ecx. Tất cảbốn lệnh này chỉthực hiện một việc là tính chiều dài chuỗi được chỉtới bởi edi. .text:0040152D 8D+ lea edi, [esp+88h+input] .text:00401531 83+ or ecx, 0FFFFFFFFh .text:00401534 33+ xor eax, eax .text:00401536 33+ xor edx, edx .text:00401538 F2+ repne scasb .text:0040153A F7+ not ecx .text:0040153C 49 dec ecx]]> Chúng ta bắt đầu tìm hiểuđoạn mã kếtiếp có mục đích gì. .text:0040152D 8D+ lea edi, [esp+88h+input] .text:00401531 83+ or ecx, 0FFFFFFFFh .text:00401534 33+ xor eax, eax .text:00401536 33+ xor edx, edx .text:00401538 F2+ repne scasb .text:0040153A F7+ not ecx .text:0040153C 49 dec ecx .text:0040153D 74+ jz short loc_40155A .text:0040153F .text:0040153F loc_40153F: ; CODE XREF: check+118]]>↓<![CDATA[j .text:0040153F 8A+ mov cl, [esp+edx+88h+input] .text:00401543 8D+ lea edi, [esp+88h+input] .text:00401547 88+ mov byte ptr [esp+edx+88h+var_6C], cl .text:0040154B 83+ or ecx, 0FFFFFFFFh .text:0040154E 33+ xor eax, eax .text:00401550 42 inc edx .text:00401551 F2+ repne scasb .text:00401553 F7+ not ecx .text:00401555 49 dec ecx .text:00401556 3B+ cmp edx, ecx .text:00401558 72+ jb short loc_40153F]]> Nhưđã nói, từdòng .text:0040152D tới .text:0040153C tìm chiều dài của chuỗi input. Nếu chiều dài đó khác 0 thì sẽthực hiện phần kế tiếp, còn không thì bỏqua. Kèm trong mã tìm chiều dài là việc khởi tạo eax và edx thành 0. Phần mã từ.text:0040153F đến cuối thực hiện việc chép tuần tựcác ký tựtừinput vào var_6C. Đổi tên var_6C thành copiedInput. Ta có thểbiểu diễn đoạn mã hợp ngữnày bằng mã C như sau. char copiedInput[60]; int counter; if (strlen(input) != 0) { for (counter = 0; counter < strlen(input); counter++) { copiedInput[counter] = input[counter]; } }]]> Chúng ta tiếp tục tìm hiểu phần kế. .text:0040155A loc_40155A: ; CODE XREF: check+FD]]>↑<![CDATA[j .text:0040155A 8D+ lea edi, [esp+88h+input] .text:0040155E 83+ or ecx, 0FFFFFFFFh .text:00401561 33+ xor eax, eax .text:00401563 33+ xor edx, edx .text:00401565 F2+ repne scasb .text:00401567 F7+ not ecx .text:00401569 49 dec ecx .text:0040156A 74+ jz short loc_401589 .text:0040156C .text:0040156C loc_40156C: ; CODE XREF: check+147]]>↓<![CDATA[j .text:0040156C 8A+ mov cl, [esp+edx+88h+input] .text:00401570 8D+ lea edi, [esp+88h+input] .text:00401574 02+ add cl, dl .text:00401576 33+ xor eax, eax .text:00401578 88+ mov [esp+edx+88h+input], cl .text:0040157C 83+ or ecx, 0FFFFFFFFh .text:0040157F 42 inc edx .text:00401580 F2+ repne scasb .text:00401582 F7+ not ecx .text:00401584 49 dec ecx .text:00401585 3B+ cmp edx, ecx .text:00401587 72+ jb short loc_40156C]]> Nếu độdài chuỗi input khác 0 thì lần lượt sửa từng ký tựtrong input với giá trịlà giá trịcũcộng thêm vịtrí của ký tựđó trong chuỗi. Ta thấy trong đoạn tìmđộdài chuỗi ởtrên có khởi tạo edx thành 0 và đoạn ởdưới có thêm một lệnh inc edx đểtăng edx. Đoạn mã C tương tựnhưsau. if (strlen(input) != 0) { for (counter = 0; counter < strlen(input); counter++) { input[counter] += counter; } }]]> Phần kếtiếp có vẻhơi phức tạp hơn một chút. .text:00401589 loc_401589: ; CODE XREF: check+12A]]>↑<![CDATA[j .text:00401589 8D+ lea edi, [esp+88h+input] .text:0040158D 83+ or ecx, 0FFFFFFFFh .text:00401590 33+ xor eax, eax .text:00401592 33+ xor edx, edx .text:00401594 F2+ repne scasb .text:00401596 F7+ not ecx .text:00401598 49 dec ecx .text:00401599 49 dec ecx .text:0040159A 74+ jz short loc_4015C5 .text:0040159C .text:0040159C loc_40159C: ; CODE XREF: check+183]]>↓<![CDATA[j .text:0040159C 8A+ mov al, [esp+edx+88h+input] .text:004015A0 8A+ mov cl, [esp+edx+0Dh] .text:004015A4 3A+ cmp al, cl .text:004015A6 7E+ jle short loc_4015B0 .text:004015A8 88+ mov [esp+edx+88h+input], cl .text:004015AC 88+ mov [esp+edx+0Dh], al .text:004015B0 .text:004015B0 loc_4015B0: ; CODE XREF: check+166]]>↑<![CDATA[j .text:004015B0 8D+ lea edi, [esp+88h+input] .text:004015B4 83+ or ecx, 0FFFFFFFFh .text:004015B7 33+ xor eax, eax .text:004015B9 42 inc edx .text:004015BA F2+ repne scasb .text:004015BC F7+ not ecx .text:004015BE 83+ add ecx, 0FFFFFFFEh .text:004015C1 3B+ cmp edx, ecx .text:004015C3 72+ jb short loc_40159C]]> Chú ý kỹđoạn mã loc_40159C, IDA đã không giúp chúng ta biết esp+edx+0Dh chỉtới giá trịnào. Nhưng IDA có thểcho chúng ta biết esp+edx+88h+input thực chất là gì. Đưa con trỏvào vùng esp+edx+88h+input, nhấn chuột phải và quan sát kỹta sẽthấy esp+edx+0Ch. Có nghĩa là đoạn mã này sẽlần lượt so sánh hai ký tự kếtiếp nhau trong input. Nếu ký tựđứng trước nhỏhơn ký tựđứng sau thì hoán đổi vịtrí của chúng. Chú ý kỹlà vòng lặp chỉlặp tới chiều dài của input trừmột. Chúng ta biết điều này vì tại .text:004015BE, thay vì dec ecx ta lại có add ecx, 0FFFFFFFFh (hay ecx = ecx - 2, hay ecx chứa chiều dài chuỗi input giảm một). Đoạn mã C tương tựnhư sau. if (strlen(input) != 0) { for (counter = 0; counter < strlen(input) - 1; counter++) { char a = tmp[counter]; char b = tmp[counter + 1]; if (a > b) { tmp[counter] = b; tmp[counter + 1] = a; } } } Phần cuối cùng của hàm này khá đơn giản. .text:004015C5 loc_4015C5: ; CODE XREF: check+15A]]>↑<![CDATA[j .text:004015C5 8D+ lea edi, [esp+88h+input] .text:004015C9 83+ or ecx, 0FFFFFFFFh .text:004015CC 33+ xor eax, eax .text:004015CE F2+ repne scasb .text:004015D0 F7+ not ecx .text:004015D2 49 dec ecx .text:004015D3 74+ jz short loc_40163E .text:004015D5 8D+ lea edx, [esp+88h+target] .text:004015D9 .text:004015D9 loc_4015D9: ; CODE XREF: check+1B6]]>↓<![CDATA[j .text:004015D9 0F+ movsx eax, [esp+esi+88h+input] .text:004015DE 3B+ cmp eax, [edx] .text:004015E0 75+ jnz short loc_4015F8 .text:004015E2 8D+ lea edi, [esp+88h+input] .text:004015E6 83+ or ecx, 0FFFFFFFFh .text:004015E9 33+ xor eax, eax .text:004015EB 46 inc esi .text:004015EC 83+ add edx, 4 .text:004015EF F2+ repne scasb .text:004015F1 F7+ not ecx .text:004015F3 49 dec ecx .text:004015F4 3B+ cmp esi, ecx .text:004015F6 72+ jb short loc_4015D9 .text:004015F8 .text:004015F8 loc_4015F8: ; CODE XREF: check+1A0]]>↑<![CDATA[j .text:004015F8 83+ cmp esi, 0Fh .text:004015FB 75+ jnz short loc_40163E]]> Đoạn mã này so sánh từng ký tựcủa chuỗi input với giá trịcó sẵn trong target. Giá trịcủa esi là vịtrí của sựkhác nhau đầu tiên. Giá trị của esi phải là 15 thì khóa nhập vào mới đúng. Mã C tương tựnhưsau. if (strlen(input) != 0) { for (counter = 0; counter < strlen(input); counter++) { if (input[counter] != target[counter]) { break; } } } if (counter == 0x0F) { // phần mã hiển thịhộp thoại thông báo copiedInput là khóa đúng // phần này không được xem xét trong bài viết } // PostQuitMessage]]> Nhưvậy ta đã có đầy đủthông tin đểviết lại hàm kiểm tra khóa nhập của crackme này. Việc tìm ra một khóa thỏa mã điều kiện của hàm kiểm tra này cũng khá đơn giản. Bài viết sẽkhông đềcập tới vấn đềtìm ra một khóa mà sẽtập trung vào việc tìm ra tất cảcác khóa trong phần tới. id="brute"> Với một hàm kiểm tra đơn giản nhưvậy ta có rất nhiều kết quảthỏa mã điều kiện. Tuy nhiên, tác giảcrackme này nhất định đòi người thử sức phải tìm ra khóa có ý nghĩa. Vì hàm kiểm tra quá đơn giản nênđể tìm ra tất cảmọi khóa ta chỉcòn cách vét cạn các hoán vịcủa target. Lưu ý rằng sinh ra tất cảhoán vịcủa một chuỗi 15 ký tựsẽsinh ra 15! (15 giai thừa) chuối mới. Ai cũng nhận ra 15! là một con sốrất lớn nên trong khi sinh ra hoán vị, chúng ta phải có cách đểloại bỏtất cảcác hoán vịmà ta biết là sẽkhông thểnào dẫn tới đáp án đúng. Trước khi bắt tay vào tìm kết quả, ta cần phải sửa hàm kiểm tra để tốiưu tốc độchạy nhưsau. static char target[] = "1G#X8t'X9svC`/y"; #define TARGET_LEN 15 int check(char *input, register int length) { char tmp[16]; register char a, b; register int counter; strncpy(tmp, input, length); length--; for (counter = 0; counter < length; counter++) { a = tmp[counter]; b = tmp[counter + 1]; if (a > b) { tmp[counter] = b; tmp[counter + 1] = a; } if (tmp[counter] != target[counter]) { break; } } if (counter == length && tmp[counter] == target[counter]) { counter++; } return counter; }]]> Hàm này kiểm tra chuỗi nhập vào với độdài length. Giá trịtrảvề là vịtrí sai biệt đầu tiên. Vì việc kiểm tra khá tốn thời gian so với việc sinh hoán vịnên hàm kiểm tra này đã được tối ưu dẫnđến việc mã hàm hơi khó hiểu. Ta cần hàm này vì trong quá trình sinh hoán vịta sẽ kiểm tra ngay lập tức và loại bỏcác hoán vịcon không cần thiết. Hàm sinh hoán vịkhá đơn giản. Nó là một hàm đệquy nhưsau. static int used[TARGET_LEN]; void printKey(char *dst) { int i = 0; for (i = 0; i < TARGET_LEN; i++) { printf("%c", (char) (dst[i] - i)); } printf("\n"); } void permute(register char *dst, register index) { register int i; if (index >= TARGET_LEN) { if (check(dst, TARGET_LEN) >= TARGET_LEN) { printKey(dst); } return; } for (i = 0; i < TARGET_LEN; i++) { if (!used[i]) { dst[index] = target[i]; used[i] = 1; if (check(dst, index + 1) >= index) // cắt nhánh { permute(dst, index + 1); } dst[index] = '\x00'; used[i] = 0; } } }]]> Điều đáng lưu ý ởhàm permute là dòng if đểcắt nhánh. Nếu ký tự mới thêm vào dst không qua được hàm kiểm tra nhưcác ký tựđã có trong dst thì bỏtoàn bộnhánh đó. Đểxửdụng hàm permute ta sẽkhai báo một mảng ký tựvà khởi tạo mảng used nhưsau. void main(void) { char tmp[16]; memset(used, 0, sizeof(used)); memset(tmp, 0, sizeof(tmp)); permute(tmp, 0); }]]> Đoạn mã trên sẽin lên màn hình tất cảcác giá trịthỏa hàm kiểm trađã nói tớiởdisasm. Đểbiên dịch các đoạn mã này, ngườiđọc cần thêm stdio.h, và string.h. Lời kết: Tác giảhy vọng bài viết đã hướng dẫn người đọc cách đọc và hiểu mã hợp ngữcùng IDA đểtừđó cấu trúc lại crackme đầu tiên của Merc và phương pháp vét cạn cắt nhánh tìm các khóa thỏa mãn crackme này.
Tài liệu liên quan