K14 chia sẻ về trải nghiệm chơi CTF năm nhất Đại học kỳ 5: Những bài Buffer overflow “kinh điển”

ADMIN
18:02 09/12/2020

Giới thiệu

Xin chào, mình là Trần Nguyễn Đức Huy, hiện đang là sinh viên năm hai của Trường Đại học Công nghệ Thông tin - ĐHQG Thành phố Hồ Chí Minh. Mình hiện đang tham gia những cuộc thi CTF ở mảng Binary Exploitation, bên dưới là một số bài đơn giản, cụ thể là Buffer Overflow mà mình giải được trong khoảng thời gian mình tham gia PicoCTF 2019 team JustFreshMen.

Buffer Overflow là gì?

Giả sử nhé, chúng ta có một chương trình quản lí dữ liệu nào đó được thiết kế khá là tệ và có vẻ không được bảo mật cho lắm (quản lí sinh viên chẳng hạn). Người dùng chương trình này sử dụng các loại tài khoản khác nhau với các chức quyền khác nhau như: sinh viên, giảng viên, vân vân. mây mây... Sinh viên có thể sửa đổi thông tin cá nhân của mình như mật khẩu tài khoản, ngày tháng năm sinh, v.v... Giảng viên có thể sửa đổi điểm của sinh viên ở học phần mình phụ trách.

Ví dụ bạn sinh viên Nguyễn Văn A muốn sửa đổi ngày sinh của mình. Trong chương trình, mục ngày sinh được người thiết kế chương trình viết những dòng lưu ý hết sức cụ thể và thận trọng: 

"Tối đa 10 kí tự. Định dạng: ngày/tháng/năm"

Và vì một lí do nào đó mà vùng nhớ lưu trữ ngày sinh của sinh viên lại nằm kế bên vùng nhớ lưu trữ điểm của sinh viên, nó sẽ kiểu kiểu thế này:

Họ và tênNgày sinhĐiểm
Nguyễn Văn A01/01/20014.4

Bạn A học kì này có vẻ học không được tốt môn Cấu trúc rời rạc cho lắm, nên bạn quyết định xắn tay áo lên và khai thác chương trình với hi vọng có thể sửa được những con điểm thấp của mình. Bạn A quyết định đi trái lại với lời dặn dò thân thương của người thiết kế chương trình và nhập 11 kí tự vào mục Ngày sinh (thêm một số 0 chẳng hạn: 01/01/20010), lúc này A thấy điều kì diệu xảy ra:

Họ và tênNgày sinhĐiểm
Nguyễn Văn A01/01/20010.4

Số 4 ban đầu đã được thay thế bằng số 0 mà bạn A nhập vào (hay nói cách khác số 0 được bạn A nhập thêm vào đã tràn sang vùng nhớ lưu trữ điểm). Bạn A nhận ra tuy người thiết kế chương trình đã dặn dò kĩ lưỡng người dùng, nhưng bản thân người thiết kế chương trình lại không giới hạn những gì người dùng được phép nhập vào. Thế là bạn A nhập thêm số 10 phía sau ngày sinh của mình, Enter, và bùm, easy học bổng:

Họ và tênNgày sinhĐiểm
Nguyễn Văn A01/01/200110

Tất nhiên trên thực tế cuộc sống không thể nào đơn giản như vậy vì sự phức tạp của số học trên máy tính cũng như cách mà máy tính lưu trữ dữ liệu, nhưng đó là một vấn đề khác, chúng ta sẽ không bàn ở đây. Dù sao ví dụ này cũng sẽ giúp bạn hiểu được kha khá thế nào là tràn bộ đệm (Buffer Overflow).

Giải bài Overflow của PicoCTF2019

OverFlow0

  • Find a way to trigger the flag to print

  • If you try to do the math by hand, maybe try and add a few more characters. Sometimes there are things you aren't expecting.

Solution:

Bài này khá đơn giản.

Đề bài cho ta 2 files: 1 file source code và 1 file executable, thử chạy file executable xem có gì trong đó nào

 

Chương trình cần file flag mới có thể hoạt động, ta có thể tạo một file flag giả với nội dung chẳng hạn như "This_is_just_a_local_flag"

 

Okay vậy là ổn. Bắt tay vào thôi

Chương trình yêu cầu một argument để chạy, ta sẽ điền vào ngay sau lệnh run:

 

Có vẻ như chương trình chỉ in ra lại những gì đã được nhập vào. Thử xem qua source code xem có gì thú vị không

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

#define FLAGSIZE_MAX 64

char flag[FLAGSIZE_MAX];

void sigsegv_handler(int sig) {
fprintf(stderr, "%s\n", flag);
fflush(stderr);
exit(1);
}

void vuln(char *input){
char buf[128];
strcpy(buf, input);
}

int main(int argc, char **argv){

FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(flag,FLAGSIZE_MAX,f);
signal(SIGSEGV, sigsegv_handler);

gid_t gid = getegid();
setresgid(gid, gid, gid);

if (argc > 1) {
vuln(argv[1]);
printf("You entered: %s", argv[1]);
}
else
printf("Please enter an argument next time\n");
return 0;
}

Ở đây có 2 chỗ đáng chú ý:

 
signal(SIGSEGV, sigsegv_handler);

và hàm sigsegv_handler()

 
void sigsegv_handler(int sig) {
fprintf(stderr, "%s\n", flag);
fflush(stderr);
exit(1);

Hàm sigsegv_handler() sẽ in ra flag cho chúng ta, và có thể thấy rằng hàm này được truyền vào làm đối số của signal(). Vậy signal() là gì ?

 
man signal

signal() sẽ thực thi hàm được truyền vào ở đối số thứ 2 (signal_handler) khi tín hiệu được xác định ở đối số thứ nhất được bật. Vậy chỉ cần khiến cho tín hiệu SIGSEGV được bật thì ta sẽ có được flag. Lại tìm hiểu tiếp xem điều gì sẽ gây ra SIGSEGV

 
Nguồn: https://www.tutorialspoint.com/c_standard_library/c_function_signal.htm

Vậy để gây ra SIGSEGV, chúng ta sẽ phải ghi vượt quá vùng nhớ được cấp phát. Nếu xem lại source code ta có thể thấy được rằng ở hàm main():

 
vuln(argv[1]);

và ở hàm vuln():

 
void vuln(char *input){
char buf[128];
strcpy(buf, input);
}

Hàm vuln() nhận vào những gì ta nhập và copy vào mảng buf. Do hàm strcpy() không kiểm tra độ dài chuỗi input nhưng chuỗi buf chỉ có độ dài 128 bytes, nếu ta nhập vào nhiều hơn 128 bytes sẽ gây ra lỗi truy cập bộ nhớ và SIGSEGV sẽ được bật. Nhưng các compilers thường cấp phát nhiều hơn 4 bytes so với kích thước của mảng nên ta cần ít nhất 133 bytes để gây tràn bộ nhớ.

 
Không nên dùng strcpy nhé

Mạn phép sử dụng python vì tuổi thọ bàn phím là có hạn...

 

Vậy là chúng ta đã nhận được local flag, giờ chỉ việc thử trên shell server của PicoCTF để nhận được flag thật sự

Flag: picoCTF{xxxxxxxxxxxxxxxxxxxxxxxxxxx}

OverFlow1

 
  • Take control that return address

  • Make sure your address is in Little Endian.

Solution:

Chạy file executable ta được như sau:

 

Dù ta nhập gì đi nữa thì có vẻ chương trình cũng sẽ nhảy đến địa chỉ 0x8048705. Mở source code xem thử bên trong như thế nào

 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include "asm.h"
#define BUFFSIZE 64
#define FLAGSIZE 64
void flag() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("Flag File is Missing. please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
printf(buf);
}
void vuln(){
char buf[BUFFSIZE];
gets(buf);
printf("Woah, were jumping to 0x%x !\n", get_return_address());
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Give me a string and lets see what happens: ");
vuln();
return 0;
}

Dễ thấy được rằng hàm flag() sẽ in ra flag cho chúng ta. Nhưng chương trình không có cách nào để truy cập vào hàm flag().

Quan sát hàm vuln() ta thấy được hàm này đang sử dụng gets() để nhận input từ người dùng

 
void vuln(){
char buf[BUFFSIZE];
gets(buf);
printf("Woah, were jumping to 0x%x !\n", get_return_address());
}
 
"Never use this function." - You have been warned

Hàm gets() là một hàm nguy hiểm vào không được khuyến khích sử dụng vì gets() không kiểm tra độ dài xâu kí tự đầu vào. Kết quả là người dùng có thể tùy ý nhập bao nhiêu kí tự đều được. Các kí tự sau khi lấp đầy vùng nhớ được cấp phát, nếu chưa kết thúc, sẽ bị tràn và ghi đè lên các vùng nhớ khác.

Ta có thể dùng phương pháp này để ghi đè lên địa chỉ trả về của hàm vuln(), thay thế nó bằng địa chỉ của hàm flag(), chương trình lúc này sẽ nhảy đến địa chỉ của hàm flag().

Thử nhập vào 200 chữ 'A' xem sao

 

Dòng "Segmentation fault" nghĩa là chúng ta đã thành công trong việc gây tràn vùng nhớ và địa chỉ trả về cũng đã được thay đổi thành 0x41414141 hay "AAAA" (41 trong hex là 'A' trong ASCII). Nếu ta thay thế các kí tự 'A' bằng địa chỉ của hàm flag(), ta có thể dễ dàng hướng chương trình tới việc thực thi hàm flag().

Nhưng ta vẫn chưa biết được rằng trong 200 kí tự 'A' đã được nhập vào thì các kí tự nào đã ghi đè lên địa chỉ trả về của hàm vuln(). Ta cần xác định chính xác độ dài tính từ biến buf đến địa chỉ trả về này. Để làm được việc này ta có thể sử dụng chuỗi cyclic (căn bản là một chuỗi bao gồm các chuỗi con không trùng lặp dùng để xác định vị trí): https://wiremask.eu/tools/buffer-overflow-pattern-generator/

 

Sử dụng địa chỉ được in ra, ta có thể tìm ra được độ dài từ biến buf đến địa chỉ trả về là 76 bytes

 

Tiếp theo ta cần tìm địa chỉ của hàm flag(), ta có thể thực hiện việc này bằng readelf

 

Địa chỉ hàm flag() là 0x080485e6. Giờ ta chỉ việc xây dựng payload để đưa vào chương trình. Lưu ý rằng ta phải chuyển địa chỉ hàm flag() về dạng bytes (theo định dạng Little Endian - xem thêm ở đây: https://en.wikipedia.org/wiki/Endianness) rồi mới nối với chuỗi 76 kí tự 'A' nếu không chương trình sẽ hiểu nhầm địa chỉ trên là một chuỗi kí tự thông thường. Thư viện pwntools trong Python có hỗ trợ hàm p32 dành cho mục đích này (Why 32 ? Vì chương trình được compile ở dạng 32-bit, dùng lệnh file để có thể xác định được).

Flag: picoCTF{xxxxxxxxxxxxxxxxxxxxxxxxxxx}

TIN LIÊN QUAN
🎉 Chúc mừng đội W1.Shark gồm các bạn sinh viên ngành An toàn thông tin - Khoa Mạng máy tính và Truyền thông đã đạt giải ba trong cuộc thi Data For Life 2023! Với đề tài dự thi "Phương pháp phát hiện website lừa đảo dựa trên học sâu...
"𝐐𝐮𝐞𝐬𝐭 𝐟𝐨𝐫 𝐓𝐚𝐥𝐞𝐧𝐭: 𝐖𝐡𝐨 𝐖𝐢𝐥𝐥 𝐒𝐡𝐢𝐧𝐞?" là chương trình được tổ chức bởi Bộ môn An Toàn Thông Tin. Đoàn khoa Mạng máy tính và truyền thông cùng với sự hỗ trợ của Phòng thí nghiệm An toàn thông tin – UIT InSecLab. Chính thức khởi động vòng loại vào...
✨ Bạn là sinh viên mới vào trường, bạn có niềm đam mê đặc biệt đối với bảo mật và an ninh mạng, bạn muốn thử sức mình với những thử thách khó nhằn, vậy thì đây là cuộc thi dành cho bạn. 🔥 𝐖𝐀𝐍𝐍𝐀𝐆𝐀𝐌𝐄 𝐅𝐑𝐄𝐒𝐇𝐌𝐀𝐍 𝟐𝟎𝟐𝟑 là một cuộc...