en
Feedback

Don't get caught by a cheater! Telemetrio finds and tags such channels 👉 If you want to see the tag, subscribe

ReverseEngineering

ReverseEngineering

Open in Telegram
1 442
Subscribers
No data24 hours
+1087 days
+11030 days
Posts Archive
[rbp-20h] ↓ +5 ↓ ×3 ↓ [rbp-18h] Now we need to see where [rbp-18h] is used later If we end up with something like this mov eax, [rbp-18h] ret We understand that this value is part of the data path that leads to the function output Combine Forward and Backward Here's where things get more interesting Assume the function input is in EDI We start from the input Forward Slicing EDI ↓ EAX ↓ ADD ↓ IMUL ↓ SUB ↓ RETURN Now on the other hand we go back from RETURN Backward Slicing RETURN ↑ SUB ↑ IMUL ↑ ADD ↑ EAX ↑ EDI If these two analyses reach a common path We are more confident that we found the main Data Flow path correctly EDI ↓ ADD ↓ IMUL ↓ SUB ↓ RETURN What to look for in Ghidra When you are doing Program Slicing on a real function, there are a few things that are very important Decompiler Function Graph References Register Usage Memory References Variable Definitions Variable Uses But the two most important questions are Where is this value created And Where is this value used next These two questions seem simple, but they are the basis of a lot of serious Reverse Engineering analysis Every time you see a Register or Memory Location, you should follow this relationship Definition ↓ Value ↓ Use ↓ New Value ↓ Next Use A slightly more realistic example Let's say we have this function C++ int verify(int input) { int x = input ^ 0x55; int y = x + 0x1234; int noise = 777; noise *= 4; int z = y ^ 0xAA; return z == 0x5678; } Here our goal is to understand the last condition lua return z == 0x5678; After return, we go back first z then y then x and finally input the path return ↑ comparison ↑ z ↑ y ↑ x ↑ input If we add the calculations input ↓ XOR 0x55 ↓ + 0x1234 ↓ XOR 0xAA ↓ compare 0x5678 ↓ return But this part noise = 777 noise *= 4 has nothing to do with z and the final condition So for the analysis of this condition, it can be outside our Slice for now Why is this important to us Because in a real program, a function may have hundreds or even thousands of instructions But what we are really looking for may be just this Input ↓ Transformation ↓ Comparison ↓ Decision Slicing It helps to separate this path from a large amount of code Especially when dealing with things like this We will Obfuscation Junk Code Function Complexity Chain of Calculations Multiple Conditions The function may look very busy But when we follow the Data Flow, we will find that only a few instructions actually affect the result that matters to us Exercise: Examine this function int verify(int input) { int a = input ^ 0x31; int junk1 = 1234; junk1 += 55; int b = a * 7; int junk2 = 999; junk2 ^= 0x44; int c = b - 12; return c == 0x500; } Extract two slices First, the Backward Slice from return That is, start here return ↑ ? ↑ ? ↑ ? ↑ input Then find the Forward Slice from input input ↓ ? ↓ ? ↓ ? ↓ comparison ↓ return In the end, you should be able to get the main path to look something like this input ↓ XOR 0x31 ↓ × 7 ↓ - 12 ↓ comparison 0x500 ↓ return And these two parts junk1 junk2 should not go into the Slice for this output because they have no effect on the final result Now there is a more important step Do the same analysis on Assembly, there is no more clean and beautiful input and result names You have to follow the Registers and Memory Locations yourself and reconstruct the Data Flow from the Instructions This is where the difference between just reading the Decompiler and really analyzing the binary becomes clear @reverseengine

Program Slicing With Ghidra So far we have learned Backward Slicing and Forward Slicing Now it is time to implement these concepts on a real binary The goal is that when we see a busy function full of variables and instructions, we do not need to go through the entire function We just isolate the data path that is important to us and follow that A simple program to test For example, we have this code C #include <stdio.h> int check(int input) { int a = input + 5; int b = a * 3; int junk = 900; junk ^= 123; int result = b - 7; return result; } int main() { int x = 10; printf("%d\n", check(x)); return 0; } ` If input is 10, the calculations go like this input = 10 ↓ a = 10 + 5 = 15 ↓ b = 15 × 3 = 45 ↓ result = 45 - 7 = 38 Finally, return returns the value 38 But we also have another variable in the middle junk = 900 junk ^= 123 This value has no effect on the output And this is exactly what we want to find with Program Slicing We import the binary into Ghidra First, we compile the program and open the executable file in Ghidra Then we go to the function check Here, two parts are very important for us Decompiler Function Graph Decompiler Helps us to understand the general logic of the function more easily Function Graph also helps to see the different paths of function execution and the relationship between Basic Blocks But be careful Decompiler Itself is the absolute truth No What we see is Ghidra reconstruction from binary For serious analysis, we need to go back to Assembly and examine the Data Flow itself First we find the Return Value Suppose the Decompiler shows something like this C int check(int input) { int a; int b; int junk; int result; a = input + 5; b = a * 3; junk = 900; junk = junk ^ 123; result = b - 7; return result; } Now the main question is What things affect return Here we start with Backward Slicing We go back from Return The last part is lua return result; So the first thing we need to check is result result is created here result = b - 7; So our dependency is now return ↑ result ↑ b Now we ask where b came from b = a * 3; So return ↑ result ↑ b ↑ a Now a a = input + 5; As a result, we get return ↑ result ↑ b ↑ a ↑ input If we also put the calculations in the path input ↓ +5 ↓ ×3 ↓ -7 ↓ return So the Slice related to the output of this program is almost the same path Now let's examine the Junk Code We have this part C int junk = 900; junk ^= 123; But nowhere is the result of junk included in the result calculation So its path is like this junk ↓ junk ^ 123 ↓ X And there is no path to result As a result, when our target is return junk it is not included in the Backward Slice related to return This is where we see why Slicing is useful Instead of considering all the function instructions as equal We only keep the instructions that depend on the data we want Let's do the same thing in Assembly Assuming the compiler has converted the function to something like this mov eax, edi add eax, 5 imul eax, 3 sub eax, 7 ret Now we look from the bottom up The ret instruction returns the return value of the function In this example, the output value is in EAX So we go to the previous instruction sub eax, 7 That is, the EAX value still affects the output Before that imul eax, 3 Again, the same value We have EAX which goes into the next calculation add eax, 5 Again EAX is part of the data path And finally mov eax, edi Here it is clear that the initial value of EDI has entered this path So our Slice in Assembly becomes EDI ↓ EAX ↓ EAX + 5 ↓ EAX × 3 ↓ EAX - 7 ↓ RET Here we are following the real Data Flow at the Instruction level A very important point In real binary, there is usually no more names like input result a b You may only see things like RAX RBX RCX RDX [rbp-0x20] [rbp-0x18] This is where our work begins We have to reconstruct the relationship between these data ourselves For example mov eax, [rbp-20h] add eax, 5 imul eax, 3 mov [rbp-18h], eax We can mentally convert this path to

