Tại sao mã máy gốc không thể dễ dàng dịch ngược?


16

Với các ngôn ngữ máy ảo dựa trên mã byte như Java, VB.NET, C #, ActionScript 3.0, v.v., đôi khi bạn nghe thấy việc dễ dàng tải xuống một số trình dịch ngược từ Internet, chạy mã byte qua nó một lần thông thường, đến với một cái gì đó không quá xa mã nguồn ban đầu trong vài giây. Giả sử loại ngôn ngữ này đặc biệt dễ bị tổn thương.

Gần đây tôi đã bắt đầu tự hỏi tại sao bạn không nghe nhiều hơn về điều này liên quan đến mã nhị phân gốc, khi bạn ít nhất biết ngôn ngữ đó được viết bằng ngôn ngữ nào (và do đó, ngôn ngữ nào sẽ cố dịch ngược). Trong một thời gian dài, tôi đã hình dung ra điều đó chỉ vì ngôn ngữ máy bản địa quá điên rồ và phức tạp hơn so với mã byte thông thường.

Nhưng mã byte trông như thế nào? Nó trông như thế này:

1000: 2A 40 F0 14
1001: 2A 50 F1 27
1002: 4F 00 F0 F1
1003: C9 00 00 F2

Và mã máy gốc trông như thế nào (trong hex)? Nó, tất nhiên, trông như thế này:

1000: 2A 40 F0 14
1001: 2A 50 F1 27
1002: 4F 00 F0 F1
1003: C9 00 00 F2

Và các hướng dẫn đến từ một khung tâm trí hơi giống nhau:

1000: mov EAX, 20
1001: mov EBX, loc1
1002: mul EAX, EBX
1003: push ECX

Vì vậy, đưa ra ngôn ngữ để cố gắng dịch ngược một số nhị phân nguyên gốc thành C ++, có gì khó khăn về nó? Hai ý tưởng duy nhất nảy ra trong đầu là 1) nó thực sự phức tạp hơn nhiều so với mã byte, hoặc 2) một điều gì đó về thực tế là các hệ điều hành có xu hướng phân trang chương trình và phân tán các phần của chúng gây ra quá nhiều vấn đề. Nếu một trong những khả năng đó là chính xác, xin vui lòng giải thích. Nhưng dù bằng cách nào, tại sao bạn không bao giờ nghe về điều này về cơ bản?

GHI CHÚ

Tôi sắp chấp nhận một trong những câu trả lời, nhưng tôi muốn đề cập đến điều gì đó trước tiên. Hầu hết mọi người đều đề cập đến thực tế là các đoạn mã nguồn gốc khác nhau có thể ánh xạ tới cùng một mã máy; Tên biến cục bộ bị mất, bạn không biết loại vòng lặp ban đầu được sử dụng, v.v.

Tuy nhiên, những ví dụ như hai thứ vừa được nhắc đến là một thứ tầm thường trong mắt tôi. Một số câu trả lời mặc dù có xu hướng nói rằng sự khác biệt giữa mã máy và nguồn ban đầu mạnh hơn nhiều so với thứ tầm thường này.

Nhưng ví dụ, khi nói đến những thứ như tên biến cục bộ và loại vòng lặp, mã byte cũng mất thông tin này (ít nhất là đối với ActionScript 3.0). Tôi đã kéo những thứ đó trở lại thông qua một trình dịch ngược trước đó và tôi không thực sự quan tâm liệu một biến được gọi là strMyLocalString:Stringhay loc1. Tôi vẫn có thể nhìn vào phạm vi nhỏ, cục bộ đó và xem nó được sử dụng như thế nào mà không gặp nhiều rắc rối. Và một forvòng lặp là khá chính xác như mộtwhilevòng lặp, nếu bạn nghĩ về nó. Ngoài ra, ngay cả khi tôi sẽ chạy nguồn thông qua irFuscator (không giống như safeSWF, không làm gì nhiều ngoài việc chỉ ngẫu nhiên hóa tên biến và tên hàm thành viên), có vẻ như bạn chỉ có thể bắt đầu cô lập một số biến và hàm nhất định trong các lớp nhỏ hơn, hình tìm hiểu cách họ được sử dụng, gán tên của riêng bạn cho họ và làm việc từ đó.

Để điều này trở thành một vấn đề lớn, mã máy sẽ cần phải mất nhiều thông tin hơn thế, và một số câu trả lời đi sâu vào vấn đề này.


35
Thật khó để làm cho một con bò ra khỏi hamburger.
Kaz Dragon

4
Vấn đề chính là một nhị phân nguyên gốc giữ lại rất ít siêu dữ liệu về chương trình. Nó không có thông tin về các lớp (làm cho C ++ đặc biệt khó dịch ngược) và thậm chí không phải lúc nào cũng có chức năng - không cần thiết vì CPU vốn thực thi mã theo kiểu khá tuyến tính, mỗi lần một lệnh. Ngoài ra, không thể phân biệt giữa mã và dữ liệu ( liên kết ). Để biết thêm thông tin, bạn có thể muốn xem xét việc tìm kiếm hoặc tái hỏi tại RE.SE .
ntoskrnl

