TL;DR
IoT ngày nay đã an toàn hơn ngày xưa rất nhiều, được thêm rất nhiều lớp bảo vệ tuy nhiên vẫn không làm khó được REDTEAM@VNPT ISC
Một ngày đẹp trời nhận được một con IP CAM với lời thách thức: Con này tích hợp phần mềm bảo vệ hệ thống file, process rất sâu vào trong lõi (kernel) của firmware rồi chú có khai thác được không?
Hmmm, bấy lâu nay đang pentest ở userland nay dev họ lên kernel thì ngại gì không theo?
TL;DR for tech
IP CAM X được custom code nhân hệ điều hành để kiểm soát các syscall nguy hiểm nhằm bảo vệ hệ thống (file, process) trước các nguy cơ tấn công phá hoại, ngăn ngừa lây nhiễm mã độc.
Tôi bypass cơ chế bảo vệ này bằng cách extract kernel, dịch ngược kernel, lợi dụng những hạn chế còn tồn tại để install một kernel module PHA_KE patch lại hàm kiểm soát syscall để bypass.
Bắt tay vào làm, bước đầu không vội RCE nó từ bên ngoài làm gì, thử chọc từ shell UART dò la xem nó thế nào.
Mặc dù là UART không đặt pass gì cả (nếu mà có pass thì cũng sẽ có cách giải quyết thôi, hẹn bạn đọc ở một “nạn nhân” khác)
Nhưng có vẻ như việc ghi file nào, vào đâu đều bị LOG lại và xử lý, nếu syscall đó trong Whitelist thì được FW còn ngoài Whitelist sẽ bị DROP. Tương tự cho các syscall khác như execve, mount/unmount, insmod/rmmod.
“Nạn nhân” lần này có vẻ cứng.
Biết người biết ta trăm trận trăm thắng nên còn chờ gì nữa mà không múc cái kernel về reverse xem thế nào! May mắn thay, không giống việc ghi file, việc đọc file khá thoải mái trên thiết bị này, cộng với việc /bin/busybox có hỗ trợ TFTP nên thoải mái lấy file mà phân tích.
Câu hỏi bây giờ là kernel nằm ở đâu? Kernel cũng phải được load từ một device lên thì khả năng rất cao là nằm trong /dev/. Sau khi khám thì phát hiện ra các ứng cử viên sáng giá sau:
Khám các ứng cử viên mtd0 -> mtd5 bằng tool quen thuộc binwalk
Có vẻ như mtd0 là chứa tất cả mtdX còn lại nên sẽ khám các mtdX nghi vấn bắt đầu từ mtd3.
uImage header thì không phải là vmlinux rồi thế là chỉ còn file LZMA kia thôi.
binwalk mtd3 -e
Sử dụng command trên để extract tất cả file ra rồi bỏ vô IDA mà húp thôi.
Đúng là kernel rồi chứa đúng cái chuỗi LOG luôn.
Nhưng cuộc đời không như mơ … không có header …
Không có HEADER thì bỏ vào IDA biết base address là bao nhiêu mà xref được code? Hình dưới là đớn đau của reverse mà không biết base address
Trong hình trên chẳng biết 0x80027A70 là ở đâu cả 🙁 Chợt nhớ lại trước file LZMA này còn có file uImage header. Quay ngược lại reverse file này thì phát hiện ra chính thằng uImage header sẽ Uncompress LZMA lên mem rồi thực thi trực tiếp => thằng LZMA không còn cần header nữa nên không thấy là điều dễ hiểu.
Cuộc sống sẽ rất đơn giản nếu để ý đúng vị trí:
Rất dài, rất rối nhưng may mắn thay các anh dev chưa strip symbol và xóa debug nên vẫn có thể thấy được ý họ muốn nói rằng vị trí data uncompress là nằm ở 0x80010000
Nhưng giải quyết được base address vẫn chưa phải là tất cả vì không biết entrypoint nằm ở đâu thì IDA sẽ không bắt đầu phân tích được (IDA phân tích từ gốc đến ngọn) 🙁
Không cần!!! Đoán rằng để giám sát các syscall thì sẽ hook vào các syscall đấy và phần dễ hook nhất đối với kernel version 3.10 chính là SỬA sys_call_table
Tham khảo https://elixir.bootlin.com/linux/v3.10.27/source/arch/mips/kernel/scall64-o32.S#L194 dựa vào source code tôi tìm ra được signature của phần này và lần mò theo signature mà analyzes code đúng vị trí thôi.
Wéo weo wèo !!! Kết hợp với việc phân tích file /dev/mem (mem của kernel) tôi thấy các địa chỉ của bản sys_call_table này không bị thay đổi từ đó đưa ra kết luận họ không làm như tôi đoán (sửa sys_call_table) mà họ đã chèn code vào phía sau hoặc trước khi jump đến các địa chỉ trong sys_call_table này. Cái nào dễ làm trước, đã có xref của phần phía sau thì ta đi sâu vào phần phía sau vậy …
Đây là so sánh giữa sys_unlink gốc và sys_unlink được modify. Vậy là việc hook vào syscall được thực hiện sau khi jump đến các địa chỉ trong sys_call_table. Sau một hồi reverse dài dai dở thì phát hiện ra việc FW hay DROP một syscall là do duy nhất một đoạn code sau quyết định
Chỉ cần NOP đi *a2 = 1 và NOP luôn cái việc call hàm NOTSURE_printk thì sẽ bypass được và bịt mồm cái LOGGER lại đỡ rối CONSOLE
Nhưng làm sao sửa mem kernel bây giờ ??? Sửa /dev/mem ??? Có quyền ghi file đâu 🙁 Xâu chuỗi ngược về hình ảnh xuất LOG
ERROR: Chekc x: iAction: 7, iActorId: /bin/busybox — 3, iTargetId: /tmp/test — 15, bPlainTextAttr: 0, bIsExists :0
Có vẻ như là cơ chế hook này không parse được tất cả các tham số ví dụ như nếu gọi các câu lệnh như
mount /dev/xxx /path_to_mount
Thì path_to_mount sẽ không được kiểm tra … thử xem …
Oh yeah! Họ chỉ kiểm tra device có được phép mount hay không còn mount vào đâu thì không quan tâm =)) Thế thì câu chuyện lại đơn giản, ta chôm một kernel module có sẵn, patch lại init_module để sửa mem kernel (phân vùng kernel là rwx) là xong?
Nhưng làm gì có quyền insmod??? insmod cũng là một syscall bị kiểm soát 🙁 Nhưng mà thiết bị cũng cần phải insmod các driver khác chứ??? Yep đúng vậy một câu lệnh grep -rnw / -e ‘xxxx.ko’ là sẽ tìm ra file thực thi có quyền insmod. Chỉ việc mount đè nó với busybox của chính IP CAM là xong! (Cơ chế bảo vệ sẽ nhầm lẫn tưởng file bị đè bằng busybox có chức năng insmod chính là file gốc insmod những file .ko hợp phép)
Patch lại một file .ko chôm được:
Có thể một số bạn đọc sẽ thắc mắc là tại sao tôi không làm 1 ĐƯỜNG CƠ BẢN mà lại phải JUMP sang chỗ khác … lý do là đối với các SYMBOL/VARIABLE thuộc loại GLOBAL thì khi được import lên KERNEL sẽ được FIX vào một vị trí trên KERNEL và để chính MODULE này sử dụng được thì KERNEL sẽ tự sửa lại CODE để trỏ đúng vị trí đã FIX trên KERNEL.
Vậy nên CODE tại các vị trí này sẽ luôn bị sửa đổi nên phải “NÉ” nó ra.
Kết quả:
(BEFORE)
(AFTER)
Như thế là đã xong phần bypass bảo vệ nhân hệ điều hành. Hẹn gặp các bạn ở phần 2 RCE từ giao diện điều khiển CAMERA!
Thanks for reading,
__Neo of RedTeam VNPT ISC__