[rbp-20h] ↓ +5 ↓ ×3 ↓ [rbp-18h] حالا باید ببینیم [rbp-18h] بعدا کجا استفاده میشه اگر در اخر به چیزی شبیه این برسیم mov eax, [rbp-18h] ret میفهمیم این مقدار بخشی از مسیر داده‌ای هست که به خروجی تابع میرسه Forward و Backward رو با هم ترکیب کنیم اینجا کار جالب‌تر میشه فرض میکنیم ورودی تابع در EDI قرار داره از ورودی شروع میکنیم Forward Slicing EDI ↓ EAX ↓ ADD ↓ IMUL ↓ SUB ↓ RETURN حالا از طرف دیگه از RETURN برمیگردیم عقب Backward Slicing RETURN ↑ SUB ↑ IMUL ↑ ADD ↑ EAX ↑ EDI اگر این دو تحلیل به یه مسیر مشترک برسن اطمینان بیشتری داریم که مسیر اصلی Data Flow رو درست پیدا کردیم EDI ↓ ADD ↓ IMUL ↓ SUB ↓ RETURN داخل Ghidra به چی نگاه کنیم وقتی دارید Program Slicing رو روی یه تابع واقعی انجام میدید چند چیز خیلی مهمن Decompiler Function Graph References Register Usage Memory References Variable Definitions Variable Uses ولی دو سوال از همه مهم‌ترن این مقدار کجا ساخته میشه و این مقدار بعدا کجا استفاده میشه این دو سوال ساده به نظر میرسن ولی پایه خیلی از تحلیل‌های جدی Reverse Engineering هستن هر بار یه Register یا Memory Location دیدید باید همین رابطه رو دنبال کنید Definition ↓ Value ↓ Use ↓ New Value ↓ Next Use یه مثال کمی واقعی‌تر فرض کنیم این تابع رو داریم C++ int verify(int input) { int x = input ^ 0x55; int y = x + 0x1234; int noise = 777; noise *= 4; int z = y ^ 0xAA; return z == 0x5678; } اینجا هدف ما فهمیدن شرط اخره lua return z == 0x5678; پس از return به عقب برمیگردیم اول z بعد y بعد x و در اخر input مسیر میشه return ↑ comparison ↑ z ↑ y ↑ x ↑ input اگر محاسبات رو هم اضافه کنیم input ↓ XOR 0x55 ↓ + 0x1234 ↓ XOR 0xAA ↓ compare 0x5678 ↓ return ولی این قسمت noise = 777 noise *= 4 هیچ ارتباطی با z و شرط نهایی نداره پس برای تحلیل این شرط فعلا میتونه خارج از Slice ما قرار بگیره چرا این موضوع برای ما مهمه چون توی برنامه واقعی ممکنه یه تابع صدها یا حتی هزاران Instruction داشته باشه ولی چیزی که واقعا دنبالش هستیم ممکنه فقط این باشه Input ↓ Transformation ↓ Comparison ↓ Decision Slicing کمک میکنه همین مسیر رو از بین حجم زیادی از کد جدا کنیم مخصوصا وقتی با چیزهایی مثل این روبه‌رو میشیم Obfuscation Junk Code Function Complexity Chain of Calculations Multiple Conditions ممکنه ظاهر تابع خیلی شلوغ باشه ولی وقتی Data Flow رو دنبال کنیم بفهمیم فقط چند دستور واقعا روی نتیجه‌ای که برامون مهمه تاثیر دارن تمرین: این تابع رو بررسی کنید int verify(int input) { int a = input ^ 0x31; int junk1 = 1234; junk1 += 55; int b = a * 7; int junk2 = 999; junk2 ^= 0x44; int c = b - 12; return c == 0x500; } دو تا Slice استخراج کنید اول Backward Slice از return یعنی از اینجا شروع کنید return ↑ ? ↑ ? ↑ ? ↑ input بعد Forward Slice رو از input پیدا کنید input ↓ ? ↓ ? ↓ ? ↓ comparison ↓ return در اخر باید بتونید مسیر اصلی رو تقریبا به این شکل دربیارید input ↓ XOR 0x31 ↓ × 7 ↓ - 12 ↓ comparison 0x500 ↓ return و این دو قسمت junk1 junk2 نباید وارد Slice مربوط به این خروجی بشن چون روی نتیجه نهایی تاثیری ندارن حالا یه مرحله مهم‌تر هم وجود داره همین تحلیل رو روی Assembly انجام بدید اونجا دیگه خبری از اسم‌های تمیز و قشنگ input و result نیست باید خودتون Register ها و Memory Location ها رو دنبال کنید و Data Flow رو از روی Instruction ها بازسازی کنید اینجاست که تفاوت بین فقط خوندن Decompiler و واقعا تحلیل کردن باینری کم‌ کم مشخص میشه @reverseengine

