Ghi chú:
A develper is experiementing with different ways to protect their software. They have sent in a windows binary that is suposed to super secureand really hard to debug. Debug and see if you can find the flag.
Chúng ta có thể sử dụng lệnh 'file' để cung cấp cái nhìn tổng quan về loại tệp.
Mở file trong IDA32, quan sát trong hàm mainCRTStartup_0
ta thấy chương trình sử dụng BeingDebugged và NtGlobalFlag field
của PEB
để kiểm tra chương trình có đang debug không.
Sau đó, thực hiện XOR
các byte của hàm main()
với opcode 0x5C để có thể thực thi được.
Ta được nội dụng hàm main()
như sau:
Chương trình thực hiện lấy v11 XOR 0x4B
rồi trả về kết quả tính được.
While cleaning up the workspace of a recently retired employee, we noticed that one of the core files of the very important programs they were working on didn't match up with the backups we have of it, could you check it out for us?
Chúng ta có thể sử dụng lệnh 'file' để cung cấp cái nhìn tổng quan về loại tệp.
Tệp có 7 section
, khi ta mở file trong PEBear
, ta tìm thấy tệp có một section
tên ivir
. Đây là một section
lạ. Đồng thời, ta lại thấy Timestamp = deadc0de
.
Mở chương trình trong IDA64.
Trong hệ điều hành Windows, cấu trúc Thread Environment Block (TEB) sẽ chứa thông tin chi tiết cần thiết để quản lý luồng (thread) đang chạy. TEB là một thành phần quan trọng trong không gian người dùng của hệ điều hành, liên quan mật thiết đến việc thực thi và điều hành các chương trình.
Bên trong TEB, ta có ProcessEnvironmentBlock (PEB) nắm giữ thông tin môi trường chi tiết của quá trình mà luồng nằm trong. PEB cũng chứa thành viên Ldr – nó cung cấp quyền truy cập vào danh sách liên kết InMemoryOrderModuleList, nơi liệt kê tất cả các module đã được nạp theo đúng thứ tự trong bộ nhớ. Thông qua danh sách này, Ldr giúp quản lý các module khác nhau
Từng API trong module sẽ được tính checksum CRC32 và kiểm tra có bằng 0xBC553B82. Ta sẽ tìm được chương trình muốn gọi GetProcAddress
Hàm sub_14001C1A8
để xác định hành động cần thực hiện - sao chép tên các API để sử dụng GetProcAddress sau này gọi các API đó.
Từ đây, ta có thể thay đổi tên của các biến để chương trình rõ ràng và dễ hiểu hơn. Mục đích của nó để tải động các hàm từ kernel32.dll
, sử dụng trong việc xử lý tệp và quản lý tiến trình.
Chương trình kiểm tra byte thứ năm từ EntryPoint có giá trị là 5 hay không. Nếu có, nó sẽ gọi một hàm hiện tại được mã hóa, nếu không, mã sẽ chọn và lưu trữ một số 64-bit sẽ được sử dụng sau này để giải mã. Khi chưa thực thi, giá trị byte đó là 1.
Chương trình sẽ kiểm tra qua các tệp có phần mở rộng exe
. Nếu tìm thấy, nó tệp với tên đã có và cấp quyền truy cập (GENERIC_READ và GENERIC_WRITE). Nếu việc mở file thành công sẽ lấy kích thước của file. Sau đó, kích thước mới của file được tính bằng cách cộng thêm một giá trị cố định (0xD80) vào kích thước hiện tại của file, mục đích để ghi thêm một section mới.
Hàm này tạo ra một đối tượng file mapping. Khi một file được mapped, dữ liệu của nó có thể được xem như một mảng liên tục trong không gian địa chỉ của quá trình. Khi đã có một đối tượng file mapping, MapViewOfFile cho phép ánh xạ nó vào không gian địa chỉ của quá trình gọi hàm. Nó cũng cho phép bạn chỉ định cặp quyền truy cập và bảo vệ khi ánh xạ, như chỉ đọc, chỉ ghi, hoặc ghi đè.
Như ta đều đã biết, cấu trúc của một PE File
bao gồm:
Đây là hình ảnh mô tả về phần DOS Header, DOS Stub and PE Header. Tại đây ta thấy, thành phần cuối cùng của DOS Header
là File address of new exe header
= 0xF8
. Thành viên này rất quan trọng đối với trình tải PE trên hệ thống Windows vì nó cho trình tải biết nơi tìm PE Header
.
Ta kết luận v25 = 0xF8
=> v25 + 4 = 0xFC
và v25 + 8 = 0x100
. Tương ứng với Machine
và Time Date Stamp
. Biết v26 = v25 + 20 = 0x10C
và v27 = v26 + 40 * Section Count = 0x224
.
RVA
và FileOffset
sẽ có giá trị là phần đầu của section
.
Ở đây tôi đang ví dụ với tệp đề cho. Trên thực thế, nó không thể load chính nó được vì tệp được load sẽ bị chương trình take control một cách tuyệt đối. Không thể mở nó trong các chương trình khác.
Hiểu đến đây, ta sẽ sử dụng struct của PE header để sửa chương trình cho thuận tiện trong phân tích sau này.
Chương trình thực hiện kiểm tra:
Lấy giá trị của trường SizeOfHeaders
từ Optional Header. Kiểm tra xem kích thước mới của section .ivir (0xD80) có phải là bội số của SizeOfHeaders
không, nếu không thì bổ sung thêm. Tính toán con trỏ đến dữ liệu thô của section .ivir
bằng cách cộng trường PointerToRawData
và VirtualSize
của section. Đặt các cờ của section .ivir với các quyền truy cập thích hợp. Cập nhật kích thước của image
trong File Header
và Timestamp
.
Lấy giá trị của các cờ của section .ivir
gán với 0x60500060, ta đặt tên biến là IvirSectionFlag. Các thay đổi này tạo ra một section mới trong file thực thi và cập nhật các giá trị SizeOfCode
Initialized Data
và Uninitialized Data
nếu cần thiết. Dựa trên những giá trị cài đặt của các cờ bit để điều chỉnh kích thước section trong file thực thi.
Nếu đủ vùng nhớ phần cho phần section mới, nó sẽ được ghi thêm section đó và section header được thêm và sửa đổi để đảm bảo chúng tuân thủ các quy tắc căn chỉnh và phần còn lại của quá trình chỉ là sao chép mã lây nhiễm và làm cho nó thay thế EntryPoint của injected file
Thiết lập để trỏ tới ký tự cuối cùng trong file có kích thước mới. Nếu vị trí con trỏ vượt qua giới hạn của vùng nhớ hiện tại, nó sẽ di chuyển dữ liệu cũ sang phía sau để tạo không gian cho dữ liệu mới. Sau đó, nó thực hiện việc sao chép dữ liệu mới từ vị trí bắt đầu của start
vào vùng nhớ đã được làm trống. Sau khi hoàn thành việc xử lý dữ liệu, nó đóng tài nguyên bằng cách gọi hàm CloseHandle()
.
Phần cuối của chương trình thực hiện giải mã nhiều lần bằng cách sử dụng các khóa đã đề cập ở trên - start + 5
. Lúc đầu nó có giá trị là 1, sau mỗi lần giải mã thì tăng 1 cho giá trị đó, túc là lặp lại 4 lần đến khi *(start + 5) = 5
thì gọi hàm byte_14001C7E4()
. Ta viết đoạn mã để giải mã hàm này.
Ta di chuyển đến và bôi đen từ 0x14001C7E4 đến 14001C971
và make code -> make function
. Hãy chắc rằng bạn đã NOP bốn byte tại byte_14001C018
. Ta sẽ nhận được các hàm sau.
Đoạn mã load một số hàm quan trọng từ thư viện kernel32.dll và user32.dll bằng cách sử dụng hàm GetFunction_v2
, sau đó gọi một số hàm API của hệ thống như GetUserNameA
và MessageBoxA
để thực hiện các tác vụ như hiển thị hộp thoại thông báo và lấy tên người dùng. Sau đó, mã kiểm tra xem tên người dùng có khớp với một giá trị cụ thể không. Nếu có thì tạo MessageBoxA
và in ra flag
Our new crazy conspiracy theorist intern, has blocked everyone from the coffee machine because he saw that aliens were trying to steal the "out of the world" secret recipe. Your mission is to unveil the secrets that lie behind his profound madness and teach him a javaluable lesson.
Chúng ta có thể sử dụng lệnh 'file' để cung cấp cái nhìn tổng quan về loại tệp.
Mở chương trình trong IDA64.
JNI (java native interface)
là framework cho phép gọi các hàm java trong JVM(java virtual machine) từ các ngôn ngữ thấp hơn như C++, C, assembly… Cocos2dx cung cấp 1 class singleton để có thể sử dụng JNI đó là JNIHelper
. Để sử dụng JNI_CreateJavaVM
, bạn cần bao gồm tệp jni.h
trong chương trình C của mình. Hàm sẽ có cấu trúc như sau: JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
.
Khi chạy chương trình bình thường, nó sẽ thông báo thiếu thư viện libjvm.so
. Ta thực hiện các bước sau:
sudo apt update
sudo apt install openjdk-17-jdk
sudo gedit ~/.bashrc
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
LD_LIBRARY_PATH=/usr/lib/jvm/java-17-openjdk-amd64/lib/server ./coffee_invocation
Ta đã biết thực chất đây là một chương trình C/C++
sử dụng JNI
để chạy các hàm Java. Ta sẽ xây dựng struct của các function
trong Java. Tôi đã tham khảo:
Ta viết đoạn struct có nội dung sau:
Trong IDA -> View -> Open subviews -> Local types -> Insert
vào tạo struct như trên. Lưu ý, tôi đã dùng tên NULL1, NULL2, NULL3 ,NULL4
để tránh trùng tên khi xây dựng struct
. Hãy chắc rằng bạn Set type
cho _int64 a1
thành JNINativeInterface *env
.
Từ đây ta phân tích chương trình bình thường.
Nếu số lượng tham số nhỏ hơn hoặc bằng 1, chương trình sẽ in ra một thông báo cảnh báo. Ngược lại, nếu có ít nhất một tham số truyền vào, chương trình sẽ gọi hàm sub_1FB9()
với tham số thứ nhất của dòng lệnh.
Tại hàm sub_2121()
chương trình sẽ hiển thị menu
lựa chọn: Normal Coffee
, Espresso
, [REDACTED]
, Exit
. Tất cả đều dẫn đến kết thúc chương trình
Hàm bắt đầu bằng việc gọi hai hàm khác check1
và check2
để kiểm tra quyền truy cập vào cà phê. Nếu bất kỳ một trong số hàm này trả về kết quả khác không (có nghĩa là không có quyền truy cập), hàm sẽ in ra thông báo "No coffee for you!"
và trả về 1.
Nếu không có vấn đề gì với quyền truy cập, hàm sẽ in ra thông báo "Access granted."
và tiếp tục với việc in ra danh sách cà phê có sẵn để lựa chọn. Sau khi in ra danh sách cà phê, hàm sẽ in ra thông báo "Enjoy!"
và flag
.
Hàm check1():
Đầu tiên chúng ta cần vượt qua hàm check1
.
Tại hàm loadShutdownNativeFunctions
, nó gọi RegisterNatives
- cho phép ta thay đổi hàm tại halt0
và runAllFinalizers
. Giải thích sơ qua về hai hàm.
halt0:
thường được gọi sau khi hàm System.exit(status_code)
được thực thi trong chương trình Java. Hàm này sẽ kết thúc Java Virtual Machine
. Tuy nhiên, hàm register_shutdown
sẽ chặn hàm halt0
và chuyển hướng nó đến hàm được trỏ bởi con trỏ on_halt
. Điều này cho phép bạn thực hiện các hành động tùy chỉnh trước khi JVM kết thúc hoàn toàn
.runAllFinalizers:
thường chạy tất cả các finalizer
đã đăng ký trước khi JVM kết thúc
. Finalizer là một phương thức được gọi tự động khi đối tượng không còn được tham chiếu bởi bất kỳ biến nào. Finalizer thường được sử dụng để giải phóng tài nguyên được cấp phát bởi đối tượng. Họ đã không đề cập gì về nó, nên không có thay đổi gì.Thực hiện đặt các byte
từ mangJNI1
vào một biến trong môi trường JVM
Thực hiện đặt các short (2 byte)
từ mangJNI2
vào một biến trong môi trường JVM
Hãy đọc mô tả hoạt động của hàm check1()
ở trong phần mô ta code ở trên, tôi đã comment khá kĩ.
Binwalk
là một công cụ mạnh mẽ và linh hoạt cho phép bạn phân tích, trích xuất và khám phá nội dung ẩn trong các file binary. Ta dùng binwalk
để lấy các file ẩn, với câu lệnh: binwalk -D='.*' ./coffee_invocation
. Sau khi trích xuất được, ta dùng http://www.javadecompilers.com/ để decompile chương trình. Tôi dùng Bytecode Viewer
để xem thêm thông tin về Bytecode Disassembler
Trong lớp Verify1
, thực hiện kiểm tra hai chuỗi. Nó so sánh từng ký tự của hai chuỗi và in ra một thông báo nếu chúng không giống nhau. Nếu một trong hai chuỗi là null
hoặc nếu chiều dài của chúng không giống nhau
, nó cũng in ra một thông báo tương ứng.
Như ta đã phân tích ở trên, hai chuỗi đó chính là password
và check_password_byte
. Ta viết chương trình đề tìm ra nửa đầu của flag
. Chú ý: bytes != Bytes và short != Short
Sau khi lớp Verify1
được thực thi, ánh xạ của Byte và Short được trả về ánh xạ bình thường của chúng.
Hàm check2():
Ta phân tích hàm check2()
, nó cũng gần tương tự như hàm check1()
, nó có hơi phức tạp hơn một chút.
Nó cũng hoán đổi ý nghĩa của True và False
, do đó True trở thành False và False trở thành True
.
Tại hàm loadShutdownNativeFunctions
, halt0
ánh xạ tới hàm sub_190F()
, thay vì thay đổi giá trị Byte và Short
, nó thay đổi giá trị của Characters
. Điều này có thể được thực hiện lên đến 13 lần.
Hãy đọc mô tả hoạt động của hàm check2()
ở trong phần mô ta code ở trên, tôi đã comment khá kĩ.
Có thể thấy, có một vài Boolean
được hoán đổi, System.exit(i+3)
sẽ hoán đổi các bảng giá trị ở mỗi lần lặp của vòng lặp, v.v. Điều này có nghĩa là cứ sau 2 lần lặp lại các ký tự sẽ cần một bảng thay thế khác được lưu trữ trong tệp. Ta viết chương trình đề tìm ra nửa sau của flag
.
Tiến hành nhập flag
ta vừa tìm được.
After more and more recent hits of the infamous Jupiter Banking Malware we finally managed to get a sample of one module. Supposedly it steals secrets from Firefox users?
Chúng ta có thể sử dụng lệnh 'file' để cung cấp cái nhìn tổng quan về loại tệp.
Mở chương trình trong IDA.
Khả năng cao chương trình sẽ hook module
của Firefox. Vì vậy mình đã cài Firefox cho máy tính. Ta có thể quan sát hàm sub_140001000
.
Sử dụng hàm CreateToolhelp32Snapshot
để tạo ảnh chụp nhanh của các tiến trình đang chạy. Lặp qua danh sách các tiến trình và tìm tiến trình firefox.exe
. Hàm CreateRemoteThread
khiến một luồng thực thi mới bắt đầu trong không gian địa chỉ của quy trình đã chỉ định. Thread
có quyền truy cập vào tất cả các đối tượng mà tiến trình mở ra => khả năng cao nó sẽ injected Firefox process
.
Các chương trình mã độc sẽ sử dụng CreateRemoteThread API để có thể chạy mã lệnh trên một tiến trình khác. Ta so sánh giữa CreateThread
và CreateRemoteThread
Cả hai API đều khá giống nhau, tuy nhiên CreateRemoteThread
có nhiều thuộc tính hơn.
hProcess:
lấy handle của tiến trình mà ta sẽ thực thi lệnhlpStartAddress:
con trỏ tới hàm do ứng dụng xác định sẽ được luồng thực thi. Con trỏ này đại diện cho địa chỉ bắt đầu của luồng.lpParameter:
Một con trỏ tới một biến được truyền tới luồng.Cách thức hoạt động: đầu tiên, Injector Process
sẽ tạo nhớ trong không gian địa chỉ ảo và ghi DLL file path
lên vùng nhớ đó. Sau đó, CreateRemoteThread
sẽ gọi LoadLibrary API
(hàm này sẽ cần path của các thư viện API). Khi ta tạo thread trong Target Pcocess
- thread này sẽ trỏ đến LoadLibrary
và load DLL mà ta đã ghi path vào vùng nhớ ở trên => injected DLL
Ta viết đoạn mã IDAPython để XOR
vùng nhớ sub_140017000 và 0x72
. Từ đó ta có thể quan sát hàm sub_140017005
chính xác hơn.
Hàm lấy con trỏ đến danh sách liên kết các module. NtCurrentTeb()
trả về con trỏ đến cấu trúc Thread Environment Block
, từ đó chúng ta truy cập thông tin về tiến trình thông qua ProcessEnvironmentBlock
và danh sách liên kết các module bởi Ldr->InMemoryOrderModuleList
. Tính mã CRC32 của module và so sánh để load module cần dùng
Một kĩ thuật hay hơn mình học được từ bạn kvn11
là mình có thể viết đoạn mã để tạo danh sách các module và tính CRC32. Ta sẽ chạy đoạn mã python để tạo danh sách các module, sau đó đưa danh sách ấy vào mảng name trong đoạn mã C
Ta tính được mã CRC32 là của hàm GetProcessAddress()
. Chương trình lưu GetProcessAddress() vào r12 và sẽ thực hiện gọi nhiều lần.
Ta tiếp tục quan sát hàm sub_14001742D()
, chương trình tiếp tục tính mã CRC32 của các module (ở đây là các *.dll).
Ta chạy đoạn mã python để tạo danh sách các module, sau đó đưa danh sách ấy vào mảng name trong đoạn mã C.
Sau khi đưa danh sách các DLL và tính toán, ta biết được kết ứng với nss3.dll
và là kết quả duy nhất. Mình dự đoán chương trình sẽ chỉ gọi bản sao tên của nss3.dll
nên đã thử và tìm được:
0x3DCE28A2
ứng với NSS3.dll
0x8861d80b
ứng với nss3.dll
0x83bcbe6a
ứng với NSS3.DLL
Sau khi có địa chỉ bắt đầu của thư viện đó, Firefox sẽ lấy địa chỉ của hàm PR_Write
trong Firefox's NSPR - được sử dụng để ghi dữ liệu vào một tệp hoặc socket. Hàm này trả về số byte đã được ghi thành công hoặc -1 nếu có lỗi xảy ra.
Tại hàmsub_1400174B7()
, chương trình thực hiện sao chép từ PR_Write
sẽ được sao chép lên vùng nhớ của tiến trình.
Sau đó chương trình kiểm tra giá trị của RBX
liệu có nhỏ hơn 4 và giá trị tại RDX
có phải là STOP
không. Ta có thể bỏ qua bước kiểm tra bằng cách thay RIP hiện tại.
Để bắt đầu sử dụng API Windows Sockets (ws2_32.dll), chương trình khởi tạo việc sử dụng WS2_32.DLL bằng lệnh gọi tới WSAStartup. Hàm này phải là hàm Windows Sockets đầu tiên được gọi bởi một ứng dụng hoặc DLL, cho biết phiên bản đặc tả Windows Sockets mà ứng dụng hoặc DLL mong muốn sử dụng và bắt đầu sử dụng WS2_32.DLL.
Chương trình gọi socket(2, 2, 17)
để tạo một socket với các tham số cụ thể: Số đầu tiên 2 đại diện cho loại địa chỉ (AF_INET cho IPv4), số thứ hai 2 chỉ loại socket (SOCK_DGRAM cho gói tin datagram), và 17 chỉ định giao thức (IPPROTO_UDP cho giao thức UDP). Lệnh này sẽ tạo một socket UDP để giao tiếp qua IPv4.
Chương trình lấy địa chỉ của hàm sendto
. Tiếp đó, chương trình tiến hành giải mã các byte và gửi dữ liệu đến một đích cụ thể.
Sau khi thực hiện xong, chương trình gọi closesocket
để đóng socket hiện có. Từ đây, ta có thể viết đoạn mã để giải mã các byte được gửi đó.
We've extracted an embedded operating system running on an intercepted deep-space satellitle launched by Arodor. If we can breach the secure enclave and extract their security mechanisms, we can crack their encrypted communications
Chúng ta có thể sử dụng lệnh 'file' để cung cấp cái nhìn tổng quan về loại tệp.
Ta được cung cấp các tệp tin sau:
bzImage
là kernel Linux image, được sử dụng để khởi động hệ điều hành.initramfs.cpio.gz
là hệ thống tập tin ban đầu được nén, chứa các công cụ và tập tin cần thiết để khởi động hệ thống.run.sh
là tập tin script bash, chứa các lệnh để tự động hóa việc khởi động hệ thống hoặc thực hiện các tác vụ khác.Ta biết được đây là thử thách kiểu kernel exploitation
. Đây là lần đầu tiên mình làm dạng bài này nên gặp nhiều khó khăn.
Ta tiến hành giải nén bằng lệnh gunzip -c initramfs.cpio.gz > initramfs.cpio
.
Tệp initramfs.cpio
là một tập tin nén được sử dụng trong quá trình khởi động của hệ điều hành Linux. Nó chứa một hệ thống tệp cơ bản được giải nén vào bộ nhớ RAM trong quá trình khởi động ban đầu. Tệp này bao gồm các chương trình và thư viện cần thiết để khởi động hệ điều hành và thiết lập môi trường người dùng. Ta tiếp tục giải nén tệp initramfs.cpio
bằng lệnh cpio -i -F initramfs.cpio
Trong tất cả các tệp được giải nén, chỉ có tệp checker
và checker.ko
là đáng chú ý. Tệp checker.ko
là một phần của một mô-đun nhân Linux có trách nhiệm quản lý một thiết bị ký tự có tên là checker
. Giá trị nhập vào sẽ được lưu trong biến của tệp checker
và đưa cho tệp checker.ko
thực hiện kiểm tra. Kết quả trả về sẽ được check
in ra thông báo.
Ta dễ dàng tìm ra hàm thực hiện kiểm tra giá trị ta nhập như sau:
Nó chỉ là một phép xor đơn giản, ta viết chương trình để tìm ra giá trị đó.
I am stuck in a maze. Can you lend me a hand to find the way out?
Ta có được ba tệp: enc_maze.zip
, maze.exe
, maze.png
. Mở tệp maze.exe
trong DIE.
Ta biết được tệp này có trình nén là PyInstaller
. Tôi sử dụng pyinstxtractor-ng trích các tệp được nhúng bên trong. Bạn cũng có thể dùng link để thao tác trên web mà không cần tải về.
Ta biết EntryPoint có khả năng là maze.pyc
và pyiboot01_bootstrap.pyc
. Ta sử dụng Uncompyle6 để decompile Python bytecode.
Ta biết được mật khẩu của enc_maze.zip
là Y0u_Ar3_W4lkiNG_t0_Y0uR_D34TH
và chương trình có import obf_path
nên ta cũng hãy cùng xem đoạn mã của nó.
Ta gần như không thể đọc đoạn mã từ đây do đã bị mã hóa, bạn hãy làm theo tại đây để tìm được nội dung đoạn mã gốc.
Chương trình giải mã đầu tiên
Câu lệnh đầu tiên để giải mã
Kết quả giải mã đầu tiên
Chương trình giải mã thứ hai sau khi bỏ chuỗi "__regboss__"
Câu lệnh thứ hai để giải mã
Kết quả giải mã thứ hai
Thực thi đoạn mã trên ta có gợi ý seed = 493
và for i in range(300): randint(32,125)
. Trong đoạn mã của maze.py
, tôi đã thắc mắc vì sao lại dùng mảng key rỗng để giải mã tệp. Nên mình để key = RandomNumberArray
.
Ta nhận được một tệp ELF64. Mở nó trong IDA.
Chương trình kiểm tra dữ liệu nhập đúng hay sai bằng cách tính tổng của ba kí tự kế nhau và so sánh với mảng unk_2060
. Tới đây ta viết đoạn giải mã thôi.