it-swarm-vi.com

ASLR và DEP hoạt động như thế nào?

Làm thế nào để ngẫu nhiên bố trí không gian địa chỉ (ASLR) và ngăn chặn thực thi dữ liệu (DEP) hoạt động, về mặt ngăn chặn các lỗ hổng bị khai thác? Họ có thể được bỏ qua?

115
Polynomial

Ngẫu nhiên bố trí không gian địa chỉ (ASLR) là một công nghệ được sử dụng để giúp ngăn chặn shellcode thành công. Nó thực hiện điều này bằng cách bù đắp ngẫu nhiên vị trí của các mô-đun và các cấu trúc trong bộ nhớ nhất định. Ngăn chặn thực thi dữ liệu (DEP) ngăn một số lĩnh vực bộ nhớ nhất định, ví dụ: ngăn xếp, từ được thực hiện. Khi được kết hợp, việc khai thác lỗ hổng trong các ứng dụng sử dụng kỹ thuật shellcode hoặc lập trình hướng trở lại (ROP) trở nên cực kỳ khó khăn.

Đầu tiên, chúng ta hãy xem làm thế nào một lỗ hổng thông thường có thể được khai thác. Chúng tôi sẽ bỏ qua tất cả các chi tiết, nhưng hãy nói rằng chúng tôi đang sử dụng lỗ hổng tràn bộ đệm ngăn xếp. Chúng tôi đã tải một lượng lớn các giá trị 0x41414141 Vào tải trọng của chúng tôi và eip đã được đặt thành 0x41414141, Vì vậy chúng tôi biết rằng nó có thể khai thác được. Sau đó, chúng tôi đã sử dụng một công cụ thích hợp (ví dụ: pattern_create.rb) Của Metasploit để khám phá phần bù của giá trị được tải vào eip. Đây là phần bù bắt đầu của mã khai thác của chúng tôi. Để xác minh, chúng tôi tải 0x41 Trước phần bù này, 0x42424242 Ở phần bù và 0x43 Sau phần bù.

Trong quy trình không phải ASLR và không phải DEP, địa chỉ ngăn xếp giống nhau mỗi khi chúng tôi chạy quy trình. Chúng tôi biết chính xác nó ở đâu trong bộ nhớ. Vì vậy, hãy xem ngăn xếp trông như thế nào với dữ liệu thử nghiệm mà chúng tôi đã mô tả ở trên:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 42424242   > esp points here
 000ff6b4  | 43434343
 000ff6b8  | 43434343

Như chúng ta có thể thấy, esp trỏ đến 000ff6b0, Đã được đặt thành 0x42424242. Các giá trị trước đó là 0x41 Và các giá trị sau là 0x43, Như chúng tôi đã nói. Bây giờ chúng tôi biết rằng địa chỉ được lưu trữ tại 000ff6b0 Sẽ được chuyển đến. Vì vậy, chúng tôi đặt nó thành địa chỉ của một số bộ nhớ mà chúng tôi có thể kiểm soát:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Chúng tôi đã đặt giá trị tại 000ff6b0 Sao cho eip sẽ được đặt thành 000ff6b4 - phần bù tiếp theo trong ngăn xếp. Điều này sẽ khiến 0xcc Được thực thi, đó là một lệnh int3. Vì int3 Là điểm dừng ngắt phần mềm, nên nó sẽ đưa ra một ngoại lệ và trình gỡ lỗi sẽ tạm dừng. Điều này cho phép chúng tôi xác minh rằng việc khai thác đã thành công.

> Break instruction exception - code 80000003 (first chance)
[snip]
eip=000ff6b4

Bây giờ chúng tôi có thể thay thế bộ nhớ tại 000ff6b4 Bằng shellcode, bằng cách thay đổi tải trọng của chúng tôi. Điều này kết luận khai thác của chúng tôi.

