I. Giới thiệu
Andressanitizer là một plugin của trình biên dịch giúp developers phát hiện các vấn đề về bộ nhớ trong mã nguồn. Để phát hiện các vấn đề đó, Asan thực hiện chèn các checker xung quanh các vùng nhớ được truy cập trong quá trình biên dịch và sẽ làm chương trình dừng lại nếu phát hiện việc truy cập bộ nhớ không đúng. ASan được sử dụng rộng rãi trong quá trình fuzzing nhờ khả năng phát hiện lỗi mà qua các testing unit có thể bỏ sót, và có hiệu năng tốt hơn so với các công cụ tương tự.
ASan được thiết kế cho C và C++, nhưng cũng có thể sử dụng với Objective-C, Rust, Go, và Swift. Report này sẽ tập trung vào C và sẽ hướng dẫn cách sử dụng ASan, giải thích các lỗi mà nó báo, hiểu rõ các nguyên lý triển khai, và nêu lên những hạn chế cũng như các lỗi thường gặp của ASan, giúp người đọc phát hiện những lỗi chưa từng được phát hiện trước đây.
So với các công cụ khác như Valgrind, ASan có cách tiếp cận khác. Valgrind có thể chạy mà không cần biên dịch lại từ mã nguồn, nhưng đổi lại hiệu năng phải cao hơn (hiệu năng giảm 20 lần so với 2 chỉ lần của ASan) và cũng có thể phát hiện ít lỗi hơn.
Một số điểm mạnh và yếu của ASAN
AddressSanitizer (ASan) là một công cụ mạnh mẽ với nhiều ưu điểm vượt trội so với các công cụ phát hiện lỗi bộ nhớ khác. Điểm nổi bật của ASan là hiệu năng cao, với mức giảm hiệu năng chỉ khoảng 2 lần, thấp hơn nhiều so với các công cụ như Valgrind, thường làm chậm chương trình tới 20 lần. Khả năng tích hợp tốt vào quy trình phát triển, nhờ việc ASan là một phần của các trình biên dịch phổ biến như LLVM Clang và GCC, giúp ASan trở thành lựa chọn ưu tiên trong phát hiện lỗi bộ nhớ ngay từ giai đoạn phát triển. Thêm vào đó, ASan liên tục được cộng đồng phát triển và cập nhật, đặc biệt là trong các phiên bản LLVM gần đây, giúp nó có khả năng mở rộng hỗ trợ các cấu trúc dữ liệu chuẩn như vector và deque trong C++. ASan cũng hỗ trợ nhiều ngôn ngữ lập trình ngoài C/C++, bao gồm Objective-C, Rust, Go, và Swift, giúp các nhóm phát triển đa ngôn ngữ dễ dàng áp dụng.
Tuy nhiên, ASan cũng tồn tại một số hạn chế khi so sánh với các công cụ tương tự. Đầu tiên, ASan yêu cầu mã nguồn phải được biên dịch lại với các flag đặc biệt để hoạt động, trong khi Valgrind có thể kiểm tra trực tiếp các chương trình đã biên dịch. Ngoài ra, ASan không phát hiện được toàn bộ các lỗi bộ nhớ, đặc biệt là các vấn đề về rò rỉ bộ nhớ nếu không kết hợp với các công cụ bổ trợ như LeakSanitizer. ASan còn phụ thuộc vào các bản cập nhật của trình biên dịch; những ai sử dụng các phiên bản trình biên dịch cũ hoặc không đầy đủ hỗ trợ có thể gặp khó khăn khi triển khai ASan. Cuối cùng, do cần thêm bộ nhớ để lưu trữ thông tin kiểm tra, ASan yêu cầu dung lượng bộ nhớ lớn hơn, có thể gây hạn chế trong các hệ thống tài nguyên thấp.
II. Kiến trúc của ASAN
ASan được xây dựng dựa trên hai khái niệm chính: shadow memoryvà redzones. Bộ nhớ bóng là một vùng bộ nhớ chuyên dụng lưu trữ meta data về bộ nhớ của ứng dụng. Redzones là các vùng bộ nhớ đặc biệt được đặt xen giữa các đối tượng trong bộ nhớ (ví dụ: các biến trên stack hoặc các phân bổ trên heap) để ASan có thể phát hiện các hành vi truy cập bộ nhớ ngoài phạm cho phép.
1. Shadow memory

Các giá trị trong shadow memory giúp xác định liệu một vùng bộ nhớ (granule) có thể được truy cập hoàn toàn, truy cập một phần, hoặc không nên được truy cập. Nếu một vùng bị đánh dấu là không nên truy cập, nó sẽ được gọi là poisoned, và giá trị của shadow memory cho biết lý do tại sao ASan cho rằng vùng đó không hợp lệ. ASan sẽ hiển thị bảng chú giải này cùng với các báo cáo lỗi.

Cấu trúc của shadow byte legend
AddressSanitizer (ASan) sử dụng shadow memory để giám sát và kiểm tra tính hợp lệ của các thao tác truy cập bộ nhớ trong chương trình. Mỗi vùng nhớ trong chương trình được ánh xạ sang một byte trong shadow memory, với các giá trị khác nhau biểu thị trạng thái của vùng nhớ đó. Chi tiết:
- Truy cập hợp lệ:
- Nếu một vùng nhớ có thể truy cập hoàn toàn, giá trị shadow byte tương ứng sẽ là
00. - Nếu vùng đó chỉ được phép truy cập một phần (ví dụ: buffer có kích thước nhỏ hơn 8 byte), shadow byte sẽ lưu số byte có thể truy cập (giá trị từ
01đến07).
- Nếu một vùng nhớ có thể truy cập hoàn toàn, giá trị shadow byte tương ứng sẽ là
- Bị đánh dấu không hợp lệ (Poisoned):
- Khi một vùng nhớ không được phép truy cập, shadow memory sẽ gán các giá trị đặc biệt để chỉ rõ lý do.
- Ví dụ:
fd: Vùng đã được giải phóng (freed heap), không nên truy cập trừ khi được cấp phát lại.f8: Lỗi “stack use after scope” (truy cập biến ngoài phạm vi hợp lệ).f7: Bộ nhớ bị lỗi hoặc cố ý đánh dấu bởi người dùng (poisoned by user).fa,f1,f3: Các vùng bảo vệ stack nhằm phát hiện lỗi tràn bộ nhớ.
- Ứng dụng của Shadow Memory:
- Shadow memory hỗ trợ phát hiện các lỗi nghiêm trọng như:
- Use-after-free: Truy cập vào bộ nhớ đã được giải phóng.
- Buffer overflow/underflow: Vượt quá kích thước hợp lệ của vùng nhớ.
- Truy cập ngoài phạm vi: Thao tác với biến stack hoặc heap không hợp lệ.
- Shadow memory hỗ trợ phát hiện các lỗi nghiêm trọng như:
- Vùng truy cập một phần:
- Các vùng truy cập một phần thường gặp khi:
- Bộ đệm trên heap nhỏ hơn 8 byte.
- Biến trên stack có kích thước nhỏ hơn 8 byte.
- Các vùng truy cập một phần thường gặp khi:
Bảng chú giải giá trị shadow byte mà ASan cung cấp trong các báo cáo lỗi giúp lập trình viên nhanh chóng xác định nguyên nhân và loại lỗi bộ nhớ, từ đó cải thiện chất lượng và bảo mật cho chương trình.
2. Redzones
Các redzones là các vùng nhớ đặc biệt được chèn vào bộ nhớ của quá trình để đóng vai trò như các vùng đệm thực hiện ngăn cách các đối tượng khác nhau trong bộ nhớ bằng các byte đã bị “đánh dấu” (poisoned) để không cho phép truy cập. Việc này giúp phát hiện các lỗi truy cập ngoài phạm vi vì các vùng redzone này sẽ báo lỗi khi có sự xâm phạm. Do đó, khi biên dịch chương trình với ASan, bố trí bộ nhớ sẽ thay đổi.
Ví dụ, trong đoạn mã sau, ba biến được khai báo trên stack: buf là mảng chứa sáu phần tử mỗi phần tử có 2 byte, a có 2 byte, và b có 1 byte:

Chương trình lỗi
Trong đoạn mã trên, biến buf[10] truy cập ra ngoài phạm vi của mảng buf. Khi chạy chương trình với ASan, công cụ này phát hiện rằng việc truy cập đã va chạm vào vùng “stack right redzone”, được biểu thị trong shadow memory bằng ký hiệu [f3].

Báo cáo lỗi từ ASAN
Trong đó:
- 01 tương ứng với biến b, 02 tương ứng với biến a, và 00 04 tương ứng với mảng buf.
- [f3] là redzone của “stack right” đã bị vi phạm.
Với ASan, các vùng redzone được thêm vào giữa các biến trên stack để tạo khoảng cách an toàn. Thông thường, nếu không có ASan, các biến như a, b, và buf sẽ được sắp xếp sát nhau trong bộ nhớ. Tuy nhiên, ASan thêm các redzone giữa các biến này và cũng thêm padding trước và sau chúng. Điều này giúp phát hiện các truy cập vượt quá giới hạn cho phép.
Lưu ý: Redzone không được thêm vào giữa các phần tử trong mảng hoặc giữa các phần tử trong struct, vì điều này có thể phá vỡ tính toàn vẹn của cấu trúc dữ liệu và các chương trình phụ thuộc vào sự liên tiếp trong bộ nhớ của các phần tử.

Ánh xạ từ ví dụ
Khi không sử dụng ASan, các biến như a, b, và buf sẽ thường nằm sát nhau trong bộ nhớ, không có khoảng đệm giữa chúng. Tuy nhiên, khi biên dịch với ASan, các vùng redzones được chèn vào giữa các biến và trước/sau các biến này để tạo ra khoảng cách. Điều này xảy ra vì các biến cần phải có khả năng truy cập một phần (partially addressable), và các vùng redzone giúp phát hiện các truy cập ngoài phạm vi cho phép
III. Cách thức hoạt động và cách ASAN thêm các đoạn mã vào chương trình
ASan sử dụng shadow memory để theo dõi trạng thái của từng đoạn bộ nhớ trong chương trình.
- Granule – một đơn vị bộ nhớ nhỏ nhất được ánh xạ của bộ nhớ (8 byte) được ánh xạ (mapped) đến một byte trong shadow memory.
- Giá trị trong shadow memory:
- 0: Cả granule có thể được truy cập hợp lệ.
- Số dương: Một phần granule có thể truy cập (dựa vào số byte được phép).
- Số âm: Granule bị “poisoned”, không thể truy cập.
Quá trình Instrumentation (thêm mã kiểm tra vào chương trình) trong quá trình biên dịch.
Ví dụ với chương trình C đơn giản sử dụng 8 bytes bộ nhớ khi sử dụng ASAN và không có ASAN

Assembly code của chương trình khi sử dụng ASAN và không sử dụng ASAN
Chúng ta có thể thấy với chương trình sử dụng ASAN thì có thêm các đoạn mã được chèn vào như phép chia 8 (granule size) để tính toán vị trí trong shadow memory
1200: 48 c1 ea 03 shr $0x3,%rdx
Cộng thêm offset vào shadow memory:
1204: 48 81 c2 00 80 ff 7f add $0x7fff8000,%rdx
Kiểm tra giá trị trong shadow memory:
120b: 0f b6 12 movzbl (%rdx),%edx
120e: 84 d2 test %dl,%dl
1210: 74 08 je 121a <touch+0x31>
- Lệnh movzbl tải giá trị byte tại địa chỉ %rdx vào thanh ghi %edx và test %dl,%dl kiểm tra xem giá trị này có bằng 0 không (trong trường hợp này, 0 có nghĩa là toàn bộ granule có thể truy cập).
- Nếu không bằng 0 (nghĩa là có lỗi trong truy cập bộ nhớ), chương trình nhảy tới lệnh gọi ASan để báo lỗi.
Nếu shadow memory không hợp lệ, gọi __asan_report_load8
1212: 48 89 c7 mov %rax,%rdi
1215: e8 86 fe ff ff callq 10a0 <__asan_report_load8@plt>
Đó là một vài thao tác cơ bản của ASAN thực hiện quá trình Instrumentation.
IV. Demo
Để hiểu rõ hơn về cách sử dụng ASAN cũng như cách ASAN phát hiện và hiển thị lỗi cho người dùng, chúng ta cùng đi đến một ví dụ đơn giản

Chương trình demo có lỗi truy cập phần tử ngoài mảng
Các bước thực hiện:
- Chạy dòng lệnh sau để biên dịch với tùy chọn debug và sử dụng ASAN:
gcc -g -fsanitize=address -o asan_example asan.c - Chạy chương trình: ./asan_example
- Kết quả khi chạy:

Kết quả khi chạy chương trình
- Phân tích kết quả:
- Kết quả từ AddressSanitizer (ASan) cho thấy một lỗi stack-buffer-overflow, tức là bạn đã truy cập vào một phần bộ nhớ nằm ngoài phạm vi mà chương trình đã cấp phát, gây ra việc truy cập bộ nhớ không hợp lệ.
- Địa chỉ lỗi: 0x7fffd39beb68 là địa chỉ bộ nhớ nơi xảy ra lỗi.
- Kích thước đọc: Đọc 4 byte tại địa chỉ đó.
- Địa chỉ trong mã nguồn: Lỗi xảy ra tại main trong chương trình tại dòng 15 (/home/bbb/WorkSpace/asan.c:15).
- Lý do lỗi

- Biến arr có kích thước 5 phần tử, mỗi phần tử có kích thước 4 byte (vì đây là mảng số nguyên int), tổng cộng là 5 * 4 = 20 byte.
- Tuy nhiên, tại offset 72,chúng ta đã truy cập vượt ra ngoài phạm vi mảng arr. Điều này có nghĩa là chúng ta đã cố gắng truy cập phần tử ngoài phạm vi của mảng (cụ thể là arr[10]).
- Shadow bytes: Đây là mô tả bộ nhớ “shadow” mà ASan sử dụng để theo dõi trạng thái bộ nhớ, giúp phát hiện các lỗi bộ nhớ.
- Vùng bộ nhớ được đánh dấu với f1 và f3 cho thấy đó là các vùng redzone của stack, nơi bộ nhớ đã bị ghi đè. [f3] cho thấy vị trí mà vùng nhớ stack đã bị truy cập không đúng.
V. Ứng dụng của ASAN
Asan là một công cụ mạnh mẽ giúp lập trình viên gỡ lỗi. Một vài ứng dụng chính của ASAN được sử dụng trong việc:
- Sử dụng bộ nhớ sau khi đã giải phóng (dangling pointer dereference)
- Tràn bộ nhớ heap (Heap buffer overflow)
- Tràn bộ nhớ stack (Stack buffer overflow)
- Tràn bộ nhớ toàn cục (Global buffer overflow)
- Sử dụng bộ nhớ sau khi hàm trả về (Use after return)
- Sử dụng bộ nhớ sau khi ra khỏi phạm vi (Use after scope)
- Lỗi thứ tự khởi tạo (Initialization order bugs)
- Rò rỉ bộ nhớ (Memory leaks)
- Use-after-free (UAF): Khi một vùng bộ nhớ được giải phóng nhưng vẫn tiếp tục được sử dụng, ASan sẽ phát hiện và báo lỗi. Điều này đặc biệt hữu ích khi kiểm tra các lỗi quản lý bộ nhớ trong các ứng dụng phức tạp.
VI. Tổng kết
AddressSanitizer (ASan) là một công cụ mạnh mẽ giúp phát hiện và sửa chữa các lỗi liên quan đến bộ nhớ trong các ứng dụng C/C++. Với khả năng phát hiện nhanh chóng các lỗi như tràn bộ đệm, sử dụng bộ nhớ sau khi giải phóng, và rò rỉ bộ nhớ, ASan đóng vai trò quan trọng trong việc nâng cao độ ổn định và bảo mật của phần mềm. Việc tích hợp ASan vào quy trình phát triển phần mềm sẽ giúp giảm thiểu nguy cơ gặp phải các lỗi nghiêm trọng liên quan đến bộ nhớ, đồng thời cải thiện hiệu suất của ứng dụng trong môi trường sản xuất.
Tuy nhiên, ASan cũng có một số hạn chế, chẳng hạn như việc làm tăng bộ nhớ sử dụng và ảnh hưởng đến hiệu suất trong quá trình chạy thử. Dù vậy, lợi ích mà ASan mang lại trong việc phát hiện lỗi sớm và đảm bảo tính an toàn của ứng dụng là vô cùng quan trọng và đáng giá. Việc sử dụng ASan nên trở thành một phần không thể thiếu trong quá trình kiểm tra và phát triển phần mềm hiện đại.

