T
tmnt53
Guest
Khai thác lỗi format string (phần I)
T
- tmnt53
- 5.838 Lượt xem
Ở phần trước mình đã giới thiệu lỗi format string, về khả năng leak dữ liệu và crash chương trình (Giới thiệu lỗi format string: https://whitehat.vn/forum/thao-luan/exploitation/62801-gioi-thieu-loi-format-string). Hôm nay, mình sẽ giới thiệu tiếp khả năng dùng lỗi format string để khai thác chương trình.
Các bạn nên có kiến thức cơ bản về assembly, biết lập trình assembly. Bài viết về lỗi tràn bộ đệm của bạn minhslyfox (https://whitehat.vn/forum/thao-luan/exploitation/61787-pwn-exploit-loi-tran-bo-dem-phan-1) viết khá chi tiết về stack, cách một hàm được gọi, và demo một cách khai thác lỗi tràn bộ đệm. Các bạn nên đọc và làm theo bài đó trước.
Study case: chương trình echoServer của phần format string trước
Mã nguồn + file chạy (và của các ví dụ liên quan):
Chương trình có chức năng in ra mọi thứ mà ta nhập vào, cho đến khi ta nhập exit. Ở đó, printf(buf) tạo nên lỗi format string:
Format string %n
%n là một dạng format đặc biệt, cho phép ta ghi lại số ký tự đã được in ra trước đó.
Ví dụ trong chương trình sau (compile: gcc format_dollar_sign.c -o format_dolloar_sign -m32):
Tại dòng 7, ta có %n với tham số tương ứng là &nChars. Printf in ra “hello”, độ dài 5, nên nChars sẽ được gán giá trị bằng 5, giống như dòng 8 in ra “nChars = 5”:
Tương tự, dòng số 9 in ra “A number 2000” có độ dài 13, nên nChars tương ứng với %n được gán bằng 13. Hay tại dòng 13, ta nhập vào buf = “1234567890”, nên nChars được gán bằng 10.
Format %n giúp ta ghi được nội dung tùy ý vào bộ nhớ.
Format string với $
Ngoài %n, $ là một ký tự format rất ít dùng nhưng lại hỗ trợ đắc lực khi khai thác format string.
$ giúp ta xác định tham số thứ mấy sẽ tương ứng với format đang xét.
Ví dụ (compile: gcc format_dollar_sign.c –o format_dolloar_sign –m32):
Khi chạy chương trình, ta sẽ có:
Tại dòng 6, hàm printf nhận %1[imath]s, thì nó tham chiếu tới tham số thứ 1 của hàm (ta coi xâu “hello %1[/imath]s
” là tham số thứ 0, thì “A” là tham số thứ 1), nên nó in ra “hello A”. Ở đây số 1 nằm trước $ chỉ số thứ tự của tham số.
Tương tự, %2$s tương ứng với “B”, nên dòng 7 in ra “hello B”.
Còn tại dòng thứ 8, 50 và 51 tương ứng với các tham số thứ 50, 51. Tuy trong code không có 2 tham số này, nhưng thực tế, nó sẽ ám chỉ các tham số thứ 50, 51 truyền vào stack, và đó chính là các giá trị dword tương ứng nằm tại vị trí 50, 51 trên stack. Rõ hơn, ta sẽ thấy trong gdb:
Mô tả: trong session gdb trên, mình đặt breakpoint tại lệnh call printf(“hello %50[imath]p %51[/imath]p”). x /2wx $esp+4*50 in ra 2 dword tại offset 4*50 trong stack (mỗi dword có kích thước 4byte), được các giá trị mà hàm printf sẽ in ra.
Các “con trỏ hàm” GOT (Global Offset Table)
Trong cấu trúc của file elf, có một vùng dùng để lưu địa chỉ của các hàm trong thư viện mà ta sẽ gọi tới, gọi là GOT. Ví dụ trong chương trình echoServer, ta gọi đến các hàm thư viện setvbuf, printf, fgets, strcmp, exit thì trong GOT sẽ có các con trỏ lưu địa chỉ của các hàm này. Cụ thể hơn như sau:
Trong chương trình echoServer, khi phải gọi tới một hàm nào đó trong thư viện libc (hay bất cứ thư viện chia sẻ nào khác), vd khi lần đầu tiên gọi printf, chương trình thực hiện như sau:
Như vậy có thể nói rằng, khi lần đầu gọi một hàm thư viện, thì chương trình thực hiện phân giải địa chỉ của hàm, và lưu nó vào trong GOT. Trong các lần gọi sau thì chương trình không phải phân giải nữa mà gọi trực tiếp hàm luôn qua GOT.
Bảng GOT khi xem trong IDA:
Vậy trong GOT có chứa địa chỉ của các hàm trong libc. Có thể coi đây là các con trỏ hàm.
Cùng với việc sử dụng $ và %n, ta sẽ thực hiện khai thác chương trình, bằng cách ghi đè các con trỏ hàm này. Mình sẽ tiếp tục bài khai thác trong phần tiếp theo.
Các bạn nên có kiến thức cơ bản về assembly, biết lập trình assembly. Bài viết về lỗi tràn bộ đệm của bạn minhslyfox (https://whitehat.vn/forum/thao-luan/exploitation/61787-pwn-exploit-loi-tran-bo-dem-phan-1) viết khá chi tiết về stack, cách một hàm được gọi, và demo một cách khai thác lỗi tràn bộ đệm. Các bạn nên đọc và làm theo bài đó trước.
Study case: chương trình echoServer của phần format string trước
Mã nguồn + file chạy (và của các ví dụ liên quan):
Chương trình có chức năng in ra mọi thứ mà ta nhập vào, cho đến khi ta nhập exit. Ở đó, printf(buf) tạo nên lỗi format string:
%n là một dạng format đặc biệt, cho phép ta ghi lại số ký tự đã được in ra trước đó.
Ví dụ trong chương trình sau (compile: gcc format_dollar_sign.c -o format_dolloar_sign -m32):
Tại dòng 7, ta có %n với tham số tương ứng là &nChars. Printf in ra “hello”, độ dài 5, nên nChars sẽ được gán giá trị bằng 5, giống như dòng 8 in ra “nChars = 5”:
Tương tự, dòng số 9 in ra “A number 2000” có độ dài 13, nên nChars tương ứng với %n được gán bằng 13. Hay tại dòng 13, ta nhập vào buf = “1234567890”, nên nChars được gán bằng 10.
Format %n giúp ta ghi được nội dung tùy ý vào bộ nhớ.
Format string với $
Ngoài %n, $ là một ký tự format rất ít dùng nhưng lại hỗ trợ đắc lực khi khai thác format string.
$ giúp ta xác định tham số thứ mấy sẽ tương ứng với format đang xét.
Ví dụ (compile: gcc format_dollar_sign.c –o format_dolloar_sign –m32):
Khi chạy chương trình, ta sẽ có:
Tại dòng 6, hàm printf nhận %1[imath]s, thì nó tham chiếu tới tham số thứ 1 của hàm (ta coi xâu “hello %1[/imath]s
” là tham số thứ 0, thì “A” là tham số thứ 1), nên nó in ra “hello A”. Ở đây số 1 nằm trước $ chỉ số thứ tự của tham số.
Tương tự, %2$s tương ứng với “B”, nên dòng 7 in ra “hello B”.
Còn tại dòng thứ 8, 50 và 51 tương ứng với các tham số thứ 50, 51. Tuy trong code không có 2 tham số này, nhưng thực tế, nó sẽ ám chỉ các tham số thứ 50, 51 truyền vào stack, và đó chính là các giá trị dword tương ứng nằm tại vị trí 50, 51 trên stack. Rõ hơn, ta sẽ thấy trong gdb:
Mô tả: trong session gdb trên, mình đặt breakpoint tại lệnh call printf(“hello %50[imath]p %51[/imath]p”). x /2wx $esp+4*50 in ra 2 dword tại offset 4*50 trong stack (mỗi dword có kích thước 4byte), được các giá trị mà hàm printf sẽ in ra.
Các “con trỏ hàm” GOT (Global Offset Table)
Trong cấu trúc của file elf, có một vùng dùng để lưu địa chỉ của các hàm trong thư viện mà ta sẽ gọi tới, gọi là GOT. Ví dụ trong chương trình echoServer, ta gọi đến các hàm thư viện setvbuf, printf, fgets, strcmp, exit thì trong GOT sẽ có các con trỏ lưu địa chỉ của các hàm này. Cụ thể hơn như sau:
Trong chương trình echoServer, khi phải gọi tới một hàm nào đó trong thư viện libc (hay bất cứ thư viện chia sẻ nào khác), vd khi lần đầu tiên gọi printf, chương trình thực hiện như sau:
- Gọi hàm printf@plt (như ta thấy lệnh tên là call printf@plt). Hàm printf@plt này nằm trong sẵn module echoServer.
- printf@plt chỉ có một lệnh là jmp GOT[printf] (ta tạm gọi vùng nhớ dành cho printf trong GOT là GOT[printf]. Như vậy nó chỉ thực hiện mỗi một việc là nhảy tới địa chỉ được lưu tại GOT[printf].
- Nếu là lần đầu tiên được gọi tới, GOT[printf] trỏ tới một chuỗi các lệnh để tìm ra địa chỉ hàm printf thực sự, lưu nó vào GOT[printf], rồi gọi printf.
- Gọi hàm printf@plt
- printf@plt thực hiện jmp GOT[printf].
- Trong GOT[printf] lúc này có địa chỉ của hàm printf thật, nên printf được thực hiện luôn.
Như vậy có thể nói rằng, khi lần đầu gọi một hàm thư viện, thì chương trình thực hiện phân giải địa chỉ của hàm, và lưu nó vào trong GOT. Trong các lần gọi sau thì chương trình không phải phân giải nữa mà gọi trực tiếp hàm luôn qua GOT.
Bảng GOT khi xem trong IDA:
Vậy trong GOT có chứa địa chỉ của các hàm trong libc. Có thể coi đây là các con trỏ hàm.
Cùng với việc sử dụng $ và %n, ta sẽ thực hiện khai thác chương trình, bằng cách ghi đè các con trỏ hàm này. Mình sẽ tiếp tục bài khai thác trong phần tiếp theo.
Chỉnh sửa lần cuối bởi người điều hành: