بخش بیست و نهم بافر اورفلو
Binary Only Fuzzing
وقتی سورس کد نداریم
تا اینجا Fuzzing رو بیشتر روی برنامههایی دیدیم که سورس کدشون دستمون بود
ولی توی Reverse Engineering و Fuzzing یه عالمه وقت پیش میاد که فقط یه فایل اجرایی داریم
مثلا
program.exe
یا
program
نه سورس داریم
نه مستندات درست و حسابی
نه حتی دقیقا میدونیم داخل برنامه چه خبره
اینجاست که باید اول برنامه رو بررسی کنیم
بفهمیم ورودی رو از کجا میگیره
بفهمیم اون ورودی چطوری داخل برنامه حرکت میکنه
بعد همون مسیر رو برای Fuzzing آماده کنیم
مرحله اول
باز کردن Binary
اول برنامه رو داخل ابزارهایی مثل Ghidra یا IDA باز میکنیم
هدفمون این نیست که از همون اول کل برنامه رو بفهمیم
قرار نیست بشینیم میلیونها Instruction رو یکی یکی بخونیم
اول دنبال جاهایی میگردیم که برنامه با داده خارجی سر و کار داره
مثلا توی Linux و برنامههای مختلف ممکنه با چیزهایی مثل این برخورد کنیم
read
recv
fread
fgets
scanf
توی Windows هم چیزهایی مثل
ReadFile
recv
fgets
میتونن سرنخ خوبی باشن
ولی اینجا یه نکته مهم داریم
صرفا دیدن اسم یک تابع به این معنی نیست که همونجا ورودی اصلی برنامه قرار داره
ممکنه داده از چندتا تابع رد بشه
ممکنه Wrapper داشته باشیم
ممکنه Parser جداگانه وجود داشته باشه
پس باید مسیر داده رو دنبال کنیم
مرحله دوم
دنبال کردن مسیر ورود داده
فرض کنید داخل Decompiler به چیزی شبیه این رسیدیم
buffer = malloc(256);
read(fd, buffer, size);
process_data(buffer);
اینجا یه مسیر خیلی خوب داریم
External Input
↓
read
↓
buffer
↓
process_data
یعنی داده خارجی وارد برنامه شده
داخل یک Buffer قرار گرفته
بعد رفته داخل تابعی که احتمالا اون داده رو پردازش میکنه حالا دیگه فقط نمیدونیم برنامه ورودی داره بلکه یه سرنخ داریم که ورودی بعد از وارد شدن به برنامه کجا میره
مرحله سوم
بفهمیم ورودی چه شکلیه
این قسمت خیلی مهمه
چون Fuzzer نمیتونه همینطوری یه مشت داده تصادفی بریزه توی برنامه و انتظار داشته باشه برنامه هم با خوشحالی همه رو پردازش کنه
برنامه معمولا انتظار داره ورودی یه ساختار خاص داشته باشه
مثلا ممکنه برنامه یک فایل بخونه
program sample.dat
یا ورودی رو از stdin بگیره
program < sample.dat
یا اصلا ورودی از شبکه بیاد
Client
↓
Network
↓
recv
↓
Parser
پس باید بفهمیم
ورودی از کجا میاد
چه فرمتی داره
کجا Parse میشه
و چه قسمتهایی از برنامه با اون کار میکنن این اطلاعات برای Fuzzing خیلی مهمن چون هرچی بیشتر ساختار ورودی رو بفهمیم
Fuzzer
هم میتونه ورودیهای مفیدتری تولید کنه
مرحله چهارم
ساختن Seed
حالا فرض کنید فهمیدیم برنامه یک فایل با فرمت خاص خودش رو میخونه
مثلا:
sample.dat
اول یه فایل سالم و کوچک پیدا میکنیم
به این میگیم
Seed
بعد Fuzzer همین Seed رو تغییر میده
Seed
↓
Mutation
↓
Program
↓
New Path
یا
Crash
`مثلا یه بخش از فایل تغییر میکنه
بعد برنامه اجرا میشه
Fuzzer بررسی میکنه آیا برنامه وارد مسیر جدیدی شده یا نه
یا مثلا Crash کرده یا نه
چرا اینجا مهمه
اینجا دقیقا جاییه که این به کمک Fuzzing میاد
Fuzzer
به تنهایی همیشه نمیتونه بفهمه ساختار ورودی چیه
ولی ما میتونیم با بررسی Assembly و Decompiler سرنخهایی پیدا کنیم
مثلا فرض کن توی Assembly میبینیم:
cmp byte ptr [rax], 48h
jne invalid
اینجا میبینیم برنامه داره یک Byte مشخص رو بررسی میکنه
یعنی احتمالا یه بخش خاص از ورودی برای برنامه اهمیت داره
بعد یه جای دیگه میبینیم
cmp dword ptr [rax+4], 100h
اینجا برنامه داره مقداری رو در Offset چهار بررسی میکنه
حالا اگه چندتا از این Checkها رو کنار هم بذاریم
کم کم میتونیم حدس بزنیم ورودی چه ساختاری داره
مثلا ممکنه به چیزی شبیه این برسیم:
Offset 0
↓
Magic
Offset 4
↓
Size
Offset 8
↓
Flags
Offset 12
↓
Data
یعنی بدون داشتن Source Code هم میتونیم از روی رفتار Binary کم کم ساختار Input Format رو دربیاریم
البته یه نکته مهم
این چیزی که از Assembly درمیاریم همیشه صددرصد قطعی نیست
ممکنه Compiler کد رو Optimize کرده باشه
ممکنه ساختار پیچیدهتر باشه
ممکنه چندتا مسیر مختلف برای Parse کردن ورودی وجود داشته باشه
پس باید فرضیههامون رو با اجرای واقعی برنامه و Debugging بررسی کنیم
یک نکته خیلی مهم
در Binary Only Fuzzing معمولا سه مرحله خیلی ارزشمند داریم
Input Discovery
↓
Program Analysis
Fuzzing
یعنی اول بفهمید
ورودی از کجا میاد
بعد بفهمید
چطور داخل برنامه پردازش میشه
بعد Fuzzing رو روی همون مسیر انجام بده
این خیلی بهتر از اینه که بدون شناخت برنامه فقط یه عالمه داده تصادفی تولید کنیم و امیدوار باشیم چیزی منفجر بشه
چون سیستم عامل و برنامهها متاسفانه برای همکاری با آرزوهای ما طراحی نشدن
وقتی Crash پیدا شد
حالا فرض کنید Fuzzer یه نتیجه بهمون داده
Crash found
input_0421.dat
اینجا کار تموم نشده
اتفاقا تازه بخش جالب ماجرا شروع شده
همون Input رو دوباره با Debugger اجرا میکنیم
Input
↓
Program
↓
Crash
↓
Debugger
↓
Call Stack
↓
Faulting Instruction
حالا میخوایم بفهمیم
دقیقا کجا Crash اتفاق افتاده
کدوم Instruction باعثش شده
Registerها چه وضعیتی داشتن
Memory چه شکلی بوده
Call Stack چه مسیری رو نشون میده
و مهمتر از همه
Root Cause چی بوده
مثلا ممکنه اول فقط ببینیم برنامه روی یه Instruction خاص Crash کرده
ولی این هنوز به معنی این نیست که همون Instruction ریشه اصلی باگه
ممکنه چند مرحله قبلتر داده خراب وارد برنامه شده باشه و اثرش تازه اینجا خودش رو نشون داده باشه
پس مسیر تحلیل میتونه این شکلی بشه:
Crash
↓
Faulting Instruction
↓
Register State
↓
Memory State
↓
Call Stack
↓
Data Flow
↓
Root Cause
این دقیقا همون جاییه که Fuzzing و Reverse Engineering دست همدیگه رو میگیرن
Fuzzer میگه
این Input باعث Crash شد
Reverse Engineer میگه
خب حالا ببینیم چرا
وقتی Source Code نداریم
اول نمیریم سراغ Fuzzing کورکورانه
اول Binary رو بررسی میکنیم
بعد دنبال Input میگردیم
بعد مسیر حرکت Input داخل برنامه رو پیدا میکنیم بعد تا جایی که میتونیم ساختار Input Format رو میفهمیم
بعد Seed مناسب پیدا میکنیم
بعد Fuzzer رو روی اون مسیر قرار میدیم
و اگر Crash پیدا شد
برمیگردیم سراغ Debugger و Reverse Engineering
کل مسیر رو میشه اینطوری دید
Binary
↓
Reverse Engineering
↓
Input Discovery
↓
Input Analysis
↓
Seed
↓
Fuzzing
↓
Crash
↓
Debugger
↓
Root Cause Analysis
نکته اصلی این پست اینه
وقتی Source Code ندارید
به این معنی نیست که نمیتونید Fuzzing انجام بدی
فقط باید قبل از Fuzzing
خود Binary رو بشناسید
Reverse Engineering اینجا کمک میکنه بفهمی
داده از کجا وارد میشه
کجا Parse میشه
چه بخشهایی از Input مهم هستن
و چه مسیرهایی داخل برنامه طی میشن
و همین اطلاعات میتونه کیفیت Fuzzing رو خیلی بهتر کنه
@reverseengine
یعنی اول بفهمید
ورودی از کجا میاد
بعد بفهمید
چطور داخل برنامه پردازش میشه
بعد Fuzzing رو روی همون مسیر انجام بده
این خیلی بهتر از اینه که بدون شناخت برنامه فقط یه عالمه داده تصادفی تولید کنیم و امیدوار باشیم چیزی منفجر بشه
چون سیستم عامل و برنامهها متاسفانه برای همکاری با آرزوهای ما طراحی نشدن
وقتی Crash پیدا شد
حالا فرض کنید Fuzzer یه نتیجه بهمون داده
Crash found
input_0421.dat
اینجا کار تموم نشده
اتفاقا تازه بخش جالب ماجرا شروع شده
همون Input رو دوباره با Debugger اجرا میکنیم
Input
↓
Program
↓
Crash
↓
Debugger
↓
Call Stack
↓
Faulting Instruction
حالا میخوایم بفهمیم
دقیقا کجا Crash اتفاق افتاده
کدوم Instruction باعثش شده
Registerها چه وضعیتی داشتن
Memory چه شکلی بوده
Call Stack چه مسیری رو نشون میده
و مهمتر از همه
Root Cause چی بوده
مثلا ممکنه اول فقط ببینیم برنامه روی یه Instruction خاص Crash کرده
ولی این هنوز به معنی این نیست که همون Instruction ریشه اصلی باگه
ممکنه چند مرحله قبلتر داده خراب وارد برنامه شده باشه و اثرش تازه اینجا خودش رو نشون داده باشه
پس مسیر تحلیل میتونه این شکلی بشه:
Crash
↓
Faulting Instruction
↓
Register State
↓
Memory State
↓
Call Stack
↓
Data Flow
↓
Root Cause
این دقیقا همون جاییه که Fuzzing و Reverse Engineering دست همدیگه رو میگیرن
Fuzzer میگه
این Input باعث Crash شد
Reverse Engineer میگه
خب حالا ببینیم چرا
وقتی Source Code نداریم
اول نمیریم سراغ Fuzzing کورکورانه
اول Binary رو بررسی میکنیم
بعد دنبال Input میگردیم
بعد مسیر حرکت Input داخل برنامه رو پیدا میکنیم بعد تا جایی که میتونیم ساختار Input Format رو میفهمیم
بعد Seed مناسب پیدا میکنیم
بعد Fuzzer رو روی اون مسیر قرار میدیم
و اگر Crash پیدا شد
برمیگردیم سراغ Debugger و Reverse Engineering
کل مسیر رو میشه اینطوری دید
Binary
↓
Reverse Engineering
↓
Input Discovery
↓
Input Analysis
↓
Seed
↓
Fuzzing
↓
Crash
↓
Debugger
↓
Root Cause Analysis
نکته اصلی این پست اینه
وقتی Source Code ندارید
به این معنی نیست که نمیتونید Fuzzing انجام بدی
فقط باید قبل از Fuzzing
خود Binary رو بشناسید
Reverse Engineering اینجا کمک میکنه بفهمی
داده از کجا وارد میشه
کجا Parse میشه
چه بخشهایی از Input مهم هستن
و چه مسیرهایی داخل برنامه طی میشن
و همین اطلاعات میتونه کیفیت Fuzzing رو خیلی بهتر کنه
@reverseengine
Binary Only Fuzzing
When we don't have source code
So far, we have mostly seen Fuzzing on programs whose source code we have
But in Reverse Engineering and Fuzzing, there are many times when we only have an executable file
For example
program.exe
or
program
We don't have the source
Or proper documentation
Or we don't even know exactly what's going on inside the program
This is where we need to first examine the program
To understand where it gets its input
To understand how that input moves inside the program
Then prepare the same path for Fuzzing
Step One
Opening the Binary
First, we open the program in tools like Ghidra or IDA
Our goal is not to understand the entire program from the beginning
We are not going to sit down and read millions of instructions one by one
First, we look for places where the program deals with external data
For example, in Linux and various programs, we may encounter things like
read
recv
fread
fgets
scanf
In Windows also has things like
ReadFile
recv
fgets
can be a good clue
But here's an important point
Just seeing the name of a function doesn't mean that the main input to the program is there
Data may be passed through several functions
We may have a Wrapper
There may be a separate Parser
So we need to follow the data path
Step 2
Follow the data path
Suppose we get something like this in the Decompiler
buffer = malloc(256);
read(fd, buffer, size);
process_data(buffer);
Here we have a very good path
External Input
↓
read
↓
buffer
↓
process_data
That is, external data entered the program
It is placed in a Buffer
Then it goes into a function that probably processes that data. Now we not only know that the program has input, but we also have a clue where the input goes after it enters the program
Step 3
Find out what the input looks like
This part is very important
Because the Fuzzer cannot just throw a bunch of random data into the program and expect the program to happily process it all
The program usually expects the input to have a specific structure
For example, the program may read a file
program sample.dat
Or take input from stdin
program < sample.dat
Or even input from the network
Client
↓
Network
↓
recv
↓
Parser
So we need to understand
Where the input comes from
What format it is
Where it is parsed
And what parts of the program work with it. This information is very important for Fuzzing because the more Understand the input structure
Fuzzer
can also generate more useful inputs
Step 4
Creating a Seed
Now suppose we understand that the program reads a file with its own format
For example:
sample.dat
First we find a healthy and small file
We call this
Seed
Then the Fuzzer modifies this Seed
Seed
↓
Mutation
↓
Program
↓
New Path
or
Crash
For example, a part of the file changes
Then the program runs
Fuzzer checks whether the program has entered a new path or not
Or whether it has crashed
Why is this important
This is exactly where Fuzzing comes in handy
Fuzzer
Alone can't always figure out what the input structure is
But we can find clues by examining the Assembly and Decompiler
For example, suppose we see in the Assembly:
cmp byte ptr [rax], 48h
jne invalid
Here we see the program is checking a specific Byte
That is, it is probably A specific part of the input is important to the program
We will see it later in another place
cmp dword ptr [rax+4], 100h
Here the program is checking a value at Offset 4
Now if we put a few of these checks together
We can gradually guess what the input structure is
For example, we might get something like this:
Offset 0
↓
Magic
Offset 4
↓
Size
Offset 8
↓
Flags
Offset 12
↓
Data
That is, without having the Source Code, we can gradually deduce the Input Format structure from the Binary behavior
Of course, one important point
When we don't have source code
So far, we have mostly seen Fuzzing on programs whose source code we have
But in Reverse Engineering and Fuzzing, there are many times when we only have an executable file
For example
program.exe
or
program
We don't have the source
Or proper documentation
Or we don't even know exactly what's going on inside the program
This is where we need to first examine the program
To understand where it gets its input
To understand how that input moves inside the program
Then prepare the same path for Fuzzing
Step One
Opening the Binary
First, we open the program in tools like Ghidra or IDA
Our goal is not to understand the entire program from the beginning
We are not going to sit down and read millions of instructions one by one
First, we look for places where the program deals with external data
For example, in Linux and various programs, we may encounter things like
read
recv
fread
fgets
scanf
In Windows also has things like
ReadFile
recv
fgets
can be a good clue
But here's an important point
Just seeing the name of a function doesn't mean that the main input to the program is there
Data may be passed through several functions
We may have a Wrapper
There may be a separate Parser
So we need to follow the data path
Step 2
Follow the data path
Suppose we get something like this in the Decompiler
buffer = malloc(256);
read(fd, buffer, size);
process_data(buffer);
Here we have a very good path
External Input
↓
read
↓
buffer
↓
process_data
That is, external data entered the program
It is placed in a Buffer
Then it goes into a function that probably processes that data. Now we not only know that the program has input, but we also have a clue where the input goes after it enters the program
Step 3
Find out what the input looks like
This part is very important
Because the Fuzzer cannot just throw a bunch of random data into the program and expect the program to happily process it all
The program usually expects the input to have a specific structure
For example, the program may read a file
program sample.dat
Or take input from stdin
program < sample.dat
Or even input from the network
Client
↓
Network
↓
recv
↓
Parser
So we need to understand
Where the input comes from
What format it is
Where it is parsed
And what parts of the program work with it. This information is very important for Fuzzing because the more Understand the input structure
Fuzzer
can also generate more useful inputs
Step 4
Creating a Seed
Now suppose we understand that the program reads a file with its own format
For example:
sample.dat
First we find a healthy and small file
We call this
Seed
Then the Fuzzer modifies this Seed
Seed
↓
Mutation
↓
Program
↓
New Path
or
Crash
For example, a part of the file changes
Then the program runs
Fuzzer checks whether the program has entered a new path or not
Or whether it has crashed
Why is this important
This is exactly where Fuzzing comes in handy
Fuzzer
Alone can't always figure out what the input structure is
But we can find clues by examining the Assembly and Decompiler
For example, suppose we see in the Assembly:
cmp byte ptr [rax], 48h
jne invalid
Here we see the program is checking a specific Byte
That is, it is probably A specific part of the input is important to the program
We will see it later in another place
cmp dword ptr [rax+4], 100h
Here the program is checking a value at Offset 4
Now if we put a few of these checks together
We can gradually guess what the input structure is
For example, we might get something like this:
Offset 0
↓
Magic
Offset 4
↓
Size
Offset 8
↓
Flags
Offset 12
↓
Data
That is, without having the Source Code, we can gradually deduce the Input Format structure from the Binary behavior
Of course, one important point
What we get from Assembly is not always 100% certain
The compiler may have optimized the code
The structure may be more complex
There may be several different paths to parse the input
So we need to check our hypotheses by actually running the program and debugging
A very important point
In Binary Only Fuzzing, there are usually three We have a very valuable stage
Input Discovery
↓
Program Analysis
The compiler may have optimized the code
The structure may be more complex
There may be several different paths to parse the input
So we need to check our hypotheses by actually running the program and debugging
A very important point
In Binary Only Fuzzing, there are usually three We have a very valuable stage
Input Discovery
↓
Program Analysis
Fuzzing
means first understand
where the input comes from
then understand
how it is processed inside the program
then do Fuzzing on that path
This is much better than just generating a bunch of random data without knowing the program and hoping that something will explode
Because the operating system and programs are unfortunately not designed to cooperate with our wishes
When a Crash is found
Now suppose the Fuzzer gives us a result
Crash found
input_0421.dat
The work is not over here
Incidentally, the interesting part has just begun
We run the same Input again with the Debugger
Input
↓
Program
↓
Crash
↓
Debugger
↓
Call Stack
↓
Faulting Instruction
Now we want to find out
Where exactly the Crash occurred
Which instruction caused it
What state did the registers have
What was the memory like
What path does the Call Stack show
And most importantly
What was the Root Cause
For example, we might first just see the program crash on a specific instruction
But this still does not mean that the same Instruction is the root cause of the bug
It is possible that corrupt data was entered into the program a few steps earlier and its effect has only shown itself here.
So the analysis path can look like this:
Crash
↓
Faulting Instruction
↓
Register State
↓
Memory State
↓
Call Stack
↓
Data Flow
↓
Root Cause
This is exactly where Fuzzing and Reverse Engineering come together
Fuzzer says
This Input caused the Crash
Reverse Engineer says
Well, now let's see why
When we don't have Source Code
First, we don't go to Fuzzing blindly
First, we examine the Binary
Then we look for Input
Then we find the path of Input movement inside the program, then we understand the Input Format structure as much as we can
Then we find the appropriate Seed
Then we put the Fuzzer on that path
And if a Crash is found
We go back to Debugger and Reverse Engineering
The whole path can be seen like this
Binary
↓
Reverse Engineering
↓
Input Discovery
↓
Input Analysis
↓
Seed
↓
Fuzzing
↓
Crash
↓
Debugger
↓
Root Cause Analysis
The main point of this post is that
when you don't have the source code
it doesn't mean you can't do fuzzing
you just need to understand the binary before fuzzing
Reverse Engineering helps you understand
where the data comes from
where it is parsed
what parts of the input are important
and what paths it takes in the program
and this information can greatly improve the quality of fuzzing
@reverseengine
means first understand
where the input comes from
then understand
how it is processed inside the program
then do Fuzzing on that path
This is much better than just generating a bunch of random data without knowing the program and hoping that something will explode
Because the operating system and programs are unfortunately not designed to cooperate with our wishes
When a Crash is found
Now suppose the Fuzzer gives us a result
Crash found
input_0421.dat
The work is not over here
Incidentally, the interesting part has just begun
We run the same Input again with the Debugger
Input
↓
Program
↓
Crash
↓
Debugger
↓
Call Stack
↓
Faulting Instruction
Now we want to find out
Where exactly the Crash occurred
Which instruction caused it
What state did the registers have
What was the memory like
What path does the Call Stack show
And most importantly
What was the Root Cause
For example, we might first just see the program crash on a specific instruction
But this still does not mean that the same Instruction is the root cause of the bug
It is possible that corrupt data was entered into the program a few steps earlier and its effect has only shown itself here.
So the analysis path can look like this:
Crash
↓
Faulting Instruction
↓
Register State
↓
Memory State
↓
Call Stack
↓
Data Flow
↓
Root Cause
This is exactly where Fuzzing and Reverse Engineering come together
Fuzzer says
This Input caused the Crash
Reverse Engineer says
Well, now let's see why
When we don't have Source Code
First, we don't go to Fuzzing blindly
First, we examine the Binary
Then we look for Input
Then we find the path of Input movement inside the program, then we understand the Input Format structure as much as we can
Then we find the appropriate Seed
Then we put the Fuzzer on that path
And if a Crash is found
We go back to Debugger and Reverse Engineering
The whole path can be seen like this
Binary
↓
Reverse Engineering
↓
Input Discovery
↓
Input Analysis
↓
Seed
↓
Fuzzing
↓
Crash
↓
Debugger
↓
Root Cause Analysis
The main point of this post is that
when you don't have the source code
it doesn't mean you can't do fuzzing
you just need to understand the binary before fuzzing
Reverse Engineering helps you understand
where the data comes from
where it is parsed
what parts of the input are important
and what paths it takes in the program
and this information can greatly improve the quality of fuzzing
@reverseengine
Backward Slicing
یعنی از نتیجه برگردید به عقب
تا اینجا در Data Flow Analysis یاد گرفتیم داده از کجا وارد میشه و چه مسیری رو طی میکنه
Backward Slicing
دقیقا برعکسشه
اینجا از یک نقطه مهم شروع میکنیم و برمیگردیم عقب تا بفهمیم چه چیزهایی باعث به وجود اومدن اون نتیجه شدن
مثلا فرض کنید این کد رو داریم:
C++
اگر هدفمون
میپرسیم:
از کجا اومده?
جواب:
y = x * 2
پس حالا باید بفهمیم
x = a + b
پس
حالا اگر به
پس در این تحلیل میتونیم فعلا کامل نادیدهاش بگیریم
مسیر Backward Slice این شکلیه:
Return y
↑
y = x * 2
↑
x = a + b
↑ ↑
a b
این روش مخصوصا وقتی با یک تابع خیلی بزرگ و شلوغ طرفید فوقالعاده کاربردیه
چون لازم نیست از اول تابع تا آخرش همه چیز رو تحلیل کنید اول مشخص میکنید دنبال چی هستید
مثلا:
نتیجه یک شرط
مقدار خروجی
یک آدرس مهم
یک مقدار نوشتهشده در حافظه
یا یک تابع مهم
بعد از همون نقطه شروع میکنید و به عقب برمیگردید
فرض کنید داخل دیساسمبل این رو میبینید:
cmp eax, 1
jne failed
سوال مهم این نیست که فقط
اول باید بپرسید:
مقدار
ممکنه چند دستور قبل:
call check_value
وجود داشته باشه
حالا باید برگردید و ببینید
یا شاید
در این حالت Backward Slicing کمک میکنه فقط دستورهایی رو بررسی کنید که واقعا روی مقدار نهایی
این کار توی برنامههای Obfuscate شده خیلی مهمه چون ممکنه دور یک شرط مهم صد تا دستور اضافی وجود داشته باشه
اما شاید فقط ده دستور واقعا روی نتیجه اون شرط تاثیر بذارن هدف Backward Slicing دقیقا پیدا کردن همین دستوره
یک نکته مهم اینه که باید فقط دنبال وابستگی مستقیم نباشید
مثلا:
A
روی B تاثیر میذاره
B
روی C
C روی نتیجه
پس
تمرین:
این تابع رو بررسی کنید:
C
از
بعد مسیرش رو روی کاغذ رسم کنید
input
↓
a
↓
b
↓
result
↓
return
رو هم بررسی کنید میبینید که وجود داره و چند عملیات روش انجام میشه ولی هیچ اثری روی خروجی نداره و دقیقا همینجاست که Backward Slicing به ما کمک میکنه وسط تعداد زیاد کد از کد مستقیم بریم سراغ چیزی که واقعا مهمه
@reverseengine
یعنی از نتیجه برگردید به عقب
تا اینجا در 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
ReverseEngineering
Backward Slicing یعنی از نتیجه برگردید به عقب تا اینجا در Data Flow Analysis یاد گرفتیم داده از کجا وارد میشه و چه مسیری رو طی میکنه Backward Slicing دقیقا برعکسشه اینجا از یک نقطه مهم شروع میکنیم و برمیگردیم عقب تا بفهمیم چه چیزهایی باعث به وجود اومدن…
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++
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:
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
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
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
Static Analysis of GLOBAL GROUP Ransomware: From Encrypted Config to Panic Mode
https://blog.ghozt.ninja/2026/01/07/aware
@reverseengine
https://blog.ghozt.ninja/2026/01/07/aware
@reverseengine
/home/ghozt
Static Analysis of GLOBAL GROUP Ransomware: From Encrypted Config to Panic Mode
Analysis Date: January 7, 2026Methodology: Static analysis onlySample: f6f7a37b49310287a253dbdf81e22f0593f44111215ca9308e46d2c68516196f TL;DRIn-depth analysis of GLOBAL GROUP ransomware (RaaS), revea
ReverseEngineering
Part 28 Buffer Overflow AFL++ and Instrumentation How does a Fuzzer understand what is going on inside a program In the previous section, we learned about libFuzzer and Coverage Guided Fuzzing Now let's move on to AFL++ AFL++ is one of the most famous…
/home/ghozt
AFL - Introduction
This is a simple walkthrough to install, setup and run AFL against an open source dumb C program. What is AFL American fuzzy lop is a security-oriented fuzzer that employs a novel type of compile-time
elf.pdf
337.1 KB
Tool Interface Standard (TIS)
Executable and Linking Format (ELF)
Specification
Version 1.2
@reverseengine
Executable and Linking Format (ELF)
Specification
Version 1.2
@reverseengine
Packt.Mobile.App.Reverse.Engineering.pdf
17.3 MB
Mobile App Reverse Engineering: Get started with discovering, analyzing, and exploring the internals of Android and iOS apps
@reverseengine
@reverseengine
RECON 2026 - Deobfuscation in tne Age of Agentic Reverse Engineering
https://www.youtube.com/watch?v=3-gJ6EUFoKM
@reverseengine
https://www.youtube.com/watch?v=3-gJ6EUFoKM
@reverseengine
YouTube
RECON 2026 - Deobfuscation in the Age of Agentic Reverse Engineering
Presented by by Tim Blazytko and Nicolò Altamura
Large language models first entered reverse engineering as helpful assistants: improving decompiler output, suggesting names, answering questions about code, and generating small analysis scripts. Useful…
Large language models first entered reverse engineering as helpful assistants: improving decompiler output, suggesting names, answering questions about code, and generating small analysis scripts. Useful…
بخش سی ام بافر اورفلو
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]
پس برنامه داره خارج از محدوده آرایه به حافظه دسترسی پیدا میکنه
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
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
ReverseEngineering
بخش سی ام بافر اورفلو Crash Triage تا اینجا یاد گرفتیم با Fuzzing کلی Crash پیدا کنیم ولی اینجا یه مشکل خیلی بزرگ داریم Fuzzer ممکنه برای فقط یک باگ صدها یا حتی هزاران Crash مختلف تولید کنه پس بعد از Fuzzing باید بشینیم بفهمیم کدوم Crash واقعا مهمه…
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
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
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