Để ngăn chặn việc khai thác này thành công, Ngăn chặn thực thi dữ liệu đã được phát triển. DEP buộc các cấu trúc nhất định, bao gồm cả ngăn xếp, được đánh dấu là không thể thực thi. Điều này được làm mạnh hơn nhờ hỗ trợ CPU với bit No-Execute (NX), còn được gọi là bit XD, bit EVP hoặc bit XN, cho phép CPU thực thi quyền thực thi ở cấp phần cứng. DEP được giới thiệu trong Linux vào năm 2004 (kernel 2.6.8) và Microsoft đã giới thiệu nó vào năm 2004 như một phần của WinXP SP2. Apple đã thêm hỗ trợ DEP khi họ chuyển sang kiến ​​trúc x86 vào năm 2006. Với tính năng DEP được bật, khai thác trước đó của chúng tôi sẽ không hoạt động:

> Access violation - code c0000005 (!!! second chance !!!)
[snip]
eip=000ff6b4

Điều này không thành công vì ngăn xếp được đánh dấu là không thể thực thi và chúng tôi đã cố gắng thực hiện nó. Để giải quyết vấn đề này, một kỹ thuật gọi là Lập trình hướng trở lại (ROP) đã được phát triển. Điều này liên quan đến việc tìm kiếm các đoạn mã nhỏ, được gọi là các tiện ích ROP, trong các mô-đun hợp pháp trong quy trình. Các tiện ích này bao gồm một hoặc nhiều hướng dẫn, theo sau là trả lại. Xâu chuỗi chúng cùng với các giá trị phù hợp trong ngăn xếp cho phép mã được thực thi.

Đầu tiên, chúng ta hãy nhìn vào ngăn xếp của chúng ta trông như thế nào ngay bây giờ:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Chúng tôi biết rằng chúng tôi không thể thực thi mã tại 000ff6b4, Vì vậy chúng tôi phải tìm một số mã hợp pháp mà chúng tôi có thể sử dụng thay thế. Hãy tưởng tượng rằng nhiệm vụ đầu tiên của chúng ta là lấy một giá trị vào thanh ghi eax. Chúng tôi tìm kiếm kết hợp pop eax; ret Ở đâu đó trong bất kỳ mô-đun nào trong quy trình. Khi chúng tôi đã tìm thấy một cái, giả sử tại 00401f60, Chúng tôi đặt địa chỉ của nó vào ngăn xếp:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 00401f60
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Khi shellcode này được thực thi, chúng ta sẽ lại vi phạm quyền truy cập:

> Access violation - code c0000005 (!!! second chance !!!)
eax=cccccccc ebx=01020304 ecx=7abcdef0 edx=00000000 esi=7777f000 edi=0000f0f1
eip=43434343 esp=000ff6ba ebp=000ff6ff

CPU đã thực hiện như sau:

  • Chuyển đến hướng dẫn pop eax Tại 00401f60.
  • Đã bật cccccccc khỏi ngăn xếp, vào eax.
  • Đã thực hiện ret, xuất hiện 43434343 Vào eip.
  • Đã vi phạm quyền truy cập vì 43434343 Không phải là địa chỉ bộ nhớ hợp lệ.

Bây giờ, hãy tưởng tượng rằng, thay vì 43434343, Giá trị tại 000ff6b8 Đã được đặt thành địa chỉ của tiện ích ROP khác. Điều này có nghĩa là pop eax Được thực thi, sau đó là tiện ích tiếp theo của chúng tôi. Chúng ta có thể xâu chuỗi các tiện ích với nhau như thế này. Mục tiêu cuối cùng của chúng tôi thường là tìm địa chỉ của API bảo vệ bộ nhớ, chẳng hạn như VirtualProtect và đánh dấu ngăn xếp là có thể thực thi được. Sau đó, chúng tôi sẽ bao gồm một tiện ích ROP cuối cùng để thực hiện một lệnh tương đương jmp esp Và thực thi shellcode. Chúng tôi đã bỏ qua thành công DEP!

Để chống lại những mánh khóe này, ASLR đã được phát triển. ASLR liên quan đến việc bù đắp ngẫu nhiên các cấu trúc bộ nhớ và địa chỉ cơ sở mô-đun để làm cho việc đoán vị trí của các tiện ích ROP và API rất khó khăn.

