Buổi 08: Tràn Số Nguyên & Chuỗi Định Dạng
Ôn tập Buffer Overflow
1. Lỗ Hổng Tràn Số Nguyên (Integer Overflow)
1.1 Định nghĩa
1.2 Tại sao lại xảy ra?
Máy tính lưu số nguyên dưới dạng nhị phân với số bit cố định. Ví dụ kiểu int 32-bit:
int var = 25;
(25)₁₀ = (00000000 00000000 00000000 00011001)₂Khi giá trị vượt quá 32 bit, bit cao nhất bị cắt bỏ (bị “tràn”), dẫn đến kết quả hoàn toàn sai.
Biểu diễn số âm (Two’s Complement):
+127 = 0111 1111
+1 = 0000 0001
0 = 0000 0000
-1 = 1111 1111
-2 = 1111 1110
-128 = 1000 00001.3 Các kiểu dữ liệu trong C
| Kiểu dữ liệu | Kích thước (bytes) | Phạm vi | Format |
|---|---|---|---|
char | 1 | -128 → 127 | %c |
unsigned char | 1 | 0 → 255 | %c |
short / int | 2 | -32,768 → 32,767 | %i hoặc %d |
unsigned int | 2 | 0 → 65,535 | %u |
long | 4 | -2,147,483,648 → 2,147,483,647 | %ld |
unsigned long | 4 | 0 → 4,294,967,295 | %lu |
float | 4 | 3.4e-38 → 3.4e+38 | %f |
double | 8 | 1.7e-308 → 1.7e+308 | %lf |
long double | 10 | 3.4e-4932 → 1.1e+4932 | %lf |
1.4 Ví dụ minh họa
#include <iostream>
using namespace std;
int main() {
int a = 4278190080; // Vượt quá INT_MAX (2147483647) → tràn âm
cout << "a = " << a << '\n'; // Output: a = -16777216
unsigned int b = 4299360564; // Vượt quá UINT_MAX (4294967295) → wrap-around
cout << "b = " << b << '\n'; // Output: b = 4273796812 (hoặc giá trị khác)
int c = 1;
unsigned int d = 4294967295; // UINT_MAX
cout << "c - d = " << c - d << '\n'; // Kết quả: 2 (do arithmetic wrap-around)
return 0;
}1.5 Cách khai thác Integer Overflow
Ví dụ khai thác thực tế:
// Đoạn code dễ bị tấn công
void* allocate_buffer(unsigned int count, unsigned int size) {
// Nếu count = 0x80000001, size = 2
// count * size = 0x100000002 → tràn thành 2 (với 32-bit)
unsigned int total = count * size; // INTEGER OVERFLOW!
void* buf = malloc(total); // Cấp phát chỉ 2 bytes
// Nhưng sau đó copy count*size bytes thật → heap overflow!
memcpy(buf, input, count * size);
return buf;
}2. Lỗ Hổng Chuỗi Định Dạng (Format String Vulnerability)
2.1 Chuỗi định dạng là gì?
Format string là một chuỗi ký tự chứa các ký hiệu chuyển đổi (conversion specifiers) dùng để định dạng dữ liệu đầu ra.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
char *format = "%s";
char *arg1 = "Hello World!\n";
printf(format, arg1); // In: Hello World!
return EXIT_SUCCESS;
}2.2 Các hàm sử dụng format string
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
#include <stdarg.h>
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);2.3 Các ký hiệu định dạng quan trọng
| Ký hiệu | Mô tả | Kích thước |
|---|---|---|
%d | Số nguyên có dấu | 4 bytes |
%u | Số nguyên không dấu | 4 bytes |
%x | Số thập lục phân (hex) | 4 bytes |
%s | Chuỗi ký tự (con trỏ) | pointer |
%c | Ký tự đơn | 1 byte |
%n | Ghi số byte đã in vào biến con trỏ | pointer |
Ký hiệu xác định độ dài:
| Ký hiệu | Độ dài | Kiểu |
|---|---|---|
hh | 1 byte | char |
h | 2 bytes | short int |
l | 4 bytes | long int |
ll | 8 bytes | long long int |
Ví dụ: %hd → đọc short int 2 bytes.
2.4 Ví dụ về định dạng nâng cao
printf("%03d.%03d.%03d.%03d", 127, 0, 0, 1);
// Output: 127.000.000.001
printf("%.2f", 5.6732);
// Output: 5.67
printf("%#010x", 3735928559);
// Output: 0xdeadbeef
// %n: đếm số byte đã in
int n = 5;
printf("%s%n\n", "01234", &n);
// n = 5 (độ dài "01234")
2.5 Cơ chế hoạt động của printf trên stack
Khi gọi printf("a=%d, b=%d, c at %08x\n", a, b, &c):
Stack layout (địa chỉ tăng dần):
┌──────────────────┐ ← ESP
│ addr of f_string│ (tham số 1: con trỏ format string)
├──────────────────┤
│ a │ (tham số 2: giá trị a)
├──────────────────┤
│ b │ (tham số 3: giá trị b)
├──────────────────┤
│ &c │ (tham số 4: địa chỉ của c)
├──────────────────┤
│ c │
│ b │
│ a │
│ 0x1001 │ ← (biến cục bộ cũ hơn)
└──────────────────┘Vấn đề: Khi thiếu tham số:
// Code bị lỗi: thiếu &c
printf("a=%d, b=%d, c at %08x\n", a, b); // Chỉ có 2 tham số, nhưng có 3 format spec
Stack layout:
┌──────────────────┐
│ addr of f_string│
├──────────────────┤
│ a │ ← %d đầu tiên đọc đây (OK)
├──────────────────┤
│ b │ ← %d thứ hai đọc đây (OK)
├──────────────────┤
│ UNKNOWN VALUE │ ← %08x đọc đây (GIÁ TRỊ RÁC TRÊN STACK!)
└──────────────────┘2.6 Khi người dùng kiểm soát format string
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
char buf[100];
fgets(buf, 100, stdin);
printf(buf); // ← LỖ HỔNG: format string do user kiểm soát!
return EXIT_SUCCESS;
}2.7 Các kỹ thuật khai thác Format String
Kỹ thuật 1: Crash chương trình
# Input: nhiều %s liên tiếp
python3 -c 'print("%s" * 100)' | ./formatKỹ thuật 2: Đọc dữ liệu trên stack
# Input: "AAAA" + "%08x." * 10
python3 -c 'print("AAAA" + "%08x." * 10)' | ./format
# Output ví dụ:
# AAAA00000064.f7fb35a0.00f0b5ff.ffffcfbe.00000001.000000c2.ffffd0b4.ffffcfbe.ffffd0bc.41414141.Kỹ thuật 3: Đọc dữ liệu tại địa chỉ tùy ý
Input: <4-byte address> + "%x " * n + "%s"Stack layout khi printf chạy:
┌─────────────────────┐ ← ESP
│ ptr to buf │ ← printf đọc format string từ đây
├─────────────────────┤
│ [addr cần đọc] │ ← buf[0..3]: 4 byte địa chỉ
├─────────────────────┤
│ ...%x ... %x ... │ ← các %x "nhảy qua" các giá trị stack
├─────────────────────┤
│ %s │ ← %s cuối cùng sẽ đọc tại địa chỉ buf[0..3]
└─────────────────────┘# Giả sử địa chỉ cần đọc là 0x08041054
addr = b'\x54\x10\x04\x08'
payload = addr + b'%x ' * 9 + b'%s'Kỹ thuật 4: Truy xuất tham số trực tiếp
// Cú pháp: %<số thứ tự>$<format>
printf("%3$d", 1, 2, 3); // Output: 3 (lấy tham số thứ 3)
printf("%1$s", "hello"); // Output: hello
Kỹ thuật 5: Ghi dữ liệu vào bộ nhớ với %n
%n là format specifier đặc biệt: ghi số byte đã in ra trước đó vào địa chỉ được chỉ định bởi tham số tương ứng.
int count;
printf("Hello%n", &count); // count = 5 (độ dài "Hello")
printf("Hi%n", &count); // count = 2 (độ dài "Hi")
Khai thác để ghi giá trị tùy ý:
# Muốn ghi giá trị 100 vào địa chỉ 0x08049704
# Cách: in đủ 100 ký tự trước %n
addr = b'\x04\x97\x04\x08' # địa chỉ đích (little-endian)
# In 96 ký tự nữa (4 byte addr đã in rồi) = tổng 100
payload = addr + b'%96x' + b'%n'Câu Hỏi Trắc Nghiệm
Câu 1. Kiểu unsigned char trong C có phạm vi giá trị là bao nhiêu?
- A. -128 đến 127
- B. 0 đến 255
- C. -256 đến 255
- D. 0 đến 127
Câu 2. Integer Overflow xảy ra khi nào?
- A. Khi biến không được khởi tạo
- B. Khi phép toán tạo ra giá trị vượt ngoài phạm vi biểu diễn của kiểu dữ liệu
- C. Khi chia cho 0
- D. Khi truy cập mảng ngoài giới hạn
Câu 3. Với signed char 8-bit, kết quả của 127 + 1 là bao nhiêu?
- A. 128
- B. 0
- C. -128
- D. -1
Câu 4. Với unsigned int 32-bit, kết quả của 4294967295 + 1 là bao nhiêu?
- A. 4294967296
- B. 1
- C. -1
- D. 0
Câu 5. Nguy hiểm chính khi integer overflow xảy ra trong hàm cấp phát bộ nhớ là gì?
- A. Chương trình dừng ngay lập tức
- B. Vùng nhớ được cấp phát nhỏ hơn thực tế cần, dẫn đến heap overflow
- C. CPU bị quá tải
- D. Stack bị xóa hoàn toàn
Câu 6. Lỗ hổng CVE BadAlloc (Black Hat 2021) liên quan đến loại lỗ hổng nào?
- A. Buffer overflow trên stack
- B. Integer overflow trong memory allocator
- C. Format string
- D. Use-after-free
Câu 7. Format string %08x có nghĩa là gì?
- A. In số hex, tối đa 8 ký tự
- B. In số hex, đệm 0 cho đủ 8 ký tự
- C. In 8 byte dưới dạng hex
- D. In địa chỉ 8 byte
Câu 8. Format specifier %n dùng để làm gì?
- A. In số nguyên âm
- B. In ký tự xuống dòng
- C. Ghi số byte đã in ra trước đó vào biến con trỏ
- D. Đọc một số từ stdin
Câu 9. Đoạn code nào sau đây có lỗ hổng format string?
- A.
printf("%s", buf); - B.
printf(buf); - C.
printf("%d %s", num, buf); - D.
fprintf(stderr, "%s\n", buf);
Câu 10. Khi dùng printf("%s%s%s%s%s") không có tham số, điều gì xảy ra?
- A. Không có gì xảy ra, in ra chuỗi rỗng
- B. Compiler báo lỗi
- C. printf đọc giá trị từ stack, xem là địa chỉ, đọc chuỗi tại đó → có thể crash
- D. printf in ra địa chỉ của stack
Câu 11. Input "AAAA" + "%08x." * 10 nhằm mục đích gì?
- A. Crash chương trình
- B. Đọc 40 byte dữ liệu từ stack và tìm vị trí của chuỗi “AAAA” (0x41414141) trong output
- C. Ghi đè return address
- D. Leak địa chỉ heap
Câu 12. Format specifier %hh chỉ định độ dài bao nhiêu byte?
- A. 4 bytes
- B. 2 bytes
- C. 1 byte
- D. 8 bytes
Câu 13. printf("%3$d", 1, 2, 3) in ra kết quả gì?
- A.
1 - B.
2 - C.
3 - D.
123
Câu 14. Tại sao kỹ thuật %N$x hữu ích hơn dùng nhiều %x?
- A. Nhanh hơn về mặt hiệu năng CPU
- B. Truy xuất trực tiếp vị trí trên stack mà không cần nhiều specifier trung gian, tiết kiệm không gian payload
- C. Ít gây crash hơn
- D. Chỉ hoạt động trên Windows
Câu 15. Trong kỹ thuật đọc địa chỉ tùy ý bằng format string, vai trò của %x trung gian là gì?
- A. In giá trị hex để debug
- B. “Nhảy qua” các giá trị trên stack để đến đúng vị trí địa chỉ cần đọc
- C. Kiểm tra ASLR
- D. Tạo ra heap spray
Câu 16. Tại sao printf(buf) nguy hiểm hơn printf("%s", buf)?
- A.
printf(buf)chậm hơn - B. Khi
bufdo user kiểm soát, user có thể nhúng format specifier để đọc/ghi stack tùy ý - C.
printf("%s", buf)không in được ký tự đặc biệt - D.
printf(buf)tốn nhiều bộ nhớ hơn
Câu 17. snprintf so với sprintf an toàn hơn vì:
- A.
snprintfnhanh hơn - B.
snprintfcó tham số giới hạn kích thước output, tránh ghi vượt buffer - C.
snprintfkhông hỗ trợ format specifier - D.
snprintftự động thoát ký tự đặc biệt
Câu 18. Biểu diễn two’s complement của -1 với 8-bit là?
- A.
1000 0001 - B.
1111 1110 - C.
1111 1111 - D.
0000 0001
Câu 19. Kết quả của printf("%.2f", 5.6732) là gì?
- A.
5.6732 - B.
5.67 - C.
5.70 - D.
5.6
Câu 20. Integer Overflow có thể dẫn đến hậu quả nào trong thực tế?
- A. Chỉ gây crash, không khai thác được
- B. Bypass kiểm tra điều kiện, cấp phát vùng nhớ sai kích thước, dẫn đến RCE
- C. Chỉ ảnh hưởng đến hiệu năng
- D. Chỉ xảy ra trên kiến trúc 32-bit
Câu 21. %#010x in số 3735928559 ra kết quả gì?
- A.
3735928559 - B.
deadbeef - C.
0xdeadbeef - D.
00deadbeef
Câu 22. Khi khai thác format string để ghi giá trị lớn (ví dụ 0xdeadbeef), kỹ thuật nào hiệu quả hơn?
- A. In 0xdeadbeef ký tự trước
%n - B. Ghi từng byte hoặc 2 byte với
%hhn/%hnvà địa chỉ riêng cho từng phần - C. Dùng nhiều
%nliên tiếp - D. Không thể ghi giá trị lớn
Câu 23. Hàm gets() bị coi là nguy hiểm vì:
- A. Chậm hơn
fgets - B. Không giới hạn độ dài đọc, đọc đến newline hoặc EOF → buffer overflow
- C. Không đọc được ký tự đặc biệt
- D. Chỉ đọc được 255 ký tự
Câu 24. Stack protector (-fstack-protector) bảo vệ bằng cách nào?
- A. Ngẫu nhiên hóa địa chỉ stack
- B. Đặt canary value giữa buffer và saved return address, kiểm tra trước khi return
- C. Mã hóa toàn bộ stack frame
- D. Chặn tất cả write operation vào stack
Câu 25. ASLR (Address Space Layout Randomization) bảo vệ khỏi điều gì?
- A. Integer overflow
- B. Kẻ tấn công hardcode địa chỉ cố định (libc, stack, heap) trong payload
- C. Format string vulnerability
- D. Logic bug
Câu 26. Tại sao format string attack có thể leak địa chỉ bộ nhớ kể cả khi ASLR bật?
- A. ASLR không bảo vệ stack
- B. Dùng
%xđọc giá trị thực tế trên stack, bao gồm con trỏ trỏ vào libc/stack đang chạy - C. ASLR bị tắt khi dùng format string
- D. Không thể leak với ASLR
Câu 27. long trong C theo bảng slide có kích thước bao nhiêu byte?
- A. 2 bytes
- B. 4 bytes
- C. 8 bytes
- D. Tùy platform
Câu 28. %s trong format string đọc dữ liệu như thế nào?
- A. Đọc 4 byte và in dưới dạng ký tự
- B. Lấy 4 byte từ stack làm địa chỉ con trỏ, đọc chuỗi tại đó đến ký tự NULL
- C. Đọc 1 byte tại vị trí ESP
- D. In địa chỉ ESP hiện tại
Câu 29. Điều kiện cần để khai thác format string vulnerability là gì?
- A. Phải có quyền root
- B. Người dùng phải kiểm soát được nội dung của chuỗi format string truyền vào hàm printf
- C. Chương trình phải chạy trên Linux
- D. Stack phải thực thi được (NX off)
Câu 30. Khi nói “đọc dữ liệu trên stack” bằng format string, tối đa đọc được bao nhiêu byte nếu buffer là 100 byte và mỗi %08x. dài 9 byte?
- A. 100 bytes
- B. Khoảng 36 bytes (4 word từ (100-4)/9 ≈ 10 specifier)
- C. Không giới hạn
- D. 40 bytes (10 word × 4 byte)
Câu 31. Lỗ hổng CVE-2022-1215 được tìm thấy trong thư viện nào?
- A. OpenSSL
- B. libinput
- C. glibc
- D. libpng
Câu 32. Two’s complement được sử dụng để biểu diễn số âm vì lý do gì?
- A. Đơn giản hơn sign-magnitude
- B. Phép cộng và trừ hoạt động đồng nhất không cần xét dấu riêng, và chỉ có một biểu diễn cho số 0
- C. Biểu diễn được số lớn hơn
- D. Dễ đọc hơn cho con người
Câu 33. scanf("%s", buf) khác fgets(buf, n, stdin) ở điểm nào quan trọng về bảo mật?
- A.
scanfnhanh hơn - B.
scanf("%s")không giới hạn độ dài, đọc đến whitespace → buffer overflow;fgetsgiới hạn n byte - C.
fgetskhông đọc được khoảng trắng - D.
scanfmã hóa input
Câu 34. Khi phân tích output của AAAA%08x.%08x...%08x, ý nghĩa của việc 41414141 xuất hiện ở vị trí thứ N là gì?
- A. Đây là địa chỉ của biến
a - B. Buffer
bufnằm ở vị trí tham số thứ N+1 của printf (tính cả tham số format string) - C. Stack bị hỏng
- D. ASLR bị tắt
Câu 35. Tại sao cần đặt địa chỉ cần đọc/ghi vào đầu payload trong format string attack?
- A. Vì printf đọc từ trái sang phải
- B. Vì địa chỉ cần nằm trong vùng stack mà printf sẽ “thấy” khi xử lý specifier
- C. Vì địa chỉ phải là byte đầu tiên của file
- D. Không cần thiết phải ở đầu
Câu 36. CWE-134 mô tả loại lỗ hổng nào?
- A. Buffer overflow
- B. Use of Externally-Controlled Format String
- C. Integer overflow
- D. NULL pointer dereference
Câu 37. Tại sao %n bị coi là nguy hiểm đặc biệt?
- A. Vì nó làm chậm chương trình
- B. Vì nó là specifier duy nhất ghi vào bộ nhớ thay vì chỉ đọc
- C. Vì nó làm crash chương trình ngay lập tức
- D. Vì nó không được hỗ trợ trên Linux
Câu 38. Trong format string attack, mục tiêu ghi đè thường là gì?
- A. Code section của program
- B. GOT (Global Offset Table) entry, return address, function pointer
- C. CPU registers trực tiếp
- D. Kernel memory
Câu 39. Để phòng chống format string vulnerability, biện pháp nào hiệu quả nhất?
- A. Dùng ASLR
- B. Luôn dùng format string cố định (
printf("%s", input)thay vìprintf(input)) - C. Tắt tính năng
%n - D. Kiểm tra độ dài input
Câu 40. Lỗ hổng IBM Spectrum Scale CVE-2021-29740 thuộc loại gì và mức độ nghiêm trọng?
- A. Integer overflow, Medium
- B. Format string, High
- C. Buffer overflow, Critical
- D. Use-after-free, Low
Câu 41. Khi khai thác buffer overflow trong hàm test(), phần tử nào trên stack frame bị ghi đè để kiểm soát luồng thực thi?
- A. Biến
atrongmain() - B. Saved return address trong stack frame của
test() - C. Stack pointer (ESP)
- D. Code segment register (CS)
Câu 42. -fno-stack-protector tắt cơ chế bảo vệ nào?
- A. ASLR
- B. NX bit
- C. Stack canary
- D. PIE (Position Independent Executable)
Câu 43. Số lượng CVE liên quan đến format string vulnerability theo slide là bao nhiêu?
- A. Khoảng 100
- B. Khoảng 751
- C. Khoảng 50
- D. Khoảng 2000
Câu 44. double trong C có kích thước bao nhiêu byte và format string nào?
- A. 4 bytes,
%f - B. 8 bytes,
%lf - C. 4 bytes,
%lf - D. 10 bytes,
%lf
Câu 45. Tại sao integer overflow đặc biệt nguy hiểm trong code kiểm tra điều kiện an toàn?
- A. Vì điều kiện if bị xóa khỏi bộ nhớ
- B. Giá trị tràn thành số nhỏ hơn MAX → vượt qua kiểm tra
if (size > LIMIT)dù thực sự rất lớn - C. Chỉ nguy hiểm khi debug mode
- D. Integer overflow không ảnh hưởng đến if statement
Câu 46. vprintf khác printf ở điểm nào?
- A.
vprintfnhanh hơn - B.
vprintfnhận danh sách tham số dạngva_listthay vì variadic args trực tiếp - C.
vprintfkhông hỗ trợ%n - D.
vprintfchỉ in ra stderr
Câu 47. Để đọc nội dung tại địa chỉ 0xffffcfbe bằng format string, payload phải bao gồm gì?
- A. Chuỗi hex
ffffcfbe+%s - B. 4 byte
\xbe\xcf\xff\xff(little-endian) ở đầu, rồi đủ%xtrung gian, rồi%s - C.
%s+ địa chỉ ở cuối - D. Chỉ cần
%slà đủ
Câu 48. Tại sao format string attack vẫn xuất hiện nhiều trong thực tế dù đã biết từ lâu?
- A. Không có cách phòng chống
- B. Lập trình viên tiếp tục dùng
printf(user_input)do thiếu hiểu biết hoặc code legacy - C. Compiler không thể phát hiện
- D. Chỉ ảnh hưởng đến ngôn ngữ C cũ
Câu 49. Trong bảng 2’s complement 8-bit, giá trị nào có thể biểu diễn mà sign-magnitude không thể biểu diễn duy nhất?
- A. +127
- B. -128
- C. 0
- D. -0
Câu 50. Phương pháp nào trong số sau đây không phải là phương pháp khai thác format string vulnerability?
- A. Crash chương trình bằng
%s%s%s... - B. Đọc stack bằng
%x%x%x... - C. Ghi đè bộ nhớ bằng
%n - D. Tăng stack size bằng
alloca()
Câu 51. Với hệ thống 32-bit, mỗi %x trong format string attack đọc bao nhiêu byte từ stack?
- A. 1 byte
- B. 2 bytes
- C. 4 bytes
- D. 8 bytes
Câu 52. Nếu input bị giới hạn 100 byte và ta dùng cú pháp %N$x thay vì nhiều %x, lợi ích là gì?
- A. Tốc độ nhanh hơn 10 lần
- B. Payload ngắn hơn nhiều, đọc được tham số ở vị trí xa hơn trong giới hạn input
- C. Tránh được ASLR
- D. Không có sự khác biệt