Program Slicing با Ghidra تا اینجا Backward Slicing و Forward Slicing رو یاد گرفتیم حالا وقتشه همین مفاهیم رو روی یه باینری واقعی اجرا کنیم هدف اینه که وقتی یه تابع شلوغ و پر از متغیر و دستور دیدیم لازم نباشه کل تابع رو زیر و رو کنیم فقط مسیر داده‌ای که برامون مهمه رو جدا می‌کنیم و همون رو دنبال می‌کنیم یه برنامه ساده برای آزمایش مثلا این کد رو داریم C #include <stdio.h> int check(int input) { int a = input + 5; int b = a * 3; int junk = 900; junk ^= 123; int result = b - 7; return result; } int main() { int x = 10; printf("%d\n", check(x)); return 0; } ` اگه input برابر 10 باشه محاسبات این شکلی پیش میرن input = 10 ↓ a = 10 + 5 = 15 ↓ b = 15 × 3 = 45 ↓ result = 45 - 7 = 38 در نهایت return مقدار 38 رو برمیگردونه ولی این وسط یه متغیر دیگه هم داریم junk = 900 junk ^= 123 این مقدار هیچ تاثیری روی خروجی نداره و دقیقا همین چیزیه که میخوایم با Program Slicing پیدا کنیم باینری رو وارد Ghidra میکنیم اول برنامه رو کامپایل میکنیم و فایل اجرایی رو داخل Ghidra باز میکنیم بعد میریم سراغ تابع check اینجا دو بخش برای ما خیلی مهم هستن Decompiler Function Graph Decompiler کمک میکنه منطق کلی تابع رو راحت تر بفهمیم Function Graph هم کمک میکنه مسیرهای مختلف اجرای تابع و ارتباط بین Basic Blockها رو ببینیم ولی حواسمون باشه Decompiler خودش حقیقت مطلق نیست چیزی که میبینیم بازسازی Ghidra از باینریه برای تحلیل جدی باید در صورت نیاز برگردیم به Assembly و خود Data Flow رو بررسی کنیم اول Return Value رو پیدا میکنیم فرض میکنیم Decompiler چیزی شبیه این نشون بده C int check(int input) { int a; int b; int junk; int result; a = input + 5; b = a * 3; junk = 900; junk = junk ^ 123; result = b - 7; return result; } حالا سوال اصلی اینه چه چیزهایی روی return تاثیر گذاشتن اینجا از Backward Slicing شروع میکنیم از Return به عقب برمیگردیم آخرین قسمت اینه lua return result; پس اولین چیزی که باید بررسی کنیم result هست result اینجا ساخته شده result = b - 7; پس وابستگی ما فعلا اینه return ↑ result ↑ b حالا میپرسیم b از کجا اومده b = a * 3; پس return ↑ result ↑ b ↑ a حالا a a = input + 5; در نتیجه میرسیم به return ↑ result ↑ b ↑ a ↑ input اگه محاسبات رو هم داخل مسیر بذاریم input ↓ +5 ↓ ×3 ↓ -7 ↓ return پس Slice مربوط به خروجی این برنامه تقریبا همین مسیره حالا Junk Code رو بررسی کنیم این قسمت رو داریم C int junk = 900; junk ^= 123; ولی هیچ جا نتیجه junk وارد محاسبه result نشده پس مسیرش این شکلیه junk ↓ junk ^ 123 ↓ X و هیچ مسیری به result نداره در نتیجه وقتی هدفمون return هست junk داخل Backward Slice مربوط به return قرار نمیگیره اینجاست که میبینیم Slicing چرا مفیده به جای اینکه تمام دستورهای تابع رو هم‌ وزن ببینیم فقط دستورهایی رو نگه میداریم که به داده موردنظر ما وابستگی دارن همین کار رو روی Assembly انجام بدیم فرض میکنیم کامپایلر تابع رو تقریبا به این شکل تبدیل کرده mov eax, edi add eax, 5 imul eax, 3 sub eax, 7 ret حالا از پایین به بالا نگاه میکنیم دستور ret مقدار برگشتی تابع رو برمیگردونه داخل این مثال مقدار خروجی داخل EAX قرار داره پس میریم دستور قبل sub eax, 7 یعنی مقدار EAX هنوز روی خروجی تاثیر داره قبل از اون imul eax, 3 باز هم همون مقدار EAX رو داریم که وارد محاسبه بعدی میشه بعد add eax, 5 باز هم EAX بخشی از مسیر داده است و در اخر mov eax, edi اینجا مشخص میشه مقدار اولیه از EDI وارد این مسیر شده پس Slice ما در Assembly میشه EDI ↓ EAX ↓ EAX + 5 ↓ EAX × 3 ↓ EAX - 7 ↓ RET اینجا دیگه داریم Data Flow واقعی رو در سطح Instruction دنبال میکنیم یه نکته خیلی مهم در باینری واقعی معمولا دیگه خبری از اسم‌هایی مثل اینا نیست input result a b ممکنه فقط چیزایی مثل این ببینیم RAX RBX RCX RDX [rbp-0x20] [rbp-0x18] اینجاست که کار ما شروع میشه باید خودمون رابطه بین این داده‌ ها رو بازسازی کنیم مثلا mov eax, [rbp-20h] add eax, 5 imul eax, 3 mov [rbp-18h], eax میتونیم این مسیر رو ذهنی تبدیل کنیم به

Valid data[0] ─┐ data[1] │ data[2] │ data[3] ─┘ Invalid data[10] → Out of bounds We call this situation "out of bounds read" That is, the program is reading from outside the range intended for the array Now from the perspective of a reverse engineer Suppose we have a Crash and the Stack Trace shows this Process number 0 Original number 1 So we go into "process" Empty process (int index) { int data[4]; printf("%d\n", data[index]); } Here we quickly look for patterns like this Array[index] Next we need to figure out where "index" came from Index ↓ Where did it come from ↓ Confirmed ↓ Was the upper bound checked? For example, if there is access to such a check if (index < 0 || index >= 4) return; The program will prevent this situation before accessing invalid But if there are no such checks Index ↓ Array[index] ↓ Check without restrictions ↓ Access out of bounds The probability of a bug is much higher The main goal of Crash Triage In the end, we want to reduce thousands of crashes to a few limited and checkable ones For example 1000 crashes ↓ 250 crash groups ↓ 20 unique crashes ↓ 7 root causes ↓ 7 bugs That is, the job of the Fuzzer is to try a lot of different inputs and find unusual behaviors But then we need to understand what exactly these behaviors are This is where we get into the story Fuzzer ↓ Crash ↓ Crash triage ↓ Crash non-recurrence ↓ Root cause analysis In essence, the Fuzzer says Something strange happened somewhere in the program We need to understand Where Why And exactly what caused it This is exactly where Fuzzing and we connect @reverseengine

Part 30 Buffer Overflow
Crash Triage So far we have learned to find a general crash with Fuzzing But here we have a very big problem Fuzzer may generate hundreds or even thousands of different crashes for just one bug So after Fuzzing we need to figure out Which crash is really important Which crashes are actually related to a common bug Where exactly in the program did the crash occur And most importantly What was the root cause of the crash We call this stage Crash Triage That is, sorting and examining the crashes to find out which ones are really worth investigating The first thing we examine Suppose the Fuzzer caused a crash with these inputs AAAA BBBBBBBB test123 AAAAAAAAAAAAAA hello_world We should not quickly conclude that we have 5 bugs here It is possible that all five inputs ultimately lead to a specific command in the program Get For example 0x401234: mov byte ptr [rax], dl And this command causes a Crash So what is important to us is not just the input file We need to see where the Crash occurred and under what conditions That is Input ↓ Program ↓ Fault ↓ Where did it happen ↓ Why did it happen 2 Faulting Instruction One of the most important things to find when investigating a Crash is "Faulting Instruction" That is, the instruction that the CPU was executing when the Exception or Fault occurred For example Program received signal SIGSEGV RIP = 0x401234 0x401234: mov byte ptr [rax], dl Here, just knowing that the program gave us "SIGSEGV" is not enough We need to ask What value did RAX have? Why was this value invalid? Where did this value come from? For example "RAX" may point to an invalid address So instead of just saying Program crashed we need to go a step deeper Crash ↓ Faulting Instruction ↓ Invalid Register / Memory ↓ Why did this happen Crash itself is not the cause Crash is the result of a previous event 3 Stack Trace Then we go to the "Stack Trace" For example #0 0x401234 in process_data() #1 0x401567 in handle_input() #2 0x401789 in main() That means the path the program took to Crash was roughly main() ↓ handle_input() ↓ process_data() ↓ CRASH This information is very important for us Because now we know which part of the program we should check first For example, we can go to "process_data()" in IDA or Ghidra and see exactly what happened before the Crash Crash Deduplication Now suppose the Fuzzer gives us a thousand crashes If we have information like this for each crash Signal Faulting Address Faulting Instruction Stack Trace We can put similar crashes together For example Crash 1 → process_data → 0x401234 Crash 2 → process_data → 0x401234 Crash 3 → process_data → 0x401234 Crash 4 → parse_packet → 0x402010 The first to third crashes are probably in the same family Crash 1 Crash 2 Crash 3 ↓ Crash Family A But the fourth crash has a different path Crash 4 ↓ Crash Family B Of course, there is an important point here The equality of the "Faulting Address" alone does not prove that two crashes are necessarily the same bug There may be several different paths to the same instruction Get So for Deduplication we usually check several indicators together For example Faulting Instruction Call Stack Registers Memory State Crash Type Input Behavior The goal is to separate duplicate crashes from truly different crashes Sanitizers are very helpful here For example, if the program was run with "AddressSanitizer" or "ASan" Instead of just seeing a simple message like this Segmentation fault we may have much more information For example ERROR: AddressSanitizer: heap-buffer-overflow READ of size 4 #0 process_data #1 handle_input #2 main ASan can give more information about the type of error and the location of the unauthorized access and in some errors it also reports information about the allocation and access location As a result, the inspection path can go from this Crash to Crash ↓ Error Type ↓ Invalid Access ↓ Stack Trace ↓ Suspicious Function ↓ Root Cause Instead of just knowing that the program crashed

Valid data[0] ─┐ data[1] │ data[2] │ data[3] ─┘ Invalid data[10] → خارج از محدوده به این حالت میگیم "Out-of-Bounds Read" یعنی برنامه داره از جایی خارج از محدوده‌ای که برای آرایه در نظر گرفته شده داده میخونه حالا از دید Reverse Engineer فرض کنید Crash داریم و Stack Trace اینو نشون میده #0 process #1 main پس میریم داخل "process" void process(int index) { int data[4]; printf("%d\n", data[index]); } اینجا سریع دنبال الگوهایی مثل این میگردیم array[index] بعد باید بفهمیم "index" از کجا اومده index ↓ Where did it come from ↓ Was it validated ↓ Was the upper bound checked مثلا اگر قبل از دسترسی چنین بررسی‌ ای وجود داشته باشه if (index < 0 || index >= 4) return; برنامه قبل از دسترسی نامعتبر جلوی این حالت رو میگیره ولی اگر چنین بررسی‌ای وجود نداشته باشه index ↓ array[index] ↓ No Bounds Check ↓ Out-of-Bounds Access احتمال وجود باگ خیلی بیشتر میشه هدف اصلی Crash Triage در اخر میخوایم هزاران Crash رو تبدیل کنیم به چند مورد محدود و قابل بررسی مثلا 1000 Crash ↓ 250 Crash Groups ↓ 20 Unique Crash ↓ 7 Root Cause ↓ 7 Bug یعنی Fuzzer کارش اینه که کلی ورودی مختلف رو امتحان کنه و رفتارهای غیرعادی رو پیدا کنه ولی بعدش باید بفهمیم این رفتارها دقیقا چی هستن اینجاست که ما وارد داستان میشیم Fuzzer ↓ Crash ↓ Crash Triage ↓ Crash Deduplication ↓ Root Cause Analysis در واقع Fuzzer میگه یه جای برنامه یه اتفاق عجیب افتاد ما باید بفهمهیم کجا چرا و دقیقا چه چیزی باعثش شد این دقیقا جاییه که Fuzzing و ما به هم وصل میشیم @reverseengine

بخش سی ام بافر اورفلو
Crash Triage تا اینجا یاد گرفتیم با Fuzzing کلی Crash پیدا کنیم ولی اینجا یه مشکل خیلی بزرگ داریم Fuzzer ممکنه برای فقط یک باگ صدها یا حتی هزاران Crash مختلف تولید کنه پس بعد از Fuzzing باید بشینیم بفهمیم کدوم Crash واقعا مهمه کدوم Crash ها در اصل مربوط به یه باگ مشترکن Crash دقیقا کجای برنامه اتفاق افتاده و مهم‌تر از همه علت اصلی Crash چی بوده به این مرحله میگیم Crash Triage یعنی مرتب کردن و بررسی Crash ها تا بفهمیم کدوم‌ ها واقعا ارزش بررسی دارن اولین چیزی که بررسی میکنیم فرض کنید Fuzzer با این ورودی‌ ها باعث Crash شده AAAA BBBBBBBB test123 AAAAAAAAAAAAAAAA hello_world نباید سریع نتیجه بگیریم که اینجا 5 تا باگ داریم ممکنه هر پنج ورودی در اخر به یک دستور مشخص از برنامه برسن مثلا 0x401234: mov byte ptr [rax], dl و همین دستور باعث Crash بشه پس چیزی که برای ما مهمه فقط فایل ورودی نیست باید ببینیم Crash کجا اتفاق افتاده و تحت چه شرایطی اتفاق افتاده یعنی Input ↓ Program ↓ Fault ↓ Where did it happen ↓ Why did it happen Faulting Instruction یکی از مهم‌ ترین چیزایی که موقع بررسی Crash باید پیدا کنیم "Faulting Instruction" هست یعنی دستوری که CPU موقع اتفاق افتادن Exception یا Fault در حال اجرای اون بوده مثلا Program received signal SIGSEGV RIP = 0x401234 0x401234: mov byte ptr [rax], dl اینجا فقط اینکه برنامه "SIGSEGV" داده برامون کافی نیست باید بپرسیم RAX چه مقداری داشته چرا این مقدار نامعتبر بوده این مقدار از کجا اومده مثلا ممکنه "RAX" به یه آدرس نامعتبر اشاره کنه پس به جای اینکه فقط بگیم Program crashed باید بریم یه مرحله عمیق‌تر Crash ↓ Faulting Instruction ↓ Invalid Register / Memory ↓ Why did this happen Crash خودش علت نیست Crash نتیجه یک اتفاق قبلیه Stack Trace بعد می‌ریم سراغ "Stack Trace" مثلا #0 0x401234 in process_data() #1 0x401567 in handle_input() #2 0x401789 in main() یعنی مسیر رسیدن برنامه به Crash تقریبا این بوده main() ↓ handle_input() ↓ process_data() ↓ CRASH این اطلاعات برای ما خیلی مهمه چون حالا میدونیم باید اول کدوم قسمت برنامه رو بررسی کنیم مثلا میتونیم داخل IDA یا Ghidra بریم سراغ "process_data()" و ببینیم دقیقا قبل از Crash چه اتفاقی افتاده Crash Deduplication حالا فرض کنید Fuzzer هزار تا Crash به ما داده اگر برای هر Crash اطلاعاتی مثل این داشته باشیم Signal Faulting Address Faulting Instruction Stack Trace میتونیم Crashهای شبیه به هم رو کنار هم قرار بدیم مثلا Crash 1 → process_data → 0x401234 Crash 2 → process_data → 0x401234 Crash 3 → process_data → 0x401234 Crash 4 → parse_packet → 0x402010 احتمالا Crash های اول تا سوم یه خانواده هستن Crash 1 Crash 2 Crash 3 ↓ Crash Family A ولی Crash چهارم مسیر متفاوتی داره Crash 4 ↓ Crash Family B البته یه نکته مهم اینجاست برابر بودن "Faulting Address" به تنهایی ثابت نمیکنه که دو Crash حتما یک باگ هستن ممکنه چند مسیر متفاوت به یک دستور برسن پس برای Deduplication معمولا چند تا نشونه رو کنار هم بررسی میکنیم مثل Faulting Instruction Call Stack Registers Memory State Crash Type Input Behavior هدف اینه که Crashهای تکراری رو از Crashهای واقعاً متفاوت جدا کنیم Sanitizerها اینجا خیلی کمک میکنن مثلاً اگه برنامه با "AddressSanitizer" یا همون "ASan" اجرا شده باشه به جای اینکه فقط یه پیام ساده مثل این ببینیم Segmentation fault ممکنه اطلاعات خیلی بیشتری داشته باشیم مثلا ERROR: AddressSanitizer: heap-buffer-overflow READ of size 4 #0 process_data #1 handle_input #2 main ASan میتونه اطلاعات بیشتری درباره نوع خطا و محل دسترسی غیرمجاز بده و در بعضی خطاها اطلاعات مربوط به allocation و محل access رو هم گزارش کنه در نتیجه مسیر بررسی میتونه از این Crash برسه به Crash ↓ Error Type ↓ Invalid Access ↓ Stack Trace ↓ Suspicious Function ↓ Root Cause یعنی به جای اینکه فقط بدونیم برنامه از بین رفته میفهمیم تقریبا چه اتفاقی باعث از بین رفتت برنامه شده یک مثال ساده این برنامه رو ببینید #include <stdio.h> void process(int index) { int data[4] = {10, 20, 30, 40}; printf("%d\n", data[index]); } int main(void) { process(10); return 0; } اینجا آرایه فقط 4 عضو داره data[0] data[1] data[2] data[3] ولی برنامه میخواد اینو بخونه data[10] پس برنامه داره خارج از محدوده آرایه به حافظه دسترسی پیدا میکنه

RECON 2026 - Deobfuscation in tne Age of Agentic Reverse Engineering https://www.youtube.com/watch?v=3-gJ6EUFoKM @reverseengine

Mobile App Reverse Engineering: Get started with discovering, analyzing, and exploring the internals of Android and iOS apps @reverseengine

Automa'cally A,acking So2ware Reverse Engineering AI Agents @reverseengine

x86 Software ReverseEngineering, Cracking, and Counter-Measures @reverseengine

Tool Interface Standard (TIS) Executable and Linking Format (ELF) Specification Version 1.2 @reverseengine

Static Analysis of GLOBAL GROUP Ransomware: From Encrypted Config to Panic Mode https://blog.ghozt.ninja/2026/01/07/aware @reverseengine

Backward Slicing It means Work backwards from the result So far in Data Flow Analysis we have learned where the data comes from and what path it takes Backward Slicing is exactly the opposite Here we start from a significant point and work backwards to understand what caused that result For example, suppose we have this code: C++
int process(int a, int b) { int x = a + b; int y = x * 2; int junk = 500; junk ^= 123; return y; }
If our goal is to return y, we start at y We ask: Where did y come from? Answer: y = x * 2 So now we need to figure out where x came from x = a + b So a and b affect the output Now if we look at junk we see that it has no path to y or output So we can ignore it completely for now in this analysis The Backward Slice path looks like this: Return y ↑ y = x * 2 ↑ x = a + b ↑ ↑ a b This method is especially useful when you are dealing with a very large and busy function Because you do not have to analyze everything from the beginning to the end of the function. First, you specify what you are looking for For example: The result of a condition The output value An important address A value written to memory Or an important function Then you start from that point and go back Suppose you see this in the disassembler:
cmp eax, 1 jne failed
The important question is not where jne goes First, you should Ask: Where did the EAX value come from before this comparison? There may have been a few statements: call check_value Now you need to go back and see what check_value returns Or maybe EAX came from a long calculation In this case, Backward Slicing helps to check only the statements that actually affect the final value of EAX This is very important in obfuscated programs because there may be a hundred extra statements around an important condition But maybe only ten statements actually affect the result of that condition. The goal of Backward Slicing is to find exactly that statement An important point is that you should not only look for direct dependencies For example: A affects B B affects C C affects the result So A is also part of the Slice even if it is not directly connected to the result Exercise: Check this function: C
int verify(int input) { int a = input * 2; int b = a + 15; int temp = 100; temp += 50; int result = b ^ 0x33; return result; }
Start from return result and work backwards to determine which variables really affect the output Then draw the path on paper input ↓ a ↓ b ↓ result ↓ return temp Check it out, you'll see that there are a few operations that are performed but have no effect on the output, and this is exactly where Backward Slicing helps us get straight to the point in the middle of a lot of code @reverseengine

Backward Slicing یعنی از نتیجه برگردید به عقب تا اینجا در Data Flow Analysis یاد گرفتیم داده از کجا وارد میشه و چه مسیری رو طی میکنه Backward Slicing دقیقا برعکسشه اینجا از یک نقطه مهم شروع میکنیم و برمیگردیم عقب تا بفهمیم چه چیزهایی باعث به وجود اومدن اون نتیجه شدن مثلا فرض کنید این کد رو داریم: C++
int process(int a, int b) { int x = a + b; int y = x * 2; int junk = 500; junk ^= 123; return y; }
اگر هدفمون return y باشه از y شروع میکنیم می‌پرسیم: y از کجا اومده? جواب: y = x * 2 پس حالا باید بفهمیم x از کجا اومده x = a + b پس a و b روی خروجی تاثیر دارن حالا اگر به junk نگاه کنیم میبینیم هیچ مسیری به y یا خروجی نداره پس در این تحلیل میتونیم فعلا کامل نادیده‌اش بگیریم مسیر Backward Slice این شکلیه: Return y ↑ y = x * 2 ↑ x = a + b ↑ ↑ a b این روش مخصوصا وقتی با یک تابع خیلی بزرگ و شلوغ طرفید فوق‌العاده کاربردیه چون لازم نیست از اول تابع تا آخرش همه چیز رو تحلیل کنید اول مشخص میکنید دنبال چی هستید مثلا: نتیجه یک شرط مقدار خروجی یک آدرس مهم یک مقدار نوشته‌شده در حافظه یا یک تابع مهم بعد از همون نقطه شروع میکنید و به عقب برمیگردید فرض کنید داخل دیس‌اسمبل این رو میبینید: cmp eax, 1 jne failed سوال مهم این نیست که فقط jne کجا میره اول باید بپرسید: مقدار EAX قبل از این مقایسه از کجا اومده؟ ممکنه چند دستور قبل: call check_value وجود داشته باشه حالا باید برگردید و ببینید check_value چی برمیگردونه یا شاید EAX از یک محاسبه طولانی اومده باشه در این حالت Backward Slicing کمک میکنه فقط دستورهایی رو بررسی کنید که واقعا روی مقدار نهایی EAX تأثیر دارن این کار توی برنامه‌های Obfuscate شده خیلی مهمه چون ممکنه دور یک شرط مهم صد تا دستور اضافی وجود داشته باشه اما شاید فقط ده دستور واقعا روی نتیجه اون شرط تاثیر بذارن هدف Backward Slicing دقیقا پیدا کردن همین دستوره یک نکته مهم اینه که باید فقط دنبال وابستگی مستقیم نباشید مثلا: A روی B تاثیر می‌ذاره B روی C C روی نتیجه پس A هم بخشی از Slice محسوب میشه حتی اگر مستقیم به نتیجه وصل نباشه تمرین: این تابع رو بررسی کنید: C
int verify(int input) { int a = input * 2; int b = a + 15; int temp = 100; temp += 50; int result = b ^ 0x33; return result; }
از return result شروع کنید و به عقب برگردید مشخص کنید کدوم متغیر ها واقعا روی خروجی تاثیر دارن بعد مسیرش رو روی کاغذ رسم کنید input ↓ a ↓ b ↓ result ↓ return temp رو هم بررسی کنید میبینید که وجود داره و چند عملیات روش انجام میشه ولی هیچ اثری روی خروجی نداره و دقیقا همینجاست که Backward Slicing به ما کمک میکنه وسط تعداد زیاد کد از کد مستقیم بریم سراغ چیزی که واقعا مهمه @reverseengine