Trên Windows Vista và 7, ASLR ngẫu nhiên vị trí của các tệp thực thi và DLL trong bộ nhớ, cũng như ngăn xếp và đống. Khi một tệp thực thi được tải vào bộ nhớ, Windows sẽ lấy bộ đếm dấu thời gian của bộ xử lý (TSC), dịch chuyển nó theo bốn vị trí, thực hiện phân chia mod 254, sau đó thêm 1. Số này sau đó được nhân với 64KB và hình ảnh thực thi được tải ở phần bù này . Điều này có nghĩa là có 256 vị trí có thể thực hiện được. Vì các DLL được chia sẻ trong bộ nhớ qua các quy trình, độ lệch của chúng được xác định bởi giá trị sai lệch toàn hệ thống được tính khi khởi động. Giá trị được tính là TSC của CPU khi hàm MiInitializeRelocations được gọi lần đầu tiên, được dịch chuyển và bị che thành giá trị 8 bit. Giá trị này chỉ được tính một lần cho mỗi lần khởi động.

Khi DLL được tải, chúng đi vào vùng nhớ được chia sẻ giữa 0x500000000x78000000. DLL đầu tiên được tải luôn là ntdll.dll, được tải tại 0x78000000 - bias * 0x100000, Trong đó bias là giá trị sai lệch toàn hệ thống được tính khi khởi động. Vì sẽ rất đơn giản khi tính toán độ lệch của mô-đun nếu bạn biết địa chỉ cơ sở của ntdll.dll, nên thứ tự các mô-đun được tải cũng được chọn ngẫu nhiên.

Khi chủ đề được tạo, vị trí cơ sở ngăn xếp của họ được ngẫu nhiên. Điều này được thực hiện bằng cách tìm 32 vị trí thích hợp trong bộ nhớ, sau đó chọn một vị trí dựa trên TSC hiện tại được chuyển mặt nạ thành giá trị 5 bit. Khi địa chỉ cơ sở đã được tính toán, một giá trị 9 bit khác được lấy từ TSC để tính địa chỉ cơ sở ngăn xếp cuối cùng. Điều này cung cấp một mức độ lý thuyết cao của ngẫu nhiên.

Cuối cùng, vị trí của đống và phân bổ heap được chọn ngẫu nhiên. Giá trị này được tính là giá trị có nguồn gốc TSC 5 bit nhân với 64KB, mang lại phạm vi heap có thể là 00000000 Thành 001f0000.

Khi tất cả các cơ chế này được kết hợp với DEP, chúng tôi sẽ không được thực thi shellcode. Điều này là do chúng tôi không thể thực thi ngăn xếp, nhưng chúng tôi cũng không biết bất kỳ hướng dẫn ROP nào của chúng tôi sẽ nằm trong bộ nhớ. Một số thủ thuật có thể được thực hiện với nop trượt để tạo khai thác xác suất, nhưng chúng không hoàn toàn thành công và không phải lúc nào cũng có thể tạo.

Cách duy nhất để vượt qua DEP và ASLR một cách đáng tin cậy là thông qua rò rỉ con trỏ. Đây là tình huống trong đó một giá trị trên ngăn xếp, tại một vị trí đáng tin cậy, có thể được sử dụng để định vị con trỏ hàm có thể sử dụng hoặc tiện ích ROP. Một khi điều này được thực hiện, đôi khi có thể tạo ra một trọng tải đáng tin cậy bỏ qua cả hai cơ chế bảo vệ.

Nguồn:

Đọc thêm:

153
Polynomial

Để bổ sung cho câu trả lời tự trả lời của @ Polynomial: DEP thực sự có thể được thi hành trên các máy x86 cũ hơn (trước bit NX), nhưng với giá cả.

Cách dễ dàng nhưng hạn chế để thực hiện DEP trên phần cứng x86 cũ là sử dụng các thanh ghi phân đoạn. Với các hệ điều hành hiện tại trên các hệ thống như vậy, địa chỉ là các giá trị 32 bit trong không gian địa chỉ 4 GB phẳng, nhưng bên trong mỗi truy cập bộ nhớ lại sử dụng địa chỉ 32 bit một thanh ghi 16 bit đặc biệt , được gọi là "đăng ký phân khúc".

