Kỹ thuật dịch ngược cho người mới bắt đầu - Mã hóa XOR - Windows x64
Cùng tìm hiểu về kỹ thuật dịch ngược (reverse engineering) và cách áp dụng nó để giải mã một chương trình được mã hóa bằng thuật toán XOR cho hệ điều hành Windows x64.
Mở đầu
Trong bài đăng trên blog trước, Bizfly Cloud nghiên cứu dịch ngược một tệp nhị phân và trích xuất mật khẩu từ bên trong nó. Tuy nhiên, tệp nhị phân này chứa một mật khẩu dạng plaintext. Đây là một dấu hiệu tốt cho người mới bắt đầu, nhưng bạn sẽ không thực sự tìm thấy các loại nhị phân như vậy trong thế giới ngày nay nữa. Trong cuộc sống thực, mật khẩu chủ yếu được obfuscated hoặc mã hóa. Hầu hết password thậm chí không được lưu trữ trong chính tệp nhị phân nữa mà được lưu trữ trên một số server từ xa, nơi tất cả quá trình xử lý và xác minh của chuỗi key được thực hiện. P.S: Đây là một reposting của blog riêng của tôi.
https://scriptdotsh.com/index.php/2018/05/09/ground-zero-part-2-2-reverse-engineering-xor-encryption-windows-x64/
Trong post này, tôi đã viết một mã C / C nhỏ (cả hai đều tương thích). Ở đây, chúng tôi sẽ không có bất kỳ mật khẩu plaintext nào được mã hóa cứng trong tệp nhị phân. Trên thực tế, mật khẩu chính được mã hóa với XOR. XOR là kiểu mã hóa cơ bản nhất được sử dụng từ nhiều năm trước nhưng vẫn được sử dụng mạnh mẽ bởi rất nhiều kẻ tấn công cho đến nay. Tuy nhiên, nó không được sử dụng để mã hóa thực tế mà thường được sử dụng thường xuyên hơn để làm xáo trộn văn bản được lưu trữ. Nếu bạn muốn biết XOR, AND, OR hoạt động ra sao, có thể xem tại đây: https://en.wikipedia.org/wiki/XOR_cipher
Dưới đây là mã nhị phân của chúng tôi mà bạn có thể tìm thấy tại đây https://github.com/paranoidninja/ScriptDotSh-Reverse-Engineering/tree/master/part_2-2. Bạn cũng có thể trực tiếp tải xuống và chạy Git repo có khả năng thực thi được.
#include <stdio.h>
#include <string.h>
void CheckPass(int *XoredPassword) {
int PArray[10] = {38, 34, 37, 55, 55, 61, 33, 51, 32, 39};
bool is_equal = true;
for (int i=0; i<10; i ) {
if (XoredPassword[i] != PArray[i]) {
is_equal = false;
break;
}
}
if (is_equal == true) {
printf ("[ ] Correct Password");
}
else {
printf ("[-] Incorrect Password");
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Help:\n%s <10 character serial key>\n", argv[0]);
}
else {
int stringLength = strlen(argv[1]);
if ((stringLength > 10) || (stringLength < 10)) {
printf("[-] Serial key must be of 10 characters. Please recheck your key\n");
}
else {
int XoredDecimal[10] = {};
int keyStore[10] = {85, 86, 87, 88, 89, 90, 81, 82, 83, 84};
for (int i=0; i<10; i ) {
XoredDecimal[i] = ((int)(argv[1][i]))^(keyStore[i]);
}
CheckPass(XoredDecimal);
}
}
return 0;
}
Chúng ta có thể bắt đầu bằng cách hiểu mã C / C trước. Nhưng có thể sẽ làm hỏng cuộc vui. Lý do là khi bẻ khóa các tệp nhị phân độc quyền, hầu như là chúng tôi sẽ không tìm thấy mã nguồn. Vì vậy, hãy cứ chạy nó bình thường và xem xem chúng ta có thể khai thác được những gì.
Như bạn có thể thấy nó in ra mật khẩu không chính xác. Vì vậy, hãy tải nó lên trong x64dbg và nghiên cứu dịch ngược nó. Hãy nhớ rằng mục tiêu của chúng ta là tìm ra password và không chỉ sửa đổi giá trị còn lại để in ra mật khẩu chính xác.
Tháo gỡ
Bạn có thể tải lên nhị phân trong x64dbg bằng cách chuyển đối số và đường dẫn tuyệt đối của tệp nhị phân trong powershell hoặc cmd như sau:
PS C:\Users\Paranoid Ninja\Desktop> & '.\x64dbg - Shortcut.lnk' 'C:\Users\Paranoid Ninja\Desktop\crackMe_xor.exe' password12
Sau khi được tải, một điều cần nhớ là không giống như linux, các cửa sổ không trực tiếp đưa chúng ta đến mã chưa được phân tách của mã nhị phân trong trình gỡ lỗi. Đầu tiên sẽ tải lên các cửa sổ DLLs cần thiết và sau đó nó đòi hỏi chạy cùng nhị phân. Bạn có thể điều hướng đến tab Log trong trình gỡ lỗi và xem các tệp DLL được tải tại đây.
Như bạn có thể thấy ở trên, ntdll.dll, kernel32.dll, KernelBase.dll và msvcrt.dll đang được gọi ở đây. Lý do tại sao tôi thông báo về các tệp DLL này là vì chúng tôi không cần phải chạm vào chúng như bây giờ. Và nếu chúng ta chỉ làm stepi, nó sẽ đưa chúng ta qua từng bước của các DLL và vào bên trong chúng theo cách chúng ta không muốn như hiện tại. Chúng tôi sẽ chỉ được dịch ngược kỹ thuật nhị phân trong hôm nay. Vì vậy, hãy xem Chuỗi tham chiếu như chúng tôi đã làm lần trước và tìm ra điểm mà chúng tôi có thể thiết lập điểm ngắt cho nhị phân này.
Chuỗi tham chiếu sẽ trông giống như thế này. Chúng ta sẽ thực hiện theo text 'Serial key must be ...' trong trình phân dịch và xem mã tháo gỡ ở đó.
Bây giờ nếu bạn nhìn thấy hình ảnh trên, tôi sẽ thiết lập một điểm ngắt tại 0000000000401681. Hãy để tôi cho bạn biết lý do tại sao chúng tôi đã chọn chuỗi tham chiếu này và thiết lập một điểm ngắt ở đây. Khi chúng tôi chạy đoạn code như bên trên trước đó, chúng tôi thấy rằng nó đang thực hiện một vài kiểm tra có điều kiện. Ví dụ: Kiểm tra điều kiện đầu tiên là nếu chúng ta không nhập một đối số, nó sẽ in ra câu lệnh trợ giúp. Kiểm tra thứ hai là nếu bạn nhập một key nhỏ hơn hoặc lớn hơn 10, nó sẽ báo lỗi để nhập một key chính xác 10 ký tự. Điều này đơn giản có nghĩa là một khi chúng ta đã cung cấp đối số đúng thì nó sẽ tiến hành kiểm tra mật khẩu và đó là lý do tại sao chúng tôi đã chọn chiễu tham chiếu này.
Và nếu bạn nhìn vào địa chỉ 0000000000401668 hoặc địa chỉ 000000000040166E thì sẽ thấy cả hai đều thực hiện kiểm tra có điều kiện và sau đó chuyển đến 0000000000401681. Nếu một trong hai điều kiện không thành công, nó sẽ in lỗi key hàng loạt và out code. Bây giờ hãy chạy nhị phân để chứng minh rằng giả thuyết của chúng ta là chính xác.
Khi bạn chạy tệp nhị phân, bạn sẽ thấy từ 00000000004016A9 đến 00000000004016EF, nó bắt đầu tải thứ gì đó vào DWORD Pointers ở các vị trí khác nhau trong thanh ghi RBP.
Ngay bây giờ chúng ta đang không có bất kỳ đầu mối nào, do đó chúng tôi sẽ chỉ ghi chép lại và lưu trữ nó trong trường hợp chúng tôi cần nó trong tương lai. 10 ký tự là U, V, W, X, Y, Z, Q, R, S, T. Ngoài ra hãy nhớ rằng các số bạn nhìn thấy bên trái của các ký tự này là giá trị HEX của chính các ký tự đó. Ví dụ: nếu bạn chuyển đổi 55 thành thập phân, bạn nhận được-
55 = 5 (16 ^ 1) 5 (16 ^ 0)
= 80 5 = 85
Và tất cả chúng ta đều biết rằng 85 là giá trị ascii của ký tự U. Tương tự, nếu chúng ta chuyển đổi tất cả các giá trị thành ascii, chúng ta sẽ nhận được
Char | U | V | W | X | Y | Z | Q | R | S | T |
Hex | 55 | 56 | 57 | 58 | 59 | 5A | 51 | 52 | 53 | 54 |
ASCII | 85 | 86 | 87 | 88 | 89 | 90 | 81 | 82 | 83 | 84 |
Bây giờ hãy giữ nó sang một bên. Hãy nhớ mật khẩu của chúng tôi cũng có giá trị 10 ký tự và tập hợp này cũng có cùng độ dài. Cũng nên ghi nhớ rằng trong XOR, giá trị xor'd bằng 0, là giá trị của chính nó. Đây là lý do tại sao chuỗi được mã hóa và các giá trị key cần phải có cùng độ dài khi xoring.
Cùng quay lại code của chúng tôi bằng cách thực hiện một vài stepi, bây giờ chúng tôi đã đạt đến mức 00000000004016FA. Bạn cũng có thể thấy chúng tôi có một điều kiện khác ghi jg crackme_xor. 40172F. Nếu bạn nhìn vào thanh ghi RBX, số được tải lên là số 10.
jg đồng nghĩa với nhảy nếu thông số lớn hơn. Vì vậy, mã của chúng tôi ở đây là vòng lặp đơn giản và sẽ tiếp tục lặp lại cho đến 000000000040172D và khi giá trị tăng lên 10. Tại 000000000040172D, bạn cũng có thể thấy nó nhảy trở lại 00000000004016F6, trong đó nó so sánh giá trị với cmp dword ptr. Nếu nó lớn hơn 10, nó sẽ ngay lập tức nhảy đến 000000000040172F khác và sẽ tiếp tục lặp lại chính nó cho đến khi nhận được giá trị 10.
Bây giờ, nếu bạn tiếp tục bước chương trình với stepi, bạn có thể thấy rằng tại 0000000000401704, nó tải mật khẩu đã nhập của chúng tôi vào thanh ghi RAX và sau đó register EAX tại 000000000040170F.
Tại 0000000000401712, giá trị tại thanh ghi AL tức là 'p' đang được chuyển sang EDX, hãy đăng ký ngay. Một giá trị khác đang được chuyển đến thanh ghi EAX tại 0000000000401715. Và cả hai giá trị này đều là XOR'd tại 000000000040171E.
Bây giờ, nếu bạn lướt qua các hướng dẫn với stepi, bạn sẽ thấy rằng giá trị 'p' đã được tải lên trong thanh ghi RDX nhưng chữ cái đầu tiên của mật khẩu 'password12' và một giá trị khác được tải lên trong thanh ghi EAX cho xoring là chữ hoa 'U'.
Bây giờ, hãy xem lại toàn bộ vòng lặp từng bước một. Ngay bây giờ chúng tôi đang ở trong một bộ đếm gia số One. Hãy nhớ rằng bộ đếm này sẽ chạy 10 lần tương đương với 9 giá trị khác cũng sẽ là xor'd. Vì vậy, hãy xem những gì đã được tải lên ở vòng lặp tiếp theo.
Như bạn có thể thấy, nó đã tải 'a' từ mật khẩu 'password12' của chúng tôi vào RDX và một giá trị 'V' khác sẽ là xor'd với 'a'. Tương tự, nếu bạn chạy qua toàn bộ vòng lặp, bạn sẽ thấy rằng mỗi chữ cái của chúng tôi đang nhận xor'd theo cách dưới đây cho đến khi bộ đếm gia số đạt đến 10 tại 00000000004016FA. Vì vậy, tóm lại là những giá trị mà đang nhận xor'd với mật khẩu của chúng tôi là con số không, nhưng trước đó các key đã được tải lên đó là U, V, W, X, Y, Z, Q, R, S, T. Bây giờ, nếu chúng tôi XOR mật khẩu của chúng tôi với key ở trên, chúng tôi nhận được:
Xoring cả hai số nhị phân ở trên, chúng tôi nhận được nhị phân xor'd dưới đây:
Hãy nhớ rằng đây chỉ là key chứ không phải là mật khẩu chính. Để tìm ra mật khẩu, chúng ta cần hai giá trị. Ví dụ: nếu X xor Y = Z, thì Y xor Z sẽ là X. Vì vậy, chúng ta sẽ cần hai bộ giá trị tức là X và Y hoặc Y và Z hoặc X và Z. Và ngay bây giờ chúng ta chỉ có tập hợp key là [U, V, W, X, Y, Z, Q, R, S, T], có nghĩa là chúng tôi vẫn cần phải tìm tập hợp khác để tìm ra mật khẩu thực. Hãy tiếp tục với mã đã được tháo rời và xem bước tiếp theo sẽ ở đâu. Tại 0000000000401736, chúng ta có thể thấy nó đang gọi một số chức năng khác. Hãy nhấp vào chức năng đó và quan sát nó hoạt động.
Oh, thật tuyệt. Chúng ta có thể thấy rằng tại 000000000040156C, một tập hợp các ký tự khác đang được tải vào thanh ghi. Hãy chuyển đổi mỗi giá trị hex thành số thập phân và đây là những gì chúng tôi nhận được:
Bây giờ, hãy giữ nó sang một bên và tiếp tục với mã đang chờ xử lý. Tại 00000000004015BD, một lần nữa cmp được sử dụng để so sánh giá trị và thanh ghi RBX giữ giá trị 10. Sau đó, jg lại tiếp tục nhảy tới 00000000004015F2, nếu bộ đếm lớn hơn 10. Bây giờ, nếu chúng ta chỉ cần bước vào địa chỉ 00000000004015E2, nó sẽ so sánh giá trị '&' và giá trị '%'. Chúng tôi biết rằng '&' không là gì ngoài giá trị mà chúng tôi đã tìm thấy ở trên với mã Hex 26 và giá trị ASCII 38. Nhưng giá trị '%' là gì và tại sao giá trị đó được so sánh với '&'?
Nếu bạn chuyển đổi % thành ASCII, chúng ta sẽ nhận được 37 là đối số tương ứng thập phân của nó. Và nếu bạn scroll back up trở lại, hãy nhớ rằng '%' là giá trị xor'd của 'p' và 'U'. Bây giờ, nếu chúng ta tiếp tục vòng lặp thông qua mã được tháo rời này, bạn sẽ thấy rằng nó không tiến hành so sánh giá trị tiếp theo. Lý do chính là mã tại địa chỉ 00000000004015E2. Ở đây EDX (%) được so sánh với EAX (&), và nếu chúng không bằng nhau, Carry flag (CF) được đặt thành 1. Lệnh tiếp theo tại 00000000004015E4, đang kiểm tra xem lệnh trước đó có giá trị bằng không hay không. je, sẽ nhảy nếu giá trị bằng 0, nhưng nó sẽ không nhảy và sẽ tiến hành bước tiếp theo vì chúng tôi đã đặt giá trị là One vì cả hai đều không bằng nhau. Tại 00000000004015F6, sau đó nó sẽ tiến hành in 'Mật khẩu sai' và thoát khỏi vòng lặp và chương trình.
Vì vậy, chúng tôi vẫn chưa khôi phục mật khẩu cho chương trình của chúng tôi. NHƯNG, chúng tôi có những gì chúng tôi cần. Chúng ta có các key [U, V, W, X, Y, Z, Q, R, S, T] và giá trị được so sánh là [26, 22, 25, 37, 37, 3D, 21, 33, 20, 27]. Hãy nhớ rằng, giá trị được so sánh không phải là mật khẩu. Đó là giá trị xor'd của mật khẩu thực và key. Vì vậy, chúng ta cần phải xor giá trị so sánh với key một lần nữa để có được mật khẩu thực.
Bây giờ, hãy chuyển đổi cả hai thành thập phân và XOR chúng và hãy xem những gì chúng tôi nhận được:
Và bây giờ, nếu chúng tôi chuyển đổi giá trị thập phân xor'd cuối cùng thành text, chúng tôi nhận được:
Vì vậy, về cơ bản password nên là những strongpass, nếu giả thuyết của chúng tôi là chính xác. Hãy chèn mã đó vào tệp nhị phân của chúng tôi và kiểm tra xem chúng tôi có chính xác không:
Link bài gốc: http://niiconsulting.com/checkmate/2018/05/reverse-engineering-for-beginners-xor-encryption-windows-x64/
Theo Bizfly Cloud chia sẻ
>> Có thể bạn quan tâm: Tất tật bạn phải biết về "Giải mã mật khẩu"