السلام عليكم ورحمة الله وبركاته
قرأت مقالة عن تتبع الدوال في : http://securityxploded.com/api-call-tracing-with-pefile-pydbg-and-idapython.php أعجبتني وقمت بترجمتها ببعض التصرف ربما يجدها أحد مفيدة.
أسماء الأدوات :
ـ PEfile : وحدة بايثون لقراءة والتعامل مع ملفات PE - Portable Executable (ملفات exe, dll, sys).
ـ PyDbg : مكتبة تنقيح مكتوبة ببايثون تمكنك من عمل سكربتات لأغراض التنقيح.
ـ IDAPython : مشروع واجهة برمجية لمنقح IDA لعمل سكربتات وإضافات ببايثون . مُدرج تلقائياً في النسخ الحديثة من منقح IDA.
تتبع الدوال
تتبع الدوال API call tracing أحد التقنيات الفعالة التي تعطيك نظرة شاملة عن البرنامج. فأحياناً كل مايلزمك أن تقرأ سجل log تتبُع دوال البرنامج كي تفهم عمله. غالباً ما أستخدم هذه التقنية لمهام تحليل البرامج الضارة.
في هذه المقالة سأناقش بعضاً من أساليب تتبع الدوال. بعض المهام التي يُمكنك القيام بها عن طريق تقنية تتبع الدوال :
- ضغط وفك ضغط الملفات التنفيذية.
- أخذ نظرة على طريقة عمل البرنامج.
- العثور على الدوال التي تهمك في البرنامج.
سأستخدم سكربت PyDbg لعمل سجل بإستدعاءات الدوال ومن ثُم سأسخدم سكربت IDAPython لعمل تحليل يدوي بإستخدام المنقح IDA.
تسجيل إستدعاءات الدوال بإستخدام PEfile وPyDbg
سيلزمنا عمل سكربت يقوم بمهمتين:
- الحصول على عنوان العودة : من أين إستُدعيت الدالة؟
- إسم الدالة : ما إسم الدالة التي إستُدعيت؟
هذا يعني أنه سيلزمنا وضع نقطة توقف عند كُل دالة في البرنامج ولهذا الغرض فسنحتاج إسم تلك الدالة أو عنوانها. إذا كان لدينا إسم الدالة, فيُمكننا الحصول على عنوان الدالة ووضع نقطة توقف عند ذلك العنوان. وفي حال كان لدينا عنوان دالة ما, فسنقوم مباشرة وبوضع نقطة توقف عند هذا العنوان. لكن يبقى علينا أن نحصل على إسم تلك الدالة.
يُمكن عمل هذا بإستخدام PEfile. سنقوم عن طريقه بسرد محتويات جدول إستيراد العناوين في الملف التنفيذي Import Address Table ومن منه سنحصل على إسم الدالة.
لهذه الطريقة عيبين :
- أنها لاتستطيع تحديد الدوال التي يستدعيها البرنامج أثناء عمله من مكتبة dll عن طريق الدالة LoadLibrary.
- في حال كان الملف التفيذي مضغوط, فإن فك ضغط الملف التفيذي سينشيء جدول إستيراد العناوين أثناء عمل البرنامج.
قبل حل تلك المُشكلتين, لنتحدث عن الطريقة التي تستخدمها برامج الضغط لبناء جدول إستيراد العناوين أثناء تشغيل البرنامج.
أغلبها تقوم بإستخدام الدالة LoadLibrary لتحميل مكتبة الـdll وGetProcAddress للحصول على عنوان الدالة. تُستورد تلك الدالتين من الملف kernel32.dll والتي يُحملها Windows مع كُل البرامج. فلو قمنا بوضع نقطة توقف عند الدالة 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]
هذه صورة لبُنية الـPEB لأحد العمليات (إستخرجت بإستخدام Windbg) :

