# Báo cáo lần 4 [21/05/2023] ## Các vùng nhớ của tiến trình ### `/proc/$pid/maps` Khi một tiến trình đang chạy, ta hoàn toàn có thể xem được tiến trình đó tạo các trang nhớ tại địa chỉ nào bằng cách đọc dữ liệu của file `/proc/$pid/maps`. Giả sử ta chạy một chương trình có tên `youwantmetorunwhat` và số pid của tiến trình này là 303: ![](https://i.imgur.com/gxhSH3M.png) Là người dùng `JHT` giống với user của tiến trình, ta chỉ việc đọc file `/proc/303/maps` là có thể biết được chương trình đã tạo ra các phân vùng nào và địa chỉ của các phân vùng đó: ![](https://i.imgur.com/k9oHBKM.png) Dữ liệu vẫn còn nhưng vì dài quá nên mình chỉ lấy phần đầu. Nếu ta xem bằng gdb thì các địa chỉ này là hoàn toàn giống nhau: ![](https://i.imgur.com/XaXuCGL.png) ### `/proc/$pid/mem` Bạn có bao giờ thắc mắc tại sao GDB lại có thể in ra dữ liệu của các trang nhớ đó không? Theo mình nghĩ là nó sẽ lấy dữ liệu từ file `/proc/$pid/mem` để in ra và đây là file đọc và ghi được. Dữ liệu trong file `/proc/$pid/mem` là khác nhau cho các tiến trình khác nhau. Để đọc được dữ liệu của các trang nhớ đó, ta không dùng `cat` thể đọc như cách đọc file được vì nó sẽ báo lỗi. Ta đọc thử file `/proc/303/mem` với tiến trình như phần trên: ![](https://i.imgur.com/rQFnbwg.png) Muốn đọc được file mem đó, ta buộc phải đưa con trỏ tới đúng vị trí địa chỉ mà chương trình khởi tạo. Chẳng hạn khi ta đọc file `/proc/303/maps` để lấy thông tin các địa chỉ, ta biết được tại địa chỉ `0x400000` là vùng nhớ có tồn tại trong tiến trình. Để đọc được dữ liệu của vùng nhớ `0x400000` đó, ta phải mở file `/proc/303/mem` trước và rồi đưa con trỏ tới vị trí thứ `0x400000` thì việc đọc file sẽ không còn báo lỗi. Trước tiên, ta dùng GDB để attach với tiến trình và xem dữ liệu tại trang `0x400000` để biết tại trang đó đang có dữ liệu gì: ![](https://i.imgur.com/7NZ8tS2.png) Đó là 16 byte đầu của trang `0x400000`. Tiếp theo, ta sẽ dùng lệnh `dd` để đọc dữ liệu từ file `/proc/303/mem` (vì lệnh `cat` không thể thay đổi con trỏ file được, lệnh `dd` thì được): ```bash dd skip=4194304 count=16 bs=1 if=/proc/303/mem | xxd ``` Chú thích: - Tham số `skip=<n>`: Hoạt động như lệnh seek, đưa con trỏ qua `<n>` byte và bắt đầu đọc tiếp từ đó - Tham số `count=<n>`: Đọc `<n>` lần - Tham số `bs=<n>`: Đọc `<n>` byte dữ liệu --> Tổng dữ liệu đọc: `count * bs` - Tham số `if=<path>`: Đọc từ file có đường dẫn `<path> - Lệnh `xxd` in dữ liệu không đọc được Đây là cách mà lệnh `xxd` hoạt động: ![](https://i.imgur.com/0mIWdfa.png) Nên nếu có byte nào không phải ký tự in được thì ta vẫn có thể đọc được bình thường. Do đó câu lệnh bên trên sẽ in ra 16 byte đầu của vùng nhớ `0x400000`: ![](https://i.imgur.com/FsHWxLp.png) So sánh với dữ liệu in ra bằng GDB bên trên thì ta thấy hoàn toàn giống nhau. Các bạn có thể thử tiếp với các phân vùng khác để hiểu rõ thêm. ### Dump dữ liệu Khi ta có thể đọc dữ liệu trong các trang nhớ của tiến trình, ta có thể dump nó ra để lấy các dữ liệu cần thiết. Script bên dưới đây sẽ giúp ta thực hiện điều đó: ```python #! /usr/bin/env python import re maps_file = open("/proc/self/maps", 'r') mem_file = open("/proc/self/mem", 'rb', 0) output_file = open("self.dump", 'wb') for line in maps_file.readlines(): # for each mapped region m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line) if m.group(3) == 'r': # if this is a readable region start = int(m.group(1), 16) end = int(m.group(2), 16) mem_file.seek(start) # seek to region start chunk = mem_file.read(end - start) # read region contents output_file.write(chunk) # dump contents to standard output maps_file.close() mem_file.close() output_file.close() ``` Script này sẽ dump toàn bộ dữ liệu trong tất cả các trang của tiến trình và ta hoàn toàn có thể thay đổi pid cần dump. Nhưng nếu ta đang làm trong GDB mà muốn dump dữ liệu để phân tích thì ta cũng sẽ có câu lệnh sau: ```gdb gef➤ #dump memory <output_path> <start_addr> <end_addr> ``` Vẫn với tiến trình `303` bên trên, nếu ta muốn dump toàn bộ dữ liệu của trang đầu từ địa chỉ `0x400000` tới `0x5a8000` thì ta chỉ việc chạy như sau: ```gdb gef➤ dump memory 303.dump 0x400000 0x5a8000 ``` Và ta có được file đã dump ra: ![](https://i.imgur.com/ug3NmKq.png) Nếu ta kiểm tra với dữ liệu đã đọc ở bên trên, ta thấy dữ liệu này hoàn toàn trùng khớp. Đây là dữ liệu dump ra: ![](https://i.imgur.com/oYNPD98.png) Còn đây là dữ liệu xem trong GDB: ![](https://i.imgur.com/IENcG2C.png) ### Ghi đè dữ liệu Tất cả file `/proc/$pid/mem` của các tiến trình đều có quyền đọc và ghi. Do đó chuyện gì sẽ xảy ra nếu ta ghi dữ liệu vào trong file `/proc/$pid/mem`? Chúng ta sẽ cùng tìm hiểu đoạn code c như sau: ```c #include <stdio.h> #include <stdlib.h> char sc[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91" "\xd0\x8c\x97\xff\x48\xf7\xdb\x53" "\x54\x5f\x99\x52\x57\x54\x5e\xb0" "\x3b\x0f\x05"; int main() { FILE *fp = fopen("/proc/self/mem", "w"); if (fp == NULL){ printf("Error opening /proc/self/mem"); exit(0); } fseek(fp, (long int)__builtin_return_address(0), SEEK_CUR); fwrite(sc, 1, 27, fp); fclose(fp); } ``` Compile đoạn code đó và chạy thì chương trình cho ta shell. Vấn đề ở đây là khi ta ghi vào `/proc/self/mem` thì ta hoàn toàn có thể thay đổi dữ liệu của các vùng nhớ, thậm chí là các vùng nhớ chỉ đọc. Trước khi ta thực hiện ghi vào file `/proc/self/mem`, ta cần phải đưa con trỏ vào đúng vị trí cần ghi. Mục tiêu ở đây là ta sẽ thay đổi dữ liệu của địa chỉ `__libc_start_main_ret`. Đây là các lệnh của `__libc_start_main_ret` trước khi thay đổi: ![](https://i.imgur.com/o5yvDph.png) Do đó ta sẽ muốn thay đổi các lệnh này thành shellcode của ta để thực hiện việc tạo shell. Để thay đổi thông qua `/proc/self/mem`, ta sẽ thiết lập con trỏ tới địa chỉ của `__libc_start_main_ret`: ```c fseek(fp, (long int)__builtin_return_address(0), SEEK_CUR); ``` Sau đó ta thực hiện việc ghi shellcode vào: ```c fwrite(sc, 1, 27, fp); ``` Ghi vậy nhưng các lệnh của `__libc_start_main_ret` vẫn chưa thay đổi. Dữ liệu thực sự được ghi khi ta đóng file: ```c fclose(fp); ``` Đây là kết quả sau khi đã ghi đè `__libc_start_main_ret`: ![](https://i.imgur.com/fBGQbEe.png) Khi thực hiện main xong, ta sẽ nhảy vào được `__libc_start_main_ret` để thực thi shellcode: ![](https://i.imgur.com/Nvm6lQi.png) Cuối cùng ta lấy được shell! ## KCSC CTF 2023 Dưới đây là các challenge và solve script. - Challenge [ret2libc](https://github.com/nhtri2003gmail/CTFWriteup/blob/master/2023/KCSCCTF/ret2libc.zip) - Challenge [racecar](https://github.com/nhtri2003gmail/CTFWriteup/blob/master/2023/KCSCCTF/racecar.zip) - Challenge [pwncry](https://github.com/nhtri2003gmail/CTFWriteup/blob/master/2023/KCSCCTF/pwncry.zip)