[PT007] Cách thức giả lập bằng Qiling và tìm lỗ hổng bảo mật


Giới thiệu

Qiling là một nền tảng cho phép giả lập nhiều môi trường (Linux, macOS, Windows, FreeBSD, DOS,…) và cấu trúc nhân khác nhau (hỗ trợ x86 - 16bit, 32bit, x64, ARM, ARM64 và MIPS) để debug cũng như fuzzing các file thực thi nhằm tìm kiếm lỗ hổng bảo mật. Thông tin chi tiết về Qiling các bạn có thể tham khảo tại địa chỉ này. Nhân dịp thực hiện kiểm tra,  đánh giá bảo mật trong một dự án cần đến sự hỗ trợ của nền tảng này, tôi muốn chia sẻ với mọi người về cách giả lập bằng Qiling và tìm ra lỗ hổng bảo mật.


Chi tiết

Việc đầu tiên là tìm các chân và scan port của thiết bị, phần này khá đơn giản vì mạch này vẫn để UART debug.

Sử dụng serial để kết nối vào và kiểm tra xem thiết bị sử dụng kiến trúc nào. Trên thiết bị không có lệnh “file” nên tôi lấy binary về và check.

Thiết bị sử dụng kiến trúc ARM, little endian. Kiểm tra các port đang mở với lệnh netstat.

Vì mục đích bài này để giới thiệu cũng như hướng dẫn sử dụng Qiling nên tôi sẽ chỉ tập trung vào 1 port là 5060 udp. Các port còn lại, có thể tôi sẽ viết tiếp vào một thời điểm thích hợp khác.

Tải file binary về và sử dụng Ghidra để phân tích:

Để tìm đoạn mã xử lí của port 5060 thì tôi sẽ tìm những chỗ gọi đến hàm bind recv, recvfrom. Các hàm này sẽ liên quan đến việc tạo socket và nhận dữ liệu được gửi đến thiết bị.


Sau một thời gian phân tích, ta có thể tìm được phần xử lí dữ liệu nhận được của port 5060

Đọc các dòng log của chương trình có thể thấy chương trình sử dụng thư viện eXosip, về cơ bản được xây dựng dựa trên thư viện osip, phục vụ cho việc lập trình VoIP (Voice over Internet Protocol).

Tôi có thể lấy source của thư viện về và thực hiện fuzzing, tuy nhiên source hiện tại của thư viện chưa chắc đã có lỗi và phiên bản thiết bị sử dụng chưa chắc đã là phiên bản mới nhất. Thậm chí luồng thực thi của thư viện và chương trình khác nhau khá nhiều. Và điều cuối cùng là chúng tôi muốn có PoC cụ thể đối với mỗi sản phẩm được chúng tôi đánh giá.

Do đó, tôi quyết định fuzzing trên chính file executable này bằng cách sử dụng Qiling, cũng là một phần muốn áp dụng Qiling vào công việc thực tế để đánh giá hiệu quả của nó. Hướng dẫn cài đặt và sử dụng Qiling mọi người có thể tham khảo tại đây. 

Về cài đặt, tôi khuyến nghị nên thực hiện manual install vì vài lần tôi cài thẳng bằng pip thì bị lỗi khá nhiều. Mục tiêu là chọn ra một hàm xử lí dữ liệu được gửi tới, setup các param cần thiết, emulate hàm này và thực hiện fuzzing.

Như trong hình trên có thể thấy được hàm osip_parse là target thích hợp. Hàm nhận hai tham số: buf – dữ liệu nhận được, len – độ dài dữ liệu nhận được. Để có thể sử dụng Qiling emulate được chương trình thì cần hai thứ: script và môi trường. Tôi sẽ tạo một thư mục rootfs đơn giản bằng cách copy thư mục mẫu của Qiling.


Script:

```

import sys

from qiling import *

 

def run_sandbox(path, rootfs, output):

    ql = Qiling(path, rootfs, output = output)

    ql.run()

 

if __name__ == "__main__":

    run_sandbox(["./arm_linux/bin/main"], "./arm_linux", "none")

```