في هذه الطريقة, يتم عمل مُحمّل يقوم أولاً بتحديد عنوان المكتبة kernel32.dll ,وهو ثاني عنوان بعد عنوان المكتبة ntdll.dll في القائمة الموجودة في InLoadOrderModuleList, وبعدها يتم البحث في جدول إستيراد العنوانين حتى يُعثر على عنوان الدالة LoadLibrary. بعد هذه الخطوة, سيقوم المُحمّل بتحميل جميع مكتبات الـdll التي يحتاجها البرنامج وسيقوم بإستخراج عناوين الدوال بإستخدام إحدى هاتين الطريقتين :
- عن طريق GetProcAddress: مثل الطريقة السابقة.
- عن طريق البحث في جدول إستيراد العناوين لكل مكاتب الـdll التي يحتاجها البرنامج.
حان وقت كتابة السكربت. السكربت سيقوم الخطوات التالية بالترتيب:
- سيبحث في جدول العناوين للبرنامج ويضع نقطة توقف عند كُل دالة.
- سيضع أيضاً نقطة توقف عند الدالة GetProcAddress.
- إذا توقف عند نقطة توقف ولم تكن عند الدالة GetProcAddress, فسنجعل السكربت يستخرج عنوان العودة من المكدس ويكتبه في السجل log مع إسم الدالة.
- أما لو وصل لنقطة توقف وكانت عند GetProcAddress, فسنستخرج إسم الدالة من المُكدس وعنوان العودة وسنضع نقطة توقف عند عنوان العودة.
- إذا توقفنا عند عنوان عودة, فسنستخرج قيمة المسجل eax والتي تحتوي عنوان العودة وسنضع نقطة توقف عنده.
حسب هذا المُخطط سنكتب سكربت PyDbg يسجل إسم كل دالة مع عنوان العودة :
'''
Author: Amit Malik
http://www.securityxploded.com
'''
import sys
import 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
حسناً, لنبطق الفكرة على أمثلة واقعية.
فك ضغط UPX بإستخدام تقنية تتبع الدوال
لدينا سجل لأحد البرامج المضغوطة بـUPX. دقق النظر فيه, هل يُمكنك تحديد أي دالة تحتوي نقطة الإدخال الأصلية OEP - original entry point؟
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
لاحظ أن الدالة المشار إليها بـ1 عنوان عودتها 0x00784bdd والدالة التي تليها والمشار إليها بـ2 عنوان عودتها 0x0045ac09. الفرق بين العناوين كبير جداً ممايدل أن العنوان 0x0045ac09 عنوان نقطة الإدخال الأصلية.
يُمكنك التأكّد من هذا بتشغيل البرنامج تحت Ollydbg وسيظهر كما في الصُورة:

أكثر البرامج الضارة هذه الأيام تستخدم ضغط خاص مكتوب لها وعني وجدت أن هذه التقنية فعالة كثيراً في فك الضغط عنها.
أخذ نظرة على طريقة عمل البرنامج
إنظر لسجل تتبع هذا البرنامج ودقق النظر به, هل يُمكنك معرفة مايقوم به البرنامج؟
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
من الواضح أنّ البرنامج يقُوم بقراءة أحد الملفات وحقن كُود في ذاكرة أحد العمليات.
العثور على الدوال التي تهمك في البرنامج
لدينا هنا سجل لبرنامج آخر :
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
السطور المشار إليها تظهر دوال للشبكات وتكشف أن للبرنامج نشاطات شبكية.
# توسيع الإستخدام مع IDAPython
يُمكننا إستخدام عناواين الدوال الموجودة في ملف السجل لتمييز الدوال داخل IDA. هذا سكربت IDAPython سيقوم بقراءة ملف السجل الذي في الأعلى ثُم سيُلون تلك إستدعاءات تلك الدوال :
'''
Author: Amit Malik
http://www.securityxploded.com
'''
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()ختاماً
في هذا المقال تعلمت كيف تقُوم بعمل تتبع للدوال بإستخدام سكربتات PyDbg وIDAPython والقيام بمهام مفيدة مثل فك الضغط ومراقبة تصرف البرامج وإكتشاف الدوال المُميزة إلى آخره.
هناك العديد من التطبيقات الأخرى والمفيدة للتبع الدوال وهذا المقال ليس سوى دليل للبدئ.
بالتوفيق