Ứng dụng biến môi trường LD_PRELOAD trong Linux

Vimentor Admin
Vimentor Admin
12 bài viết — 8 Người theo dõi |
0
0

Giới thiệu

Đôi khi trong lúc sử dụng các ứng dụng trong Linux, có những trường hợp bạn muốn thay đổi một tính năng nào đó của ứng dụng để nó hoạt động theo ý muốn của mình, hoặc muốn tìm hiểu hay giám sát hoạt động của chương trình đó. Cách thông thường nhất (trong trường tốt nhất), đó là sửa mã nguồn của chương trình theo mục đích của mình. Tuy nhiên, trong nhiều trường hợp mã nguồn không có sẵn, ví dụ như chỉ có chương trình đã được biên dịch hay file thực thi hay không đủ thông tin để biên dịch lại từ mã nguồn.

Một trong những kĩ thuật có thể sử dụng trong trường hợp này đó là can thiệp vào hoạt động của Linux để “cài cắm” những đoạn code do mình viết vào một chương trình có sẵn. Việc này không đòi hỏi bạn có mã nguồn của chương trình đó mà vẫn có thể thay đổi, tìm hiểu hay giám sát hoạt động của nó. Trong Linux, ta có thể sử dụng biến môi trường LD_PRELOAD cho mục đích này.

LD_PRELOAD là gì

Shared library và thực thi ứng dụng trong Linux

Trước khi giới thiệu biến môi trường LD_PRELOAD là gì và sử dụng nó như thế nào, ta cần hiểu sơ lược về cách một chương trình ứng dụng được biên dịch, được nạp và được khởi chạy như thế nào trong môi trường Linux. Đây cũng là một kiến thức hữu dụng cần biết khi lập trình ứng dụng trên Linux.

Ví dụ, chúng ta có một chương trình Hello World viết bằng C. Nội dung của chương trình này, như mọi chương trình Hello World khác, sử dụng hàm puts() để in dòng chữ “Hello World!” ra màn hình.

#include <stdio.h>
int main()
{
    puts("Hello world!\n");
    return 0;
}

Sử dụng GCC để biên dịch code này thành một file thực thi hello.o:

$ gcc –Wall hello.c –o hello.o

Để chạy chương trình này, ta thường thực hiện lệnh như sau:

$ ./hello.o

Hello World!

Làm thế nào để Linux nạp và thực thi hello.o khi thực hiện câu lệnh như trên?

Thông thường, một ứng dụng sẽ sử dụng các hàm hay code được cung cấp bởi các library (thư viện), đặc biệt là shared library (hay thư viện liên kết động). Chương trình hello.c đơn giản ở trên ít nhất đã dùng hàm puts() của thư viện libc. Trong quá trình biên dịch để tạo ra file thực thi, phần code trong shared library sẽ không được include vào trong file thực thi. Có nghĩa là trong file hello.o không hề chứa phần code định nghĩa hàm puts(). Thay vào đó, những phần code đó sẽ được hệ thống nạp vào tại thời điểm chương trình được thực thi, khi cần thiết. Để xem một file thực thi sử dụng những shared library nào, ta có thể dùng công cụ ldd:

$ ldd hello.o

        linux-gate.so.1 =>  (0x00735000)

        libc.so.6 => /lib/libc.so.6 (0x0093b000)

        /lib/ld-linux.so.2 (0x00919000)

 

Ta có thể thấy danh sách các shared library mà chương trình hello.o cần dùng tới, trong đó có libc. Danh sách này được xác định ở compile time dựa trên mã nguồn  của chương trình.

Khi chương trình được thực thi, một thành phần khác trong hệ thống Linux, gọi là dynamic linker/loader (bộ liên kết động) sẽ nạp những thư viện động mà chương trình đó cần sử dụng và map code của các thư viện vào không gian địa chỉ của process mà chương trình đó tạo ra. Khi đó, chương trình của chúng ta có thể gọi tới phần code được implement trong các thư viện. Quá trình này được thực hiện ở run time. Hình 1 mô tả các vùng nhớ trong không gian địa chỉa bộ nhớ của một process, với các thư viện chia sẻ được map vào vùng memory mapping segment:

Hình 1: Sơ lược các phân vùng bộ nhớ trong không gian địa chỉ của một process (trong Linux)

Ví dụ với chương trình hello.o thì tại run time, sau khi libc được map vào không gian địa chỉ của process, phần code định nghĩa hàm puts() trong libc có thể được gọi tới và thực thi.

Biến môi trường LD_PRELOAD

Như đã đề cập ở trên, dynamic loader là thành phần trong Linux thực hiện việc tìm kiếm và nạp các shared library cần thiết cho chương trình của chúng ta. Thứ tự các library được load do bộ loader thực hiện tùy theo trường hợp.

Trong Linux, biến môi trường LD_PRELOAD là một biến chứa đường dẫn tới các shared library. Những thư viện này sẽ được dynamic loader nạp vào trước khi nạp bất kì thư viện nào khác, kể cả thư viện libc.

Việc một thư viện được nạp vào trước giúp cho những hàm chứa trong thư viện này được tìm tới và sử dụng trước, thay cho những hàm có trùng tên ở trong các thư viện được nạp sau nó (nếu có). Điều này có nghĩa là ta có thể thực hiện việc intercept hay overwrite các hàm thư viện có sẵn bằng các hàm do chính mình tự viết. Ví dụ, bạn có thể tự viết một phiên bản hàm puts() của riêng mình và dùng nó thay cho hàm puts() có sẵn trong libc để mỗi lần hàm này được sử dụng, nội dung của đoạn text sẽ được log vào một file nào đó chẳng hạn. Khả năng intercept hay overwrite này đưa tới nhiều ứng dụng khác hay ho.

Cụ thể cách sử dụng biến môi trường này như thế nào sẽ được trình bày kèm luôn trong phần ví dụ.

Ví dụ

Ví dụ 1

Trong ví dụ 1, ta sẽ sử dụng LD_PRELOAD cho một mục đích đơn giản: làm cho hàm sinh số ngẫu nhiên rand() luôn trả về một giá trị.

Ta có một chương trình trong file rand_num.c có sử dụng hàm rand() để tạo 10 số ngẫu nhiên liên tiếp như sau:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main()
{
    srand(time(NULL));
    int i = 10;

    while(i--)
        printf("%d\n", rand()%100);

    return 0;
}

Hãy biên dịch và chạy thử để xem output của chương trình:

$ gcc -Wall rand_num.c -o rand_num

$ ./rand_num

79

31

11

17

16

55

61

24

3

81

Bây giờ ta định nghĩa hàm rand()mới trong một file librandom.c như sau:

int rand(void)
{
    return 42;
}

Đoạn code này rất đơn giản. Ta định nghĩa một hàm rand() với prototype giống với hàm rand() của libc. Hàm rand() này luôn trả về một số “ngẫu nhiên” là 42.

Như đã biết, LD_PRELOAD là đường dẫn tới các file shared library. Vì vậy, ta sẽ biên dịch file librandom.c thành một file shared library với câu lệnh như sau:

$ gcc -shared -fPIC librandom.c -o librandom.so

Sử dụng thư viện mới này cho chương trình rand_num bằng cách gán giá trị cho LD_PRELOAD và chạy câu lệnh như sau:

$ LD_PRELOAD=$PWD/librandom.so ./rand_num

42

42

42

42

42

42

42

42

42

42

Tất cả các số “ngẫu nhiên” được sinh ra bây giờ đều là 42!

Trong câu lệnh trên, ta gán giá trị cho LD_PRELOAD trỏ đến librandom.so ngay trước khi thực thi chương trình. Như vậy, librandom.so sẽ được nạp trước libc và hàm rand() trong librandom.so sẽ được dùng thay cho hàm rand() của libc.

Với cách sử dụng như trên, giá trị của LD_PRELOAD chỉ có tác dụng trong phạm vi một câu lệnh đó. Nếu sau đó ta chạy lại chương trình rand_num một cách bình thường thì các số ngẫu nhiên sẽ lại được sinh ra như bình thường. Nếu muốn áp dụng giá trị LD_PRELOAD cho các lần thực thi sau đó mà không cần set lại, ta có thể dùng lệnh export:

$ export LD_PRELOAD=$PWD/librandom.so

Trong ví dụ này, ta đã thấy được cách sử dụng LD_PRELOAD và tác dụng của nó tới hành vi của một chương trình có sẵn mà không cần thay đổi mã nguồn của của chương trình đó.

Trong một số trường hợp khác, bạn có thể không muốn làm cho output của một chương trình bị thay đổi hoặc thay đổi một cách rõ ràng như trong ví dụ trên, mà muốn thực hiện các hành động một cách “âm thầm” và “trong suốt” hơn. Cùng xem ví dụ 2.

Ví dụ 2

Bạn có thể “thay thế” một hàm thư viện có sẵn bằng một hàm của mình, như hàm rand() trong ví dụ 1, nhưng điều tuyệt vời hơn nữa là bạn vẫn có thể gọi tới hàm gốc để thực hiện công việc gốc một cách bình thường.

Giả sử ta muốn log lại các hành động mở file bằng hàm open() mà một chương trình thực hiện. Ta sẽ định nghĩa một hàm open() mới, tương tự như cách đã làm trong ví dụ 1. Tuy nhiên, để không ảnh hưởng tới hoạt động thực tế của chương trình đó, ta vẫn cần gọi tới open() gốc để mở file như thông thường.