$ python3 myLoad.py


Kết quả là không thành công, bây giờ ta sẽ đi tìm nguyên nhân


Một điều tôi rất thích ở Qiling là khi có lỗi xảy ra, Qiling sẽ hiển thị thông tin về các thanh ghi và vùng nhớ của chương trình khi gặp lỗi. Kéo lên trên một chút ta có thể thấy log của Qiling trả ra, ở đây chương trình đã cố gắng tìm và load thư viện libminigui_ths nhưng không tìm thấy, kết quả của các hàm open đều là -2.


Trong thực tế khi làm thì tôi đã lấy nguyên thư mục /lib, các thư mục chứa các thư viện khác và các biến môi trường cần thiết về để setup.


Tôi sẽ sửa script một chút để thêm biến môi trường:

```

import sys

from qiling import *

 

def run_sandbox(path, rootfs, output, env):

    ql = Qiling(path, rootfs, output=output, env=env)

    input("env: " + env["LD_LIBRARY_PATH"])

    ql.run()

 

if __name__ == "__main__":

    run_sandbox(["./arm_linux/bin/main"], "./arm_linux", "none",\

           {"LD_LIBRARY_PATH":"/lib:"+\

           "/env/config/wifi:"+\

           "/env/customer/tslib/lib:"+\

           "/env/customer/libminigui:"+\

           "/env/customer/lib:"+\

           "/env/customer/morfeicore/libs"})

```



Tuy nhiên khi làm bằng cách này tôi gặp một vấn đề với Qiling là với biến LD_LIBRARY_PATH thì linker chỉ tìm thư viện ở thư mục đầu tiên trong biến LD_LIBRARY_PATH. Tôi không thực sự chắc chắn là do Qiling hay do tôi setup. Tuy nhiên, chuyện này có thể khắc phục được bằng cách tìm tất cả các thư viện dependencies và để chung vào một thư mục nên tôi quyết định không bỏ thời gian tìm hiểu kĩ.


Sau khi đã setup đầy đủ các thư viện thì thử chạy lại:


Nhìn vào log in ra ta có thể biết được file đã được load lên thành công và bắt đầu chạy vào main(), tuy nhiên /dev/mi_sys không tồn tại nên chương trình đã kết thúc. Ta có thể bỏ qua việc này vì mục đích cuối cùng là fuzz hàm osip_parse chứ không phải là chạy chương trình này một cách hoàn chỉnh.

Để có thể làm được việc tôi nói trên, ta cần xác định địa chỉ bắt đầu và kết thúc cho ql.run() cũng như giá trị phù hợp trên các thanh ghi.

Tôi sẽ chọn địa chỉ 0x207568  0x20756c

Cùng với đó là setup các tham số của osip_parsebuf len:

Biến buf là con trỏ trỏ vào một vùng nhớ được cấp phát bởi hàm osip_malloc, đi sâu vào hàm thì ta biết được vùng nhớ sẽ được cấp phát bởi hàm calloc.



Vậy ta sẽ hook vào địa chỉ 0x207568 và cấp phát một vùng nhớ, truyền địa chỉ vùng nhớ và độ dài của dữ liệu lần lượt vào thanh ghi r0 r1.

Script sẽ có thay đổi như sau:

```

#!/usr/bin/env python3

import sys

from qiling import *

 

BEGIN_ADDRESS = 0x207568

END_ADDRESS = 0x20756c

myMap = 0

size = 3000

payload = b"this is payload" # we will replace that with real one later

 

def setupReg(ql):

     global myMap

     myMap = ql.mem.map_anywhere(size)

     ql.mem.write(myMap, payload)

     ql.reg.write("r0", myMap)

     ql.reg.write("r1",len(payload))

 

def run_sandbox(path, rootfs, output):

    ql = Qiling(path, rootfs, output = output)

    ql.hook_address(setupReg, BEGIN_ADDRESS)

    # here we hook BEGIN_ADDRESS with setReg function

    ql.run(begin=BEGIN_ADDRESS, end=END_ADDRESS)

    print("[.] Done")

 

if __name__ == "__main__":

    run_sandbox(["./arm_linux/bin/main"], "./arm_linux", "none")

```


Và chạy thử:


Ở đây Qiling báo lỗi là Invalid memory fetch, tuy nhiên PC=0x0, nghĩa là chương trình chưa được load lên, đã có lỗi trong quá trình load. Tôi sẽ để mode output của Qiling là debug để có thêm thông tin và so sánh xem khi chạy bình thường và khi chạy từ BEGIN_ADDRESS có gì khác biệt.


Bên trái là khi chạy từ BEGIN_ADDRESS, bên phải là khi chúng ta chạy từ đầu. Có thể thấy khi chạy từ BEGIN_ADDRESS đã bỏ qua các phần load thư viện dẫn đến lỗi. Để giải quyết vấn đề này, ta sẽ đợi cho chương trình load đầy đủ lên rồi sẽ chuyển luồng thực thi đến hàm osip_parse. Tôi sẽ hook vào hàm _start, cụ thể là khi hàm _start gọi hàm __libc_start_main với tham số đầu tiên là địa chỉ của hàm main. Thay vì gọi hàm main thì __libc_start_main sẽ gọi osip_parse, sau đó tôi sẽ setup các tham số cần thiết. Chúng ta sẽ không thiết lập các tham số ngay thời điểm đó, vì các tham số của hàm main(int argc, char** argv) sẽ được tìm và khởi tạo bởi hàm __libc_init_first, chứ không được truyền từ hàm __libc_start_main.



Script sẽ thay đổi như sau:

```

#!/usr/bin/env python3

import sys

from qiling import *

 

BEGIN_ADDRESS = 0x207568

END_ADDRESS = 0x20756c

myMap = 0

size = 3000

payload = b"this is payload"

 

def _startHook(ql):

     global myMap

     # malloc memory for payload

     myMap = ql.mem.map_anywhere(size)

     ql.mem.write(myMap, payload)

 

     # replace main addr with target addr

     ql.reg.write("r0", BEGIN_ADDRESS)

 

def setupReg(ql):

     # osip_parse(buf, len)

     ql.reg.write("r0", myMap) # buf

     ql.reg.write("r1",len(payload)) # len

 

def run_sandbox(path, rootfs, output):

    ql = Qiling(path, rootfs, output=output)

 

    ql.hook_address(_startHook, 0x000A6144) # call target function instead of main

    ql.hook_address(setupReg, BEGIN_ADDRESS) # setup arguments for target function

    ql.run(end=END_ADDRESS)

    #ql.run()

    print("[.] Done")

 

 

if __name__ == "__main__":

    run_sandbox(["./arm_linux/bin/main"], "./arm_linux", "debug")

```



Có vẻ là tôi đã emulate thành công hàm osip_parse, tiếp theo là phần tương tác với AFL++Phần này thì Qiling cũng có mẫu rồi nên chúng ta chỉ cần đọc hiểu là được, tôi cũng đã comment thêm một vài dòng đơn giản nhất có thể.

Script:

```

#!/usr/bin/env python3

import sys

from qiling import *

import unicornafl

unicornafl.monkeypatch()

 

BEGIN_ADDRESS = 0x207568

END_ADDRESS = 0x20756c

myMap = 0

size = 3000

 

def _startHook(ql):

     global myMap

     # map memory for payload

     myMap = ql.mem.map_anywhere(size)

 

     # replace main addr with target addr

     ql.reg.write("r0", BEGIN_ADDRESS)

 

def setupReg(ql):

     # osip_parse(buf, len)

     ql.reg.write("r0", myMap) # buf

     ql.reg.write("r1",len(payload)) # len

 

def run_sandbox(path, rootfs, output, input_file):

    ql = Qiling(path, rootfs,console=True, output=output)

 

    ### AFL go from here

   

    # callback where we write content from input to memory

    def place_input_callback(uc, payload, _, data):

      # write payload into our memory

        ql.mem.write(myMap, payload)

 

        # osip_parse(buf, len)

        ql.reg.write("r0", myMap) # buf

        ql.reg.write("r1",len(payload)) # len

 

     # afl hook at target function

    def start_afl(_ql: Qiling):

        """

        Callback from inside

        """

        # We start our AFL forkserver or run once if AFL is not available.

        # This will only return after the fuzzing stopped.

        try:

            #print("Starting afl_fuzz().")

            # input_file: this is where we feed corpus 

            # place_input_callback: we will implement how input is handle in this callback

            if not _ql.uc.afl_fuzz(input_file=input_file,

                        place_input_callback=place_input_callback,

                        exits=[ql.os.exit_point]):

                print("Ran once without AFL attached.")

                print(input_file)

                os._exit(0)  # that's a looot faster than tidying up.

        except unicornafl.UcAflError as ex:

            print("[.] Error: " + ex)

 

 

    ql.hook_address(_startHook, 0x000A6144) # call target function instead of main

    ql.hook_address(start_afl, BEGIN_ADDRESS) # setup afl

    ql.run(end=END_ADDRESS)

    #ql.run()

 

if __name__ == "__main__":

    if len(sys.argv) != 2:

        print("Require 2 arguments")

    else:

        run_sandbox(["./arm_linux/bin/main"], "./arm_linux", "debug", sys.argv[1])

```


Có thể thấy tôi đã thay hàm hook setupReg bằng hàm start_afl để nhận input từ file và tương tác với AFL,  cũng như bổ sung tham số input_file cho hàm run_sandbox. Và đây là kết quả:



Thực ra khi làm đến đoạn này, tôi đã vướng phải một lỗi khá là ngớ ngẩn. Nếu các bạn để ý trong những phiên bản đầu tiên của script thì tôi có để hàm input() để log ra xem biến env đã được cấu hình đúng chưa. Chính vì hàm này mà khi chạy với AFL tôi không biết tại sao lại lỗi và tốn khá nhiều thời gian cho lỗi này.

Tuy nhiên đến đây thì script vẫn chưa hoàn chỉnh. Như trong trường hợp này, ta tìm được document liên quan thì sẽ dễ dàng biết được cách thư viện parse data:


Đa số những trường hợp khác ta sẽ không có thông tin này, dẫn tới việc khi thực hiện fuzzing sẽ không chính xác và phải reversing để lấy đầy đủ thông tin. Nếu chỉ dừng lại ở bước vừa rồi, thì ta sẽ thấy có rất nhiều crash và đa số đều là false positive. Tôi sẽ lấy một vài sample trong thư mục crash để xem hàm osip_parse lỗi ở đâu khi xử lí những file này.


Chương trình crash ở địa chỉ 0x77fc5e10 có instructionldrb r2, [r0]– load một byte của giá trị r0 trỏ tới vào r2. Dựa vào địa chỉ các vùng nhớ được map thì 0x77fc5e10 nằm trong vùng nhớ của libc. Khả năng cao là một hàm của libc đã được gọi với tham số không được xử lý đúng cách.

Ta sẽ thêm một đoạn code nhỏ của Qiling để trace xem hàm nào đã gọi tới hàm này và chạy lại script.


Giá trị của thanh ghi r0 0, chương trình cố gắng đọc nội dung ở địa chỉ 0 nên bị lỗi. Địa chỉ cuối cùng trong chương trình gọi tới hàm libc 0x234bdc. Ta kiểm tra địa chỉ này trên Ghidra. Hàm strcmp truy cập đến mảng 0x753d34 , nhiều khả năng là do mảng này chưa được khởi tạo nên xảy ra lỗi khi hàm strcpy truy cập tới mảng này.


Kiểm tra các reference ta thấy hàm parser_init() có tác động tới địa chỉ này.


Có thể thấy được các giá trị trong mảng bắt đầu từ địa chỉ 0x753d34 được khởi tạo ở đây, vậy ta cần bổ sung hàm này vào harness để có thể chạy đúng luồng của chương trình. Lúc này luồng thực thi sẽ là:

Sau khi parser_init  chạy xong thì tôi sẽ ghi thẳng vào thanh ghi PC để chuyển luồng thực thi

```

#!/usr/bin/env python3

import sys

from qiling import *

import unicornafl

unicornafl.monkeypatch()

 

start_osipParse = 0x207568

end_osipParse = 0x20756c

 

start_parserInit = 0x0022AF8C

end_parserInit = 0x0022AF90

 

myMap = 0

size = 3000

#payload = b"a"*1000

 

def _startHook(ql):

     global myMap

     # map memory for payload

     myMap = ql.mem.map_anywhere(0x10000)

 

     # replace main addr with target addr

     ql.reg.write("r0", start_parserInit)

 

def directToTarget(ql):

     ql.reg.write("pc", start_osipParse)

 

def setupReg(ql):

     ql.mem.write(myMap, payload)

     # osip_parse(buf, len)

     ql.reg.write("r0", myMap) # buf

     ql.reg.write("r1",len(payload)) # len

 

def run_sandbox(path, rootfs, output, input_file):

    ql = Qiling(path, rootfs,console=True, output=output)

    ### AFL go from here

   

    # callback where we write content from input to memory

    def place_input_callback(uc, payload, _, data):

      # write payload into our memory

        ql.mem.write(myMap, payload)

 

        # osip_parse(buf, len)

        ql.reg.write("r0", myMap) # buf

        ql.reg.write("r1",len(payload)) # len

 

     # afl hook at target function

    def start_afl(_ql: Qiling):

        """

        Callback from inside

        """

        # We start our AFL forkserver or run once if AFL is not available.

        # This will only return after the fuzzing stopped.

        try:

            #print("Starting afl_fuzz().")

            # input_file: this is where we feed corpus 

            # place_input_callback: we will implement how input is handle in this callback

            if not _ql.uc.afl_fuzz(input_file=input_file,

                        place_input_callback=place_input_callback,

                        exits=[ql.os.exit_point]):

                print("Ran once without AFL attached.")

                os._exit(0)  # that's a looot faster than tidying up.

        except unicornafl.UcAflError as ex:

            print("[.] Error: " + ex)

 

 

    ql.hook_address(_startHook, 0x000A6144) # call target function instead of main

    ql.hook_address(directToTarget, end_parserInit) # direct to osip_parse

    ql.hook_address(start_afl, start_osipParse) # setup afl

    ql.run(end=end_osipParse)

    #ql.run()

 

 

if __name__ == "__main__":

    if len(sys.argv) != 2:

        print("Require 2 arguments")

    else:

        run_sandbox(["./arm_linux/bin/main"], "./arm_linux", "debug", sys.argv[1])

```


Sau một thời gian chạy thì cũng có kết quả là một vài crash, và tôi kiểm tra bằng cách gửi gói tin đến cổng 5060. Tiến trình có crash, tuy nhiên trên thiết bị có một script để restart lại tiến trình nếu tiến trình không tồn tại.


Khi test với thư viện phiên bản mới nhất thì không xảy ra tình trạng này, và như vậy đã đủ PoC đơn giản nên tôi chưa phân tích thêm nữa.

Đến đây, tôi hy vọng đã truyền tải tới các bạn được cách fuzzing cơ bản với Qiling. Có thể thấy việc này không quá khó nhưng cần một số kiến thức nhất định. Chắc chắn Qiling sẽ còn phát triển hơn nữa và áp dụng được cho nhiều target giúp chúng ta tiết kiệm thời gian và các nguồn lực khác.

Xin cảm ơn các bạn đã dành thời gian ra để đọc bài viết này.

Tham khảo


Click here for Englishversion




Trần Phạm Thành (aka Radcet)

Services Center - VinCSS (a member of Vingroup)


1 comment:

  1. bạn cho hỏi
    tôi muốn nghiên cứu về lĩnh vực này tôi cần học những kiến thức gì
    cảm ơn bạn

    ReplyDelete