تریس فراخوانی های API
API Call Tracing یکی از روشهایی است که با آن میتوان یک دید کلی و خلاصه از یک فایل اجرایی بدست آورد.
شاید در برخی موارد بررسی فراخوانی های API را مد نظر ما باشد مخصوصا برای فهمیدن رفتار یک Malware .
در اینجا ما در مورد برخی از این تکنیک ها توضیح می دهیم:
1. آنپک کردن برنامه های پک شده
2. نمایش رفتار برنامه
3. پیدا کردن توابعی که برایمان مفید هستند
من از pydbg برای لاگ کردن توابع فراخوانی شده استفاده می کنم, و در آخر هم از IDAPython برای انجام اتوماتیک برخی ازین آنالیزهای دستی.
API Calls Logging with PEfile & PyDbg:
با تکیه بر موارد بالا,ما برای اسکریپتمان به اطلاعات زیر نیازمندیم:
1. آدرس برگشت - از کجا API فراخوانی شده؟
2. نام API - کدام API فراخوانی شده؟
پس ما باید یک breakpoint روی هر فراخوانی تابع قرار بدیم,چونکه به نام API یا آدرس آن احتیاج داریم.اگر ما نام تابع را داشته باشیم از طریق آن میتوانیم آدرس تابع را بدست بیاوریم و نقطه توقف روی آن قرار دهیم, در اینجا, آدرس ما می تواند مستقیما breakpoint روی آن قرار گیرد.اما سوال اینجاست که چطور میتوانیم نام توابع APIرو بدست بیاوریم؟
این مشکل رو ما با PEfile حل کردیم.پس ما اول توابع ورودی رو بدست میاریم و پس از اون با قرار دادن BP روی اینها آدرسهارو نیز بدست میاریم.
اما این روش محدودیت های زیر رو هم دارد:
1. در مورد DLL هایی که از طریق تابع LoadLibrary() لود شوند شکست می خورد.
2. اگر برنامه پک شده باشه پس جدول ورودی در زمان آنپک شدن ایجاد خواهد شد, پس شکست می خورد.
قبل از حل این مشکل اجازه بدید در مورد نحوه ی کار Unpacker Stub یا لودر ها برای ساختن جدول ورودی در هنگام اجرا توضیح مختصر میدم:
به طور معمول آنها از LoadLibrary برای لود کردن DLL و GetProcAddress برای بدست آوردن آدرس API استفاده می کنند.
این 2 تا تابع توسط kernel32.dll اکسپورت شدن, و در داخل هر پروسسی به صورت پیش فرض وجود دارند.
پس اگر ما یک BP روی GetProcAddress قرار بدیم,نام API از پشته قابل دستبابی است. در ادامه یک BP هم روی آدرس بدست آمده از تابع GetProcAddress قرار می دهیم.
اما روش های دیگری هم برای بازسازی جدول ورودی در هنگام اجرا وجود دارد که توسط بدافزارها مورد استفاده قرار میگیرد.
تو اسمبلی یه چیزی شبیه به کد زیر :
push dword ptr fs:[30h] ; PEB pop eax mov eax,[eax+0ch] ; LDR mov ecx,[eax+0ch] ; InLoadOrderModuleList mov edx,[ecx] push edx mov eax,[ecx+30h]
در این روش, لودر ابتدا آدرس مبنای kernel32.dll رو بدست می آورد.
سپس به export table این DLL وارد شده و به دنبال آدرس تابع LoadLibrary میگردد.
پس از این لودر با این تابع همه کتابخانه ی مورد نیاز را لود کرده و با استفاده از روش زیر آدرس API ها را بدست می آورد:
1. GetProcAddress شبیه متد قبلی است
2. حرکت میان export table همه ی DLL های لود شده.
در اینجا ما گام های زیر را برای API Call Tracing انجام میدیم:
1. رفتن به داخل جدول ورودی فایل و قرار دادن BP روی هر API
2. همچنین قرار دادن BP روی تابع GetProcAddress
3. اگر به BP برخورد کردیم و تابع GetProcAddress نبود, پس می توانیم از پشته آدرس برگشت و نام APIرو بدست بیاریم.
4. اگر به تابع GetProcAddress برخوردیم, آدرس برگشت رو از پشته برمیداریم و روی آن BP قرار میدیم.
5. اگر به آدرس برگشت برخوردیم, پس مقدار رجیستر EAX رو بدست میاریم و یک BP روی آن ست میکنیم.
با این روش ما یه اسکریپتی می نویسیم که API رو با آدرس برگشت هارو لاگ می کنه:
import sys,struct import pefile from pydbg import * from pydbg.defines import * def log(str): global fpp print str fpp.write(str) fpp.write("\n") return def addr_handler(dbg): global func_name ret_addr = dbg.context.Eax if ret_addr: dict[ret_addr] = func_name dbg.bp_set(ret_addr,handler=generic) return DBG_CONTINUE def generic(dbg): global func_name eip = dbg.context.Eip esp = dbg.context.Esp paddr = dbg.read_process_memory(esp,4) addr = struct.unpack("L",paddr)[0] addr = int(addr) if addr < 70000000: log("RETURN ADDRESS: 0x%.8x\tCALL: %s" % (addr,dict[eip])) if dict[eip] == "KERNEL32!GetProcAddress" or dict[eip] == "GetProcAddress": try: esp = dbg.context.Esp addr = esp + 0x8 size = 50 pstring = dbg.read_process_memory(addr,4) pstring = struct.unpack("L",pstring)[0] pstring = int(pstring) if pstring > 500: data = dbg.read_process_memory(pstring,size) func_name = dbg.get_ascii_string(data) else: func_name = "Ordinal entry" paddr = dbg.read_process_memory(esp,4) addr = struct.unpack("L",paddr)[0] addr = int(addr) dbg.bp_set(addr,handler=addr_handler) except: pass return DBG_CONTINUE def entryhandler(dbg): getaddr = dbg.func_resolve("kernel32.dll","GetProcAddress") dict[getaddr] = "kernel32!GetProcAddress" dbg.bp_set(getaddr,handler=generic) for entry in pe.DIRECTORY_ENTRY_IMPORT: DllName = entry.dll for imp in entry.imports: api = imp.name address = dbg.func_resolve(DllName,api) if address: try: Dllname = DllName.split(".")[0] dll_func = Dllname + "!" + api dict[address] = dll_func dbg.bp_set(address,handler=generic) except: pass return DBG_CONTINUE def main(): global pe, DllName, func_name,fpp global dict dict = {} file = sys.argv[1] fpp = open("calls_log.txt",'a') pe = pefile.PE(file) dbg = pydbg() dbg.load(file) entrypoint = pe.OPTIONAL_HEADER.ImageBase + pe.OPTIONAL_HEADER.AddressOfEntryPoint dbg.bp_set(entrypoint,handler=entryhandler) dbg.run() fpp.close() if __name__ == '__main__': main()
و خروجی کد بالا:
RETURN ADDRESS: 0x004030e8 CALL: kernel32!GetModuleHandleA RETURN ADDRESS: 0x004030f3 CALL: kernel32!GetCommandLineA RETURN ADDRESS: 0x00404587 CALL: kernel32!GetModuleHandleA RETURN ADDRESS: 0x00404594 CALL: kernel32!GetProcAddress RETURN ADDRESS: 0x004045aa CALL: kernel32!GetProcAddress RETURN ADDRESS: 0x004045c0 CALL: kernel32!GetProcAddress
حالا چندتا سناریو رو توی دنیای واقعی بررسی می کنیم.
1) Unpacking UPX using API Call Tracing
خروجی زیر از یک فایل پک شده با UPX هستش:
RETURN ADDRESS: 0x00784b9e CALL: GetProcAddress RETURN ADDRESS: 0x00784b9e CALL: GetProcAddress RETURN ADDRESS: 0x00784b9e CALL: GetProcAddress RETURN ADDRESS: 0x00784b9e CALL: GetProcAddress RETURN ADDRESS: 0x00784b9e CALL: GetProcAddress RETURN ADDRESS: 0x00784bc8 CALL: KERNEL32!VirtualProtect RETURN ADDRESS: 0x00784bdd CALL: KERNEL32!VirtualProtect --> 1 RETURN ADDRESS: 0x0045ac09 CALL: GetSystemTimeAsFileTime --> 2 RETURN ADDRESS: 0x0045ac15 CALL: GetCurrentProcessId RETURN ADDRESS: 0x0045ac1d CALL: GetCurrentThreadId RETURN ADDRESS: 0x0045ac25 CALL: GetTickCount RETURN ADDRESS: 0x0045ac31 CALL: QueryPerformanceCounter RETURN ADDRESS: 0x0044e99f CALL: GetStartupInfoA RETURN ADDRESS: 0x0044fd9c CALL: HeapCreate
در اینجا API در مکان 1 آدرس برگشت 0x00784bdd و API در مکان دوم آدرس 0x0045ac09 رو دارد.
تفاوت میان این دوتا آدرس نشان دهنده ی این است که آدرس 0x0045ac09 در تابعی که محتوی OEP هستش قرار داره.
در زیر بررسی می کنیم:
2) Binary Behaviour Profiling
بدست آوردن نمایی از رفتار برنامه.
به خروجی زیر نگاه کنید, چه چیزی برداشت می کنید؟
RETURN ADDRESS: 0x004012ce CALL: msvcrt!fopen --> 1 RETURN ADDRESS: 0x00401311 CALL: msvcrt!fseek RETURN ADDRESS: 0x0040131c CALL: msvcrt!ftell RETURN ADDRESS: 0x0040133a CALL: msvcrt!fseek RETURN ADDRESS: 0x00401346 CALL: msvcrt!malloc --> 2 RETURN ADDRESS: 0x00401387 CALL: msvcrt!fread --> 3 RETURN ADDRESS: 0x00401392 CALL: msvcrt!fclose RETURN ADDRESS: 0x004013b4 CALL: KERNEL32!OpenProcess --> 4 RETURN ADDRESS: 0x004013ee CALL: KERNEL32!VirtualAllocEx --> 5 RETURN ADDRESS: 0x00401425 CALL: KERNEL32!WriteProcessMemory --> 6 RETURN ADDRESS: 0x0040146b CALL: KERNEL32!CreateRemoteThread --> 7 RETURN ADDRESS: 0x004014a4 CALL: msvcrt!exit
این کاملا مشخصه که این فایل باینری در حال خواندن یک فایل و همچنین تزریق کد به داخل پروسس دیگری هستش.
3) Finding Interesting Functions
پیدا کردن توابع دلخواه
RETURN ADDRESS: 0x00443c29 CALL: inet_ntoa --> point 1 RETURN ADDRESS: 0x0044a6ee CALL: KERNEL32!HeapAlloc RETURN ADDRESS: 0x00446866 CALL: KERNEL32!GetLocalTime RETURN ADDRESS: 0x0044a6ee CALL: KERNEL32!HeapAlloc RETURN ADDRESS: 0x00443f79 CALL: socket --> point 2 RETURN ADDRESS: 0x00443fb5 CALL: setsockop RETURN ADDRESS: 0x00443fd0 CALL: setsockopt RETURN ADDRESS: 0x00444045 CALL: ntohl RETURN ADDRESS: 0x0044404f CALL: ntohs RETURN ADDRESS: 0x00444063 CALL: bind --> point 3 RETURN ADDRESS: 0x0044412c CALL: ntohl RETURN ADDRESS: 0x0044413c CALL: ntohs RETURN ADDRESS: 0x0043adf6 CALL: WSAAsyncSelect RETURN ADDRESS: 0x0044416b CALL: connect --> point 4 RETURN ADDRESS: 0x00444176 CALL: WSAGetLastError RETURN ADDRESS: 0x00441979 CALL: USER32!DispatchMessageA RETURN ADDRESS: 0x00444ce0 CALL: KERNEL32!GetTickCount RETURN ADDRESS: 0x00444cfa CALL: KERNEL32!QueryPerformanceCounter RETURN ADDRESS: 0x00444499 CALL: recv --> point 5 RETURN ADDRESS: 0x0044a8c6 CALL: KERNEL32!HeapFre RETURN ADDRESS: 0x0043adf6 CALL: WSAAsyncSelect RETURN ADDRESS: 0x004441f7 CALL: closesocket RETURN ADDRESS: 0x0044a8c6 CALL: KERNEL32!HeapFree
جاهایی رو که مشخص کردم توابعی هستند که برای فعالیت های شبکه ای در نظر گرفته شدن.
Extending API Tracing with IDAPython
حالا از لاگی که توسط اسکریپت بالا ساخته میشه می تونیم استفاده کنیم و با اسکریپت زیر که برای IDAPython هستش این توابع رو با رنگ مشخص کنیم.
from idaapi import * from idc import * import sys class logparse(): def __init__(self,file_path): self.file_path = file_path self.fp = open(self.file_path,'r') self.data = self.fp.readlines() def parser(self): dict = {} for line in self.data: line_slice = line.split() address = line_slice[2] name = line_slice[4] dict[address] = name for ea in dict.keys(): print dict[ea] ea_c = PrevHead(ea) SetColor(ea_c,CIC_ITEM,0x8CE6F0) return def main(): file_path = AskFile(0,"*.*","Enter file name: ") logobj = logparse(file_path) logobj.parser() return if __name__ == '__main__': main()