Giả sử một chương trình đơn giản sử dụng hàm open() như dưới đây (testopen.c):

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main()
{
    int fd;
    char msg[] = "Hello World!\n";

    fd = open("testopen.txt", O_RDWR | O_CREAT, 0666);
    if (fd != -1)
    {
        write(fd, msg, sizeof(msg));
        close(fd);
    }
    else
    {
        perror("failed: ");
    }

    return 0;
}

Định nghĩa hàm open() của mình trong file logopen.c:

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/stat.h>
#include <dlfcn.h>

int open(const char *pathname, int flags, mode_t mode)
{
    int (*real_open)(const char *, int, mode_t);
    printf("The program uses open() to access '%s' with flags %d\n",
            pathname, flags);
    real_open = dlsym(RTLD_NEXT, "open");
    return real_open(pathname, flags, mode);
}

Cùng tìm hiểu kỹ đoạn code trên:

  • Dòng 1: định nghĩa macro _GNU_SOURCE. Macro này cho phép ta có thể sử dụng các tính mở rộng của GNU/Linux. Cần định nghĩa macro này trước khi include file dlfcn.h để có thể sử dụng tham số RTLD_NEXT cho hàm dlsym() (xem phần dưới).
  • Dòng 5: khai báo hàm open() mới.
  • Dòng 7: khai báo một con trỏ hàm trỏ tới một hàm mà có kiểu của tham số và kiểu trả về giống với hàm open() gốc. Mục đích của con trỏ hàm này chính là để trỏ tới hàm open() gốc của libc, mà sau đó ta có thể sử dụng nó để gọi hàm open() gốc giúp thực hiện chức năng mở file như thường.
  • Dòng 8: in ra màn hình thông tin log mà ta cần.
  • Dòng 10: Dùng hàm dlsym() để gán giá trị cho con trỏ hàm mà ta đã khai báo. Hàm dlsym() trả về địa chỉ của một symbol trong các thư viện được nạp. Trong trường hợp này, ta truyền 2 tham số cho hàm dlsym(). Tham số đầu tiên RTLD_NEXT có nghĩa là tìm symbol tiếp theo trong các thư viện sau thư viện hiện tại, tham số thứ 2 chính là tên symbol cần tìm, ở đây chính là hàm “open”. Như vậy, hàm dlsym() sẽ trả về địa chỉ của hàm open() gốc trong thư viện libc.
  • Dòng 11: gọi hàm open() gốc thông qua con trỏ hàm và trả về giá trị như thông thường.

Thực hiện biên dịch file logopen.c thành thư viện tương tự như trong ví dụ 1. Lưu ý: cần biên dịch với cờ -ldl để sử dụng hàm dlsym().

$ gcc -shared -fPIC -ldl logopen.c  -o logopen.so

Gán giá trị cho LD_PRELOAD trỏ đến logopen.so và chạy chương trình, ta sẽ thấy dòng log được in ra màn hình:

$ LD_PRELOAD=/home/manhnt/code/LD_PRELOAD/logopen.so ./testopen

The program uses open() to access 'testopen.txt' with flags 66

Kết luận

Với 2 ví dụ đơn giản như trên, chúng ta đã thấy được sự đơn giản trong cách sử dụng nhưng đưa lại những hiệu quả rất thú vị của biến LD_PRELOAD này. Dựa trên đó, các bạn có thể đưa ra những ý tưởng mới trong việc debug hay tìm hiểu, giám sát hoạt động của một chương trình trong Linux.

Mẹo dùng LD_PRELOAD này không sử dụng được trong trường hợp static library (thư viện tĩnh), tuy nhiên trong hầu hết các trường hợp, các ứng dụng thường sử dụng tới shared library, do vậy khả năng ứng dụng vẫn là phổ biến.

Các bạn có thể tham khảo thêm một số project mã nguồn mở ứng dụng LD_PRELOAD để có thêm ý tưởng cho mình:

  • Failmalloc: thư viện mô phỏng việc cấp phát bộ nhớ động không thành công, dùng để kiếm khả năng xử lý lỗi của ứng dụng. Link: http://www.nongnu.org/failmalloc/
  • segv_handler: hỗ trợ xử lý lỗi segmentation fault và in ra thông tin backtrace khi gặp lỗi này. Link: https://github.com/tridge/junkcode/tree/master/segv_handler
  • netjail: kiểm soát việc truy cập network của các ứng dụng, hỗ trợ phát hiện phần mềm hoạt động lén lút hoặc spyware. Link: http://netjail.sourceforge.net/

Chúc các bạn thành công!

** Nếu bạn muốn viết các nội dung đặt biệt thì hãy làm theo hướng dẫn sau

Xem thêm 10 bình luận
Viết blog mới của bạn
Báo lỗi trang
Đang tải