Câu trả lời:


39

Tại mỗi bước biên dịch, bạn sẽ mất thông tin không thể phục hồi. Bạn càng mất nhiều thông tin từ nguồn ban đầu, thì càng khó dịch ngược.

Bạn có thể tạo một trình biên dịch khử hữu ích cho mã byte bởi vì nhiều thông tin được lưu giữ từ nguồn ban đầu hơn là được bảo tồn khi tạo mã máy đích cuối cùng.

Bước đầu tiên của trình biên dịch là biến nguồn thành một số để biểu diễn trung gian thường được biểu diễn dưới dạng cây. Theo truyền thống, cây này không chứa thông tin phi ngữ nghĩa như bình luận, khoảng trắng, v.v ... Một khi điều này bị vứt đi, bạn không thể khôi phục nguồn gốc từ cây đó.

Bước tiếp theo là biến cây thành một dạng ngôn ngữ trung gian giúp tối ưu hóa dễ dàng hơn. Có khá nhiều lựa chọn ở đây và mỗi cơ sở hạ tầng trình biên dịch đều có riêng. Tuy nhiên, thông thường, thông tin như tên biến cục bộ, cấu trúc luồng điều khiển lớn (chẳng hạn như bạn đã sử dụng vòng lặp for hay while) sẽ bị mất. Một số tối ưu hóa quan trọng thường xảy ra ở đây, lan truyền liên tục, chuyển động mã bất biến, nội tuyến hàm, v.v ... Mỗi biến đổi biểu diễn thành biểu diễn có chức năng tương đương nhưng trông khác nhau đáng kể.

Một bước sau đó là tạo ra các hướng dẫn máy thực tế có thể liên quan đến cái được gọi là tối ưu hóa "lỗ nhìn trộm" tạo ra phiên bản tối ưu hóa của các mẫu hướng dẫn phổ biến.

Ở mỗi bước bạn mất ngày càng nhiều thông tin cho đến khi, cuối cùng, bạn mất rất nhiều, không thể phục hồi bất cứ thứ gì giống với mã gốc.

Mặt khác, mã byte, thường lưu các tối ưu hóa biến đổi và thú vị cho đến khi pha JIT (trình biên dịch đúng lúc) khi mã máy đích được tạo ra. Mã byte chứa rất nhiều dữ liệu meta như các loại biến cục bộ, cấu trúc lớp, để cho phép cùng một mã byte được biên dịch thành nhiều mã máy đích. Tất cả thông tin này là không cần thiết trong một chương trình C ++ và bị loại bỏ trong quá trình biên dịch.

Có các trình dịch ngược cho các mã máy đích khác nhau nhưng chúng thường không tạo ra kết quả hữu ích (thứ bạn có thể sửa đổi và sau đó biên dịch lại) vì quá nhiều nguồn gốc bị mất. Nếu bạn có thông tin gỡ lỗi cho tệp thực thi, bạn có thể thực hiện công việc thậm chí còn tốt hơn; nhưng, nếu bạn có thông tin gỡ lỗi, có lẽ bạn cũng có nguồn gốc.


5
Thực tế là thông tin được lưu giữ để JIT có thể hoạt động tốt hơn là chìa khóa.
btilly

Có phải C ++ DLL dễ dàng phân tách được không?
Panzercrisis

1
Không vào bất cứ điều gì tôi sẽ xem xét hữu ích.
chuckj

1
Siêu dữ liệu không phải là "để cho phép cùng một mã byte được biên dịch thành nhiều mục tiêu", nó ở đó để phản ánh. Biểu diễn trung gian có thể nhắm mục tiêu lại không cần phải có bất kỳ siêu dữ liệu nào.
SK-logic

2
Điều đó không đúng. Phần lớn dữ liệu là có để phản ánh nhưng sự phản chiếu không phải là cách sử dụng duy nhất. Ví dụ: các định nghĩa giao diện và lớp được sử dụng để tạo phần bù trường xác định, xây dựng các bảng ảo, v.v. trên máy đích cho phép chúng được xây dựng theo cách hiệu quả nhất cho máy đích. Các bảng này được xây dựng bởi trình biên dịch và / hoặc trình liên kết khi tạo mã gốc. Một khi điều này được thực hiện, dữ liệu được sử dụng để xây dựng chúng sẽ bị loại bỏ.
chuckj

11

Việc mất thông tin như được chỉ ra bởi các câu trả lời khác là một điểm, nhưng nó không phải là người giải quyết. Rốt cuộc, bạn không mong đợi chương trình gốc trở lại, bạn chỉ muốn bất kỳ đại diện nào bằng ngôn ngữ cấp cao. Nếu mã được nội tuyến, bạn chỉ có thể để nó hoặc tự động tính ra các tính toán phổ biến. Về nguyên tắc, bạn có thể hoàn tác nhiều tối ưu hóa. Nhưng có một số hoạt động về nguyên tắc không thể đảo ngược (ít nhất là không có số lượng máy tính vô hạn).

Ví dụ, các nhánh có thể trở thành bước nhảy được tính toán. Mã như thế này:

select (x) {
case 1:
    // foo
    break;
case 2:
    // bar
    break;
}

có thể được biên dịch thành (xin lỗi vì đây không phải là trình biên dịch thực):

0x1000:   jump to 0x1000 + 4*x
0x1004:   // foo
0x1008:   // bar
0x1012:   // qux

Bây giờ, nếu bạn biết rằng x có thể là 1 hoặc 2, bạn có thể nhìn vào các bước nhảy và đảo ngược điều này một cách dễ dàng. Nhưng còn địa chỉ 0x1012 thì sao? Bạn có nên tạo một case 3cho nó, quá? Bạn sẽ phải theo dõi toàn bộ chương trình trong trường hợp xấu nhất để tìm ra giá trị nào được phép. Thậm chí tệ hơn, bạn có thể phải xem xét tất cả các đầu vào người dùng có thể! Cốt lõi của vấn đề là bạn không thể phân biệt dữ liệu và hướng dẫn.

Điều đó đang được nói, tôi sẽ không hoàn toàn bi quan. Như bạn có thể nhận thấy trong 'trình biên dịch' ở trên, nếu x đến từ bên ngoài và không được bảo đảm là 1 hoặc 2, về cơ bản bạn có một lỗi xấu cho phép bạn nhảy đến bất cứ đâu. Nhưng nếu chương trình của bạn không có loại lỗi này, thì lý do sẽ dễ dàng hơn nhiều. (Không phải ngẫu nhiên mà các ngôn ngữ trung gian "an toàn" như CLR IL hoặc mã byte Java dễ dịch ngược hơn, thậm chí đặt siêu dữ liệu sang một bên.) Vì vậy, trong thực tế, có thể dịch ngược một số hành vi tốtcác chương trình. Tôi đang nghĩ về các thói quen phong cách chức năng cá nhân, không có tác dụng phụ và đầu vào được xác định rõ. Tôi nghĩ rằng có một vài trình dịch ngược xung quanh có thể cung cấp mã giả cho các chức năng đơn giản, nhưng tôi không có nhiều kinh nghiệm với các công cụ như vậy.


9

Lý do tại sao mã máy không thể dễ dàng chuyển đổi trở lại mã nguồn ban đầu là rất nhiều thông tin bị mất trong quá trình biên dịch. Các phương thức và các lớp không xuất có thể được nội tuyến, tên biến cục bộ bị mất, tên tệp và cấu trúc bị mất hoàn toàn, trình biên dịch có thể thực hiện tối ưu hóa không rõ ràng. Một lý do khác là nhiều tệp nguồn khác nhau có thể tạo ra cùng một tổ hợp.

Ví dụ:

int DoSomething()
{
    return Add(5, 2);
}

int Add(int x, int y)
{
    return x + y;
}

int main()
{
    return DoSomething();
}

Có thể được biên dịch thành:

main:
mov eax, 7;
ret;

Việc lắp ráp của tôi khá hoen gỉ, nhưng nếu trình biên dịch có thể xác minh rằng việc tối ưu hóa có thể được thực hiện chính xác, nó sẽ làm như vậy. Điều này là do nhị phân được biên dịch không cần biết tên DoSomethingAddcũng như thực tế là Addphương thức có hai tham số được đặt tên, trình biên dịch cũng biết rằng về DoSomethingcơ bản phương thức trả về một hằng và nó có thể nội tuyến cả cuộc gọi phương thức và phương pháp chính nó.

Mục đích của trình biên dịch là tạo ra một hội đồng, không phải là một cách để bó các tệp nguồn.


Cân nhắc thay đổi hướng dẫn cuối cùng thành chỉ retvà nói rằng bạn đang giả sử quy ước gọi C.
chuckj

3

Các nguyên tắc chung ở đây là ánh xạ nhiều-một và thiếu các đại diện kinh điển.

Đối với một ví dụ đơn giản về hiện tượng nhiều-một, bạn có thể nghĩ về những gì xảy ra khi bạn thực hiện một hàm với một số biến cục bộ và biên dịch nó thành mã máy. Tất cả thông tin về các biến bị mất vì chúng chỉ trở thành địa chỉ bộ nhớ. Một cái gì đó tương tự xảy ra cho các vòng lặp. Bạn có thể lấy một forhoặcwhile vòng lặp và nếu chúng được cấu trúc vừa phải thì bạn có thể nhận được mã máy giống hệt với jumphướng dẫn.

Điều này cũng dẫn đến việc thiếu các đại diện chính tắc từ mã nguồn gốc cho các hướng dẫn mã máy. Khi bạn cố gắng dịch ngược các vòng lặp, làm thế nào để bạn ánh xạ các jumphướng dẫn trở lại các cấu trúc lặp? Bạn có làm cho chúng forvòng lặp hoặc whilevòng lặp.

Vấn đề càng thêm bực tức bởi thực tế là các trình biên dịch hiện đại thực hiện các hình thức gấp và nội tuyến khác nhau. Vì vậy, vào thời điểm bạn nhận được mã máy, gần như không thể biết được mức độ cao nào xây dựng mã máy cấp thấp đến từ đâu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.