Trong cái gọi là chế độ được bảo vệ, các thanh ghi phân đoạn trỏ đến một bảng bên trong ("bảng mô tả" - thực tế có hai bảng như vậy, nhưng đó là tính kỹ thuật) và mỗi mục trong bảng chỉ định các đặc điểm của phân đoạn. Cụ thể, các loại quyền truy cập được phép và size của phân khúc. Hơn nữa, việc thực thi mã ngầm sử dụng thanh ghi phân đoạn CS, trong khi truy cập dữ liệu sử dụng chủ yếu DS (và truy cập ngăn xếp, ví dụ như với Pushpop opcodes, sử dụng SS). Điều này cho phép hệ điều hành chia không gian địa chỉ thành hai phần, các địa chỉ thấp hơn nằm trong phạm vi cho cả CS và DS, trong khi các địa chỉ phía trên nằm ngoài phạm vi của CS. Ví dụ: phân đoạn được mô tả bởi CS có kích thước 512 MB. Điều này có nghĩa là mọi địa chỉ ngoài 0x20000000 sẽ có thể truy cập dưới dạng dữ liệu (đọc hoặc ghi để sử dụng DS làm thanh ghi cơ sở) nhưng tại đó các nỗ lực thực thi sẽ sử dụng CS, tại đó CPU sẽ đưa ra một ngoại lệ (mà hạt nhân sẽ chuyển đổi thành tín hiệu phù hợp như SIGILL hoặc SIGSEGV, thường ngụ ý cái chết của quá trình vi phạm).

(Lưu ý rằng các phân đoạn được áp dụng trên không gian địa chỉ; MMU vẫn đang hoạt động, ở lớp thấp hơn, do đó, mẹo được giải thích ở trên là theo quy trình.)

Điều này rất rẻ để làm: phần cứng x86 does thực thi các phân đoạn, một cách có hệ thống (và 80386 đầu tiên đã làm điều đó; thực tế, 80286 đã có các phân đoạn như vậy có ranh giới, nhưng chỉ có độ lệch 16 bit ). Chúng ta thường có thể quên chúng vì các hệ điều hành lành mạnh đặt các phân đoạn bắt đầu ở độ lệch 0 và dài 4 GB, nhưng đặt chúng không có nghĩa là bất kỳ chi phí nào mà chúng ta chưa có. Tuy nhiên, là một cơ chế DEP, nó không linh hoạt: khi một số khối dữ liệu được yêu cầu từ kernel, kernel phải quyết định xem đây có phải là mã hay không vì mã, vì đường biên được cố định. Chúng tôi không thể quyết định tự động chuyển đổi bất kỳ trang đã cho nào giữa chế độ mã và chế độ dữ liệu.

Cách thú vị nhưng có phần tốn kém hơn để làm DEP sử dụng thứ gọi là PaX . Để hiểu những gì nó làm, người ta phải đi vào một số chi tiết.

MMU trên phần cứng x86 sử dụng các bảng trong bộ nhớ, mô tả trạng thái của mỗi trang 4 kB trong không gian địa chỉ. Không gian địa chỉ là 4 GB, vì vậy có 1048576 trang. Mỗi trang được mô tả bởi một mục 32 bit trong bảng phụ; có 1024 bảng phụ, mỗi bảng chứa 1024 mục và có một bảng chính, với 1024 mục nhập trỏ đến 1024 bảng phụ. Mỗi mục cho biết vị trí của đối tượng được trỏ (bảng phụ hoặc trang) trong RAM, hoặc liệu nó có ở đó không, và quyền truy cập của nó là gì. Nguyên nhân của vấn đề là quyền truy cập là về các mức đặc quyền (mã nhân so với vùng người dùng) và chỉ một bit cho loại truy cập, do đó cho phép "đọc-ghi" hoặc "chỉ đọc". "Thực thi" được coi là một loại truy cập đọc. Do đó, MMU không có khái niệm "thực thi" nào khác với truy cập dữ liệu. Cái nào có thể đọc được, có thể thực thi được.

(Kể từ Pentium Pro, trở lại thế kỷ trước, bộ xử lý x86 biết một định dạng khác cho các bảng, được gọi là PAE . Nó nhân đôi kích thước của các mục, trong đó chừa chỗ cho việc giải quyết nhiều RAM vật lý hơn và cũng thêm một bit NX - nhưng bit cụ thể đó chỉ được phần cứng triển khai vào khoảng năm 2004.)

Tuy nhiên, có một mẹo. RAM chậm. Để thực hiện truy cập bộ nhớ, trước tiên bộ xử lý phải đọc bảng chính để xác định bảng phụ mà nó phải tham khảo, sau đó thực hiện đọc khác vào bảng phụ đó và chỉ tại thời điểm đó, bộ xử lý có biết có cho phép truy cập bộ nhớ hay không và ở đâu trong vật lý RAM dữ liệu được truy cập thực sự. Đây là các truy cập đọc với sự phụ thuộc hoàn toàn (mỗi truy cập phụ thuộc vào giá trị được đọc trước đó) vì vậy điều này trả độ trễ đầy đủ, trên CPU hiện đại, có thể biểu thị hàng trăm chu kỳ xung nhịp. Do đó, CPU bao gồm một bộ đệm cụ thể chứa bảng MMU được truy cập gần đây nhất mục. Bộ đệm này là Bộ đệm dịch thuật .

Từ 80486 trở đi, CPU x86 không có một TLB, nhưng hai. Bộ nhớ đệm hoạt động dựa trên các heuristic và heuristic phụ thuộc vào các mẫu truy cập và các mẫu truy cập cho mã có xu hướng khác với các mẫu truy cập cho dữ liệu. Vì vậy, những người thông minh tại Intel/AMD/khác nhận thấy rằng đáng để có một TLB dành riêng cho truy cập mã (thực thi) và một cái khác để truy cập dữ liệu. Hơn nữa, 80486 có opcode (invlpg) có thể xóa một mục cụ thể khỏi TLB.

Vì vậy, ý tưởng là như sau: làm cho hai TLB có quan điểm khác nhau của cùng một mục. Tất cả các trang được đánh dấu trong các bảng (trong RAM) là "vắng mặt", do đó gây ra ngoại lệ khi truy cập. Hạt nhân bẫy ngoại lệ và ngoại lệ bao gồm một số dữ liệu về loại quyền truy cập, đặc biệt là liệu đó có phải để thực thi mã hay không. Sau đó, hạt nhân làm mất hiệu lực mục nhập TLB mới đọc (mục nhập "vắng mặt"), sau đó điền mục nhập vào RAM với một số quyền cho phép truy cập, sau đó buộc một quyền truy cập của loại cần thiết ( hoặc đọc dữ liệu hoặc thực thi mã), cung cấp mục nhập vào TLB tương ứng và only cái đó. Sau đó, hạt nhân nhanh chóng thiết lập mục nhập trong RAM trở lại để vắng mặt và cuối cùng trở lại quy trình (quay lại thử lại mã opcode đã kích hoạt ngoại lệ).

Hiệu ứng ròng là, khi thực thi trở lại mã quy trình, TLB cho mã hoặc TLB cho dữ liệu chứa mục nhập phù hợp, nhưng TLB khác không, và sẽ không vì các bảng trong RAM vẫn nói "vắng mặt". Tại thời điểm đó, hạt nhân ở vị trí để quyết định có cho phép thực thi hay không, độc lập với nó cho phép truy cập dữ liệu hay không. Do đó, nó có thể thực thi ngữ nghĩa giống như NX.

Ma quỷ ẩn giấu trong các chi tiết; trong trường hợp này, có chỗ cho cả một quân đoàn quỷ. Một điệu nhảy như vậy với phần cứng không dễ thực hiện đúng. Đặc biệt là trên các hệ thống đa lõi.

Chi phí hoạt động như sau: khi truy cập được thực hiện và TLB không chứa mục nhập có liên quan, các bảng trong RAM phải được truy cập và một mình ngụ ý mất vài trăm chu kỳ. chi phí, PaX thêm chi phí ngoại lệ và mã quản lý điền vào TLB đúng, do đó biến "vài trăm chu kỳ" thành "vài nghìn chu kỳ". May mắn thay, TLB bỏ lỡ là đúng. được đo sự chậm lại ít nhất là 2,7% cho một công việc biên dịch lớn (tuy nhiên điều này phụ thuộc vào loại CPU).

Bit NX làm cho tất cả lỗi thời này. Lưu ý rằng bản vá PaX cũng chứa một số tính năng liên quan đến bảo mật khác, chẳng hạn như ASLR, có sẵn với một số tính năng chức năng của hạt nhân chính thức mới hơn.

40
Thomas Pornin