Thursday, May 7, 2026

LAB 10: APK Security Scanner - Multi-Agent Static Analysis pt.2

Taking Android Apps Apart: Building an AI-Powered Security Scanner

Part 2: The Analysis, Execution and Findings

In Part 1 we defined our objective which was building an automated AI pipeline to analyze Android apps and identify security flaws. With our local environment set up and our JADX translator ready it was time to put our scanner to the test.

In this post we walk through the actual reverse engineering process. We detail our methodology, the roadblocks we hit and our final discoveries as we transitioned from simulated logic to real-world AI agents.

Target Recap

We set out to analyze Android applications to automatically recover source code, inspect machine code and identify underlying vulnerabilities like hidden passwords. Our targets included a suite of intentionally vulnerable mock apps to establish a baseline. After that we used a real open-source application called QuickTiles to test how robust our scanner was against actual Android architecture.

Tech Stack and Architecture

Before we dive into the results it helps to understand exactly how our machinery operates behind the scenes. Our system is built on a custom technology stack designed for speed and security.

The Foundation 

We built our backend using Node.js which is a popular environment that lets us run JavaScript on a server instead of just in a web browser. The frontend is a clean web dashboard built with standard HTML and JavaScript.

The Translators (JADX and MobSF)

 When an app is uploaded it first goes to JADX. JADX is an open-source translator software. It works by unzipping the Android app and using complex algorithms to translate the robotic binary data back into human-readable Java code. We also connected MobSF (Mobile Security Framework). MobSF works by taking the app and running it through hundreds of predefined security rules. It acts as our automated backup checker to ensure we do not miss anything obvious.

The 8-Agent Assembly Line

Our AI does not tackle the app all at once. Instead we built a pipeline of specialized AI agents. They communicate with each other in a sequential chain. When one finishes its job it packages its findings and passes them directly to the next. This relay-race system ensures each agent stays focused on its specific job.

We recently upgraded our pipeline. While some agents still use fast deterministic code several of our most critical agents are now powered by the Gemini 2.5 Flash API. This means they are no longer just following static rules but are actually thinking and reasoning about the code in real-time.

The actual brainpower for our real-world LLM agents is located in the narrateFindings and addSmaliEvidence functions. Here is how we upgraded the simulated logic to use actual AI calls:

Intake Validation Agent

 This acts as the bouncer at the door. It checks the uploaded file to make sure it is a real Android app and not a disguised file before letting it into the system.

Decompilation Agent

This agent operates the JADX translator. It carefully unpacks the app and organizes the translated Java code so the other agents can read it.

Permission & Component Agents (LLM Powered)

These agents now send the entire AndroidManifest.xml to Gemini. The AI reads the file and identifies risky configurations that are too complex for simple search rules to catch.

Static Logic Flaw Agent

This agent acts like a detective reading a book. It scans the human-readable Java code looking for hardcoded passwords or hidden bypasses. It acts as a fast filter to find interesting files for the AI.

Risk Narrative Agent (LLM Powered)

Instead of using pre-written templates this agent now sends every finding to Gemini. The AI writes a custom story for every vulnerability it sees and explains exactly how a hacker might exploit it.

Smali/DEX Bytecode Agent (LLM Powered)

This is our most advanced specialist. It extracts raw robotic machine instructions (known as Smali) and sends them to Gemini. The AI translates this difficult code into plain English so we can understand the low-level logic.

Report Aggregation Agent

This is the manager. It takes the stories from all the other agents and organizes them into a clean color-coded report for our dashboard.

MCP Connections (The Universal Plugs)

We connected our scanner using MCPs (Model Context Protocols). This is a fancy way of saying we built a universal plug. It allows our local scanner to connect directly to larger AI models so the AI can securely pull data from our tools. Instead of the AI guessing how to use our system we gave it specific "buttons" it can press.

Methodology

To inspect the targets we orchestrated a multi-stage workflow:

Intake and Extraction

We uploaded the app via our custom web dashboard.

Hybrid Analysis

We used fast search rules to find "files of interest" then routed those specific snippets to our Gemini-powered LLM agents for deep reasoning.

Decompilation Pipeline 

Our system handed the app over to JADX. It unpacked the app and translated the confusing robotic machine code into Java code.

Aggregation

The Report Aggregation Agent collected all these findings and created a unified color-coded report for our dashboard.

Analysis Narrative

We began our analysis with our mock apps to validate the pipeline. Dropping the files into the website successfully triggered the pipeline and the dashboard populated with red warning cards highlighting intentionally placed hidden passwords.

The Evolution from Simulation to Reality

Initially we used "make-believe" agents that only followed static rules. While this worked for our mock apps it failed to capture the nuance of real-world code. By introducing the Gemini API we transformed the scanner.

The Course Correction

We realized that AI is not built to read entire massive applications at once. We kept our fast "Regular Expression" search tools as a spotlight. They find the suspicious lines of code and then we send only those specific lines to Gemini. This hybrid approach allowed us to use the speed of local code with the intelligence of a massive AI model.


Findings

What did our final LLM-powered pipeline discover?

Intelligent Manifest Review

Our Permission Agent successfully identified a risky "Backup Enabled" flag. While a search rule could have found this the AI added context by explaining exactly how a local attacker could steal app data using the ADB tool.

Real-Time Bytecode Translation

Our Smali agent proved to be the most impressive upgrade. When we sent it raw machine code it correctly explained that the code was checking a local boolean variable to see if "premium" mode was active. It then accurately described how to bypass that check.

Custom Remediation Advice: 

Every finding on our dashboard now contains custom advice written by the AI. Instead of generic warnings the developer sees specific instructions tailored to their exact code snippet.

Validation

To ensure the AI was not just making things up (hallucinating) we validated the results using two methods:

Manual Comparison
We manually opened the JADX translator ourselves and visually verified the code. The line numbers and AI-generated explanations perfectly matched the actual logic.

MobSF Cross-Reference
We ran the same app through our backup scanner MobSF. The findings aligned perfectly which confirmed that our AI agents were producing high-fidelity security insights.

Future Deployment Plans

Right now everything runs securely on our private local computers. However we have ambitious plans for the future.

We plan to package this entire scanner and introduce it as a public service. To do this we plan to deploy the web application using modern cloud platforms like Vercel or Netlify. These platforms will allow us to easily host our code on the internet. To ensure our new service stays safe from hackers we will host it behind Cloudflare. Cloudflare acts as a massive digital shield that provides better protection by blocking bad traffic before it ever reaches our servers.

Reflection

Building this scanner taught us a valuable lesson about the intersection of AI and security. We learned that the most powerful systems are not 100% AI but are instead "AI-Augmented".

By using fast local code as a "filter" and the Gemini API as the "brain" we built a tool that is both fast and incredibly intelligent. The transition from simulated agents to actual LLM agents was the final step in creating a truly professional grade security scanner.

Ultimately we learned that combining a mature translator tool like JADX with the reasoning capabilities of Gemini creates a powerful synergy that can take apart even the most complex Android applications.




LAB 9: APK Security Scanner, Multi-agent Static Analysis

Taking Android Apps Apart: Building an AI-Powered Security Scanner

Part 1: Problem Definition, Background and Preparation


Mobile applications are a primary target for hackers in today’s digital landscape. Everything from our personal banking to our smart home devices lives on our phones. But what happens when software developers accidentally leave the digital "keys to the castle" inside the app itself?

In this series we will explore the world of Reverse Engineering for Android applications. Reverse engineering simply means taking something that is already built (like an app) and taking it apart to see exactly how it works on the inside.

We will not just do this manually though. We will be building an automated AI-powered vulnerability scanner. This is a system that uses Artificial Intelligence to automatically look for security flaws and explain how to fix them.

Here is a look at how we will approach this challenge, the background knowledge you need to follow along and our preparation for the analysis.


Project Overview

What are we reverse engineering? 

We will target Android Application Packages (APKs). An APK is the file format Android uses to install apps on your phone much like a .exe file on a Windows computer. By opening up an APK we will be able to look at the underlying code and structure that makes the app run.

What is the broader context?

 This project falls under Mobile Application Security Testing (MAST) which is the practice of looking for security holes in mobile apps before bad actors can find them. Usually security analysts use various tools to take an app apart and spend hours reading through the code to find mistakes. We will speed up this process by using AI Agents. Think of an "agent" as a specialized digital assistant. Instead of one AI trying to do everything we will use a "pipeline" or assembly line of several AI agents where each has a specific job. These jobs will include tasks like reading permissions, looking for passwords or writing the final report.

Why this target? 

Taking Android apps apart is a fascinating mix of software development and security. Our team will enhance our security scanner by adding a new AI agent specifically trained to read Bytecode (the low-level robotic instructions the phone reads). The challenge of having an AI read this confusing machine-level code and translate it into plain English for a human will be a perfect way to test how smart AI really is at security research.


Research Question and Goal

What do we hope to understand? 

Our main goal will be to see if a team of specialized AI agents can accurately take apart an Android app, read the code and find logic flaws. A logic flaw is a mistake in how the app is built. For example it is a flaw if a developer accidentally typed a secret password directly into the app's code (known as a "hardcoded secret") or if they built a screen that bypasses the login page. We specifically want to know if the AI will be able to understand the low-level machine code just as well as standard human-readable code.

What counts as success?


A successful result will be a fully automated assembly line that will:

  • Automatically unpack and translate the app's code.
  • Identify real security mistakes without giving us too many false alarms.
  • Extract the exact lines of code where the mistake is found as evidence.
  • Generate a plain-English explanation of why it is dangerous and how to fix it.
  • Display all of this on a clean professional web dashboard.


Background Information

Before we dive into the analysis we should break down some of the technical jargon:

  • The APK Format: Even though an APK ends in .apk it is essentially just a .zip file. If you rename it to .zip and extract it you will find several files inside. The most important are the AndroidManifest.xml (the rulebook that tells the phone what the app is allowed to do like access the camera) and classes.dex (the actual code).
  • Decompiling: When a programmer writes an app they write it in a language humans can read (like Java). Before it goes to the phone it is translated into a machine language that only the phone can read. This is called compiling. Decompiling is doing that process in reverse where we translate the machine code back into human-readable code.
  • DEX and Smali: Android's specific machine code is called DEX (Dalvik Executable). Because DEX is just binary data reverse engineers use tools to translate it into a slightly more readable format called Smali. It will still be very difficult to read which is why our newest AI agent will focus specifically on understanding it.
  • JADX: This will be a popular free software tool that acts as a translator. It takes the confusing DEX machine code and attempts to decompile it all the way back into the original Java code making it much easier for both humans and our AI to read.
  • MobSF (Mobile Security Framework): An industry-standard software tool used by security professionals to automatically scan apps. We will plug this into our system as an optional backup to double-check our AI's work.


Initial Reconnaissance

What information will be available?

 We will have the basic scaffolding for our scanner. Our setup will include:

  • A web dashboard where we can drag and drop an app to scan it.
  • An assembly line of established AI assistants handling different parts of the scan.
  • A suite of "mock" apps we will build ourselves. These will be intentionally broken apps designed to safely test if our scanner works.
  • A real open-source app to test how our scanner handles a real-world scenario.

What will be unknown? 

The biggest unknown will be how our AI handles Obfuscation. Obfuscation is a trick developers use to intentionally scramble their code before publishing it. They change clear variable names like "password" to random letters like "a.b.c()". Since AI relies heavily on words to understand context we will not know if the AI gets confused when the code is scrambled.


Challenges and Constraints

Technical Obstacles

Obfuscation: As mentioned scrambled code will make it harder for the AI to understand what it is looking at.

Information Overload: An average app can contain millions of lines of code. If we try to feed all of that into an AI like ChatGPT at once its memory will overflow and it will crash. This is known as exceeding the "context limit". We will have to figure out how to filter the code and only give the AI the most important pieces.


Operational & Ethical Constraints:

Taking apart software you do not own can sometimes break user agreements or laws. To stay strictly within legal and ethical boundaries we will only test apps we built ourselves or free open-source apps that give explicit permission to be studied.


Preparation Plan

Tooling and Environment: We will keep everything on our local private computer to ensure no data leaks out to the public internet.

The Brain: We will use an AI service to power our agents.

The Interface: A custom-built website running on our own computer.

The Translator: JADX (installed on our computer) to translate the app code.


Safety Precautions

Because we might be dealing with broken or dangerous code safety will be our top priority. We will use a technique called Static Analysis. This means we will only read the code like a book. We will never actually install or run the apps on a phone or computer. Since the code will not be running it cannot harm our system.


Initial Strategy

Our immediate next steps will involve finishing our newest Smali/DEX AI agent. We will run our safe mock apps through the assembly line to make sure everything works. Then we will upload the real open-source app have JADX translate it and see what our AI agents can discover hidden in the code!


Stay tuned for Part 2 where we will push the button unpack the apps and see what secrets our AI uncovers!

Sunday, March 29, 2026

LAB 8: x86 Assembly Programming and Patching

 Introduction

In this lab, we will write a simple crackme program in x86 32-bit assembly using the SASM IDE, compile it into a working Windows executable, and then crack our own program using the Cutter disassembler.

We will go through 3 main parts:

  1. Building our own CrackMe in SASM
  2. Analyzing our Binary in Cutter
  3. Patching our Binary in Cutter


1. Building our own CrackMe in SASM

 We programmed our CrackMe with the help of AI, specifically Google's Gemini AI. It was a constant attempt of trial and error (and asking questions to understand) but the end result was this code:


extern printf

extern scanf


section .data

    prompt_user db "Enter Username: ", 0

    prompt_pass db "Enter Password: ", 0

    success_msg db 10, "ACCESS GRANTED! Welcome to the system.", 10, 0

    denied_msg  db 10, "ACCESS DENIED! Invalid credentials. Try again.", 10, 10, 0 

    format_str  db "%s", 0

    real_user   db "admin", 0

    real_pass   db "SuperSecret2026", 0


section .bss

    in_user resb 50

    in_pass resb 50


section .text

global main          ; Removed the underscore

main:                ; Removed the underscore

    push ebp

    mov ebp, esp 


.login_prompt:

    ; --- GET USERNAME ---

    push prompt_user

    call printf

    add esp, 4

    

    push in_user

    push format_str

    call scanf

    add esp, 8


    ; --- GET PASSWORD ---

    push prompt_pass

    call printf

    add esp, 4

    

    push in_pass

    push format_str

    call scanf

    add esp, 8


    ; --- MANUAL USERNAME CHECK ---

    mov esi, in_user      

    mov edi, real_user    

.user_loop:

    mov al, [esi]         

    mov bl, [edi]         

    cmp al, 0

    je .user_check_end

    cmp al, bl            

    jne .denied           

    inc esi               

    inc edi

    jmp .user_loop        

.user_check_end:

    cmp bl, 0               

    jne .denied           


    ; --- MANUAL PASSWORD CHECK ---

    mov esi, in_pass      

    mov edi, real_pass    

.pass_loop:

    mov al, [esi]         

    mov bl, [edi]         

    cmp al, 0

    je .pass_check_end

    cmp al, bl            

    jne .denied           

    inc esi               

    inc edi

    jmp .pass_loop        

.pass_check_end:

    cmp bl, 0

    jne .denied           

    jmp .granted          


.denied:

    push denied_msg

    call printf

    add esp, 4

    jmp .login_prompt      


.granted:

    push success_msg

    call printf

    add esp, 4


    ; Cleanup and exit

    mov esp, ebp

    pop ebp

    xor eax, eax          

    ret


Our code starts in the data section, where we defined all the strings for the prompts and the messages along with the hardcoded credentials. We also used the bss section to reserve 50 bytes of space for the username and password inputs. For the logic we used extern declarations to call printf and scanf from the C library because the lab required it. This meant we had to push the arguments onto the stack and then clean up the stack with add esp after every call. The most important part is the manual comparison loop where we check the input against the real credentials byte by byte using the esi and edi registers. We chose to write our own loop instead of using a built in function to meet the extra challenge requirements. If a character does not match it uses a conditional jump to go to the denied section which tells the user to try again. If the input matches perfectly it jumps to the granted section to print the success message and then exits the program by restoring the stack and returning zero.


After writing the code we needed to put it into SASM, which is a lightweight platform meant for begginers to write assembly. This can be seen in the picture down below:


We then tried to put in the false and correct initials inside the SASM terminal to see if everything works properly, and as can be seen below, it did.




2. Analyzing our Binary in Cutter



2.1 Assembly Literacy

Looking at the graph view for our program we can see exactly how the code branches and handles the data through the different blocks. 

In the first large block at address 0x00401393 the program uses the instruction push str.Enter_Username to put the memory address of our prompt string onto the stack. This is required because the printf function needs that specific address as an argument to know what text to display on the screen. 

Further down in that same block at 0x004013a0 we see push 0x405020 which pushes the address of the in_user buffer we reserved so that scanf knows exactly where in memory to save the characters the user types in. 

After the input is stored the program uses mov esi, 0x405020 at 0x004013d1 to load that buffer address into the esi register. This sets up esi as a pointer so we can start our manual loop and read the input character by character.

 Inside the loop block at 0x004013db the instruction mov al, byte [esi] is used to grab a single byte from the memory address held in esi and move it into the al register. This is a key step because it lets the cpu compare that specific letter against the hardcoded password.

 Finally at 0x004013e5 we see the jne 0x401411 instruction which creates the main fork in our graph. This is a conditional jump that triggers if the characters do not match and as we can see from the red arrow it sends the program to the failure block to print the denied message.


2.2 Function Prologue and Epilogue




Looking at the top of our graph we can see the function prologue which consists of the instructions push ebp and mov ebp esp. This pattern is essential because push ebp saves the base pointer of the previous function so that we do not lose track of where we came from. The next instruction mov ebp esp sets our current base pointer to the top of the stack which creates a new stack frame for our main function to use for local variables and arguments. 

At the very end of our program in the bottom right block we can see the function epilogue starting at address 0x00401430. This part uses mov esp ebp to restore the stack pointer followed by pop ebp which brings back the original base pointer we saved at the start.

 Finally the ret instruction is used to jump back to the calling code and finish the program execution. These patterns exist because every function needs its own organized space in memory to work correctly without interfering with other parts of the system and the epilogue ensures that everything is cleaned up before we exit.


2.3 Conditional Logic Identification

We can see the key conditional check in our disassembly at address 0x004013e3 where the program uses the instruction cmp al bl followed by jne 0x401411.  


the bl register holds a byte from our credential string and this instruction specifically checks if we have reached the null terminator to ensure the entire password was matched. the cmp instruction compares the value to zero and the jne instruction creates a fork in the graph where it will jump to the failure routine at address 0x401411 if the condition is not met. in the decompiler view this logic is represented by the statement if pcVar2 == 0 which is the high level way of checking for that same null character. if the condition is true the decompiler shows the program entering the block that prints the access granted message and returns zero to exit. if the condition is false the code skips the success block and jumps straight to the label for code r0x00401411 which matches the exact same failure jump target we see in our assembly graph.




Part 3: Patching the Binary in Cutter

To ensure the program always grants access regardless of the input, we implemented a comprehensive patching strategy that redirects the control flow at every critical decision point. Instead of just neutralizing a single check, we targeted three specific memory addresses to force a jump directly to the success message located at 0x00401423. This "Global Redirection" approach ensures that whether the program is checking the username length, comparing password characters, or making its final validation decision, every logical path is hijacked and sent to the "ACCESS GRANTED" block.

The first modification was made at address 0x004013e1. In the original binary, this was a conditional jump that checked for the end of a string or a specific comparison result. By right-clicking this instruction in Cutter and selecting "Edit" > "Patch Instruction," we changed the original code to a jmp 0x00401423. This acts as our "Early Exit" bypass; as soon as the program begins its validation routine, it is immediately forced to leapfrog over the entire comparison logic and land directly on the success message. This effectively cracks the program before it even has a chance to evaluate the password characters.

To make the patch even more robust, we applied a second redirection at address 0x0040140d. This location originally held a jne instruction that served as the final gatekeeper before the failure message. By overwriting this with another jmp 0x00401423, we ensured that even if the program somehow bypassed our first patch, it would still be forced into the success path at the final moment. Finally, we patched the address 0x00401411, which is the actual entry point of the "ACCESS DENIED" code block. By changing this to a jump as well, we created a safety net: even if the program "thinks" the user failed and attempts to show an error, the very first instruction it hits will simply bounce the execution back to the "Access Granted" message.




The reason this three-point strategy is so effective is that it removes the need to understand every nuances of the comparison loops. In reverse engineering, if you can identify the "Convergence Point"—the memory address where the success message begins—you can turn any conditional instruction into an unconditional gateway. By replacing the logic at 0x004013e1, 0x0040140d, and 0x00401411 with direct jumps to 0x00401423, we have fundamentally rewritten the software's destiny. The original security model is now completely bypassed, making any combination of username and password technically "correct."

Additionally, what is worth mentioning, since we changed everything, even the graph changed. 




Wednesday, March 18, 2026

Lab 7: Static Software Patching

 Introduction

    Software consists of binary instructions executed by the CPU. Static patching involves altering these instructions on the disk to change program behavior permanently. It is a fundamental skill in reverse engineering used to bypass security checks or modify legacy code.

    For this project, we used Cutter to analyze a Level 1 CrackMe. By identifying and modifying specific assembly instructions, we bypassed a registration check without knowing the original source code.

This Lab will go through these main points:

  1. Introduction => An overview of the chosen CrackMe, my motivation, and the toolset used (Cutter)
  2. Initial Recon => Investigating file architecture, metadata, and extracting clues from embedded strings.
  3. Static Analysis => A deep dive into the main function, including identification of function prologues, epilogues, and key assembly instructions.
  4. Finding the Check => Locating the critical conditional jump by comparing Disassembly and Decompiler views.
  5. Patching Strategy => The technical walkthrough of the binary modification, including specific opcode changes and the final result.
  6. Conclusion => Final thoughts on the challenge, lessons learned, and alternative "KeyGen" approaches.

1. Introduction

    For this analysis, I selected Synok's KeyGenME #1 from crackmes.one. It is categorized as a Level 1 challenge, making it an ideal starting point for learning how programs handle user input and validation logic. To find this crackme, I went to this link (https://crackmes.one/search), and then input the search filters down below: 



From which, then I selected:


    My motivation for selecting this crackme was quite straightforward, i initially found it as an interesting concept. Through this link (https://reasonlabs.com/blog/what-is-a-keygen-how-to-safely-activate-software), i discovered that Keygens are a tool designed to generate serial numbers or product keys for software programs.  

    To actually crack the software, the analysis was performed using Cutter. Cutter is simply a graphical user interface for the actual code, allowing us to bridge the gap between raw assembly and readable logic. This is a really interesting, beginner-friendly tool, since it provides a multi-view environment. This means that we can see the disassembly (raw instructions), the compiler (reconstructed C code), and the graph view (the visual map of the logic) at the same time.

2. Initial Recon

    Before touching the code, we must understand the environment. Initial reconnaissance involves gathering metadata to define the program's context and identify the main pieces that will guide the analysis.
    First, we have to load our crackme into the cutter. This is showcased by the pictures below.






After loading the crackme into cutter, we are presented with the file's architecture and metadata. 


    As we can see, our target is a Windows PE (Portable Executable) file. It is built for the x86 (32-bit) architecture, specifically the i386 machine type. Metadata tells us the rules of the system. This binary was compiled in December 2008 and is a Windows CUI (Console User Interface) application. This confirms that I should look for 32-bit registers (like EAX, EBX, and ESP) and expect the program to run in a command-line environment rather than a graphical window. 

This can also be seen by simply running the keygen.


    The keygen asks us for a name, on which it then logically creates a registration code by itself, and since we dont have the proper code to match it tells us to try again.

    Additionally, we found 823 strings, from which the most important were "eg name ", "Reg Code", "Cracked! :(" and "Try again ! :(". We found these strings by simply first looking at the graph, and then clicking on the cross-reference on the str.cracked part of it, as can be seen below.


We can also see other text that tells us that searching through strings might be useless.

3. Static Analysis

    To understand how a program functions, we must examine its Static Analysis. This is the process of reading the code's structure before it is executed. By using Cutter’s Graph View and Disassembly, we can identify the patterns that govern the program's behavior.

    Every function follows a specific routine in order to manage memory. This is known as the Prologue (at the start) and the Epilogue (at the end). In the main function, the prologue sets up the stack frame. It uses push ebp (extended base pointer, which is usually used as a stable checkpoint) to save the old pointer. Then it uses mov ebp esp (extended stack pointer, which holds the memory of the current program at runtime) to set the new one. Finally, sub esp 0xc0 (it substracts 192 bytes of data in the stack, in this case to allocate space for the function) makes space for your data on the stack. The epilogue at the end uses pop ebp and ret to clean up and go back to the system, both of which can be seen below. 



    To properly understand what the code was writing, and what the program actually does we decided to first research the main assembly functions, and then research what the key logic of the keygen actually is, and how it is usually cracked. 

    For the assembly part, we created a simple table explaining what each function does, which you can find below: 


    When it comes to the keygen usual logic, this program starts by preparing its memory and asking you to type in your name and a registration code. It takes these inputs and keeps them on the stack while it works. The next step is a loop that looks at every character in the name you provided. For every letter the code runs a math algorithm to convert that character into a value. It keeps a running total of all these values in a memory spot. You will notice that it sleeps for a short time after each letter to slow things down. Once it reaches the end of your name it takes the accumulated total and adds it to itself one more time to get the final value.

    After the math is finished the program then takes the code you typed and compares it directly to the number it just calculated. This comparison is the only thing that stands between us and the success message. If the numbers match exactly the program flows into a block of code that prints the cracked message. If the numbers are even slightly different it jumps away to a block that prints the try again message and then exits. This is really simple logic which is present in all keygens, so from now on it is pretty straightforward, we just needed to come up with a solution.

    After searching the internet, we found out that you can beat this challenge in three main ways. One way is to modify the binary file directly to remove the check. This is called patching, and you just replace the jump instruction with a nop instruction so the program never fails. A second way is to run the program in a debugger and set a breakpoint right at the comparison. This lets you pause the program and read the real key directly out of the computer registers. The final way is to study the math loop and reverse the algorithm. Once you understand the math you can write a key generator script that calculates the correct code for any username you want to use. Since we are not familiar with the debugger and reversing algorithms, we tried to simply patch the code so we can crack it. 

4. Finding the Check

    To find the check I started by looking for the success strings I found during the recon phase. I looked up the cracked string in the data section and found its specific address. I used the cross reference tool in cutter to see where this string was being used in the main function. This led me straight to the bottom of the code where the success message is printed. From there I worked backward to find the comparison that happened right before it.

    This brought me to the cmp instruction which is the moment the program checks your work. I saw the jne instruction right after it which acts like a gatekeeper to send you to the failure message. When I switched to the decompiler view it made the logic even clearer. It showed a simple if statement comparing two variables which matched exactly with the assembly instructions.


5. Patching Strategy

    As mentioned before, the best method to actually crack this program is patching. To patch the program I went back to the jump at address 0x00401649. This is the part that sends the user to the failure message when the code is wrong. 

    I right clicked the instruction and selected the edit option to change the code. I replaced the jne instruction with two nop instructions. Nop stands for no operation which means the cpu does nothing and just moves to the next line. By doing this the program can no longer jump away to the error message. It is forced to go straight to the cracked message no matter what code you type in.





    In order to see if the patch actually worked i ran a simple test, i put a random name and password beforehand and after the patch and it gave me these results.

Before the patch:

After the patch:


    Based on the message we got (which is the same as the one where the reg code matches the name within the code), and based on the logic we used, we can conclude that we successfully cracked the Crackme keygen.

6. Conclusion

    Working on this crackme taught me that software is just a series of logical choices. I learned how to identify the specific moments when a cpu decides between success and failure. The hardest part was understanding the math loop that processed the name but finding the strings helped me work backward to the solution. I realized that you do not always need to solve the math if you can just change the logic of the program.

    I could have also solved this by writing a keygen script to calculate the real codes. Another way would be using a debugger to see the real key in the registers while the program is running. This project was a great way to practice the adversarial mindset needed for cybersecurity. It showed me how easy it is to bypass basic checks once you know how to read assembly.

Thursday, March 5, 2026

LAB 6 - Binary, Decimal and Hexadecimal Numbering Systems in Computer Systems

Introduction

Modern computers are complex, high-speed electronic devices that process, store, and manipulate data. However, every program, OS, or malware sample, no matter how complex, is reduced to numbers so it can be understood by the computer's CPU.

This Lab will go through these main points: 

  1. What is the CPU
  2. How does a CPU work
  3. Numbering Systems (Binary, Decimal, Hexadecimal)
  4. How binary numbers relate to the physical operation of computer hardware and CPUs
  5. Why humans commonly use decimal representation and its limitations in low-level computing
  6. Why is hexadecimal notation widely used in debugging, reverse engineering, and memory analysis
  7. How CPU instructions are ultimately stored and executed as binary values in memory
  8. The relationship between machine code, hexadecimal instruction bytes, and assembly language instructions

1. What is the CPU

To understand the true function of the CPU (Central Processing Unit), we went to IBM through this link (https://www.ibm.com/think/topics/central-processing-unit#:~:text=A%20central%20processing%20unit%20(CPU)%20is%20the%20primary%20functional%20component,in%20a%20highly%20orchestrated%20way.).

The CPU is the primary component of a computer. It’s a group of electronic circuits that run a computer's operating systems and applications and manage a variety of other computer operations, so basically everything. To put it simply, the CPU is essentially the active brain of the computer, where data input is transformed into information output, meaning it stores and executes program instructions through the vast network of circuits it has.

3 main components in the CPU

The Control Unit

The part of the CPU where we can find the circuits that guide the computer system through a system of electrical pulses and notify it to execute high-level computer instructions, think of it as a human manager assigning particular tasks to different workers.

Arithmetic/ logic Unit (ALU)

This part takes care of all the arithmetic operations and logical operations as the names suggests. It has 4 main types of operations: addition, subtraction, multiplication and division.

Memory Unit

 This part has several main functions, from handling the data flow which happens between the RAM and the CPU, to taking care of the cache memory. This part also has data and instructions needed for data processing and memory-protection.


Other important components of the CPU

Cache – a small, ultra-fast type of volatile memory located directly on or very near to the processor. It acts as a high-speed buffer between the CPU and the RAM. It is used to prevent processor idle time

Registers – a form of permanent memory which can be accessed the millisecond theyre needed.

Clock – It’s the part that issues electrical pulses at regular intervals, which coordinates the complicated circuitry within the CPU in a highly synchronized manner.

Instruction register and pointer – This part of the computer system shows the exact location of the next instruction to be executed by the CPU.

Buses – only one role, it ensures proper data transfer and data flow between various components of the computer system. It has a “width”, which essentially means how many bits can be transferred via the bus in parallel.


2. How does a CPU work

The functionality of the CPU is handled by the control unit, with synchronization assistance from the computer clock.  The CPU functions on a set instruction cycle, which has 3 main points:

Fetch – First, the CPU retrieves a specific binary instruction from the computer's RAM based on an address provided by the program counter.

Decode – The decoder within the CPU then translates the binary instructions into electrical signals that engage other parts of the CPU.

Execute – The CPU performs the required action, such as a mathematical calculation or even data transfer

Now that we’ve understood the basics of how a CPU works, we also need to understand numbering systems and how they map information directly to CPU instructions.


3. Numbering Systems (Binary, Decimal, Hexadecimal)

Numbering systems are systematic methods on how we represent write and interpreting numbers using specific digits and rules, and different values based on position. Each type of numbering system has a different power of the system’s base, on which they’re also separated. For this lab, were going to focus on 3 main ones: Binary (base 2), Decimal (base 10), and hexadecimal (base 16).

Binary

The binary numbering system is a base-2 positional numeral system that uses only two digits, 0 and 1 (bits), to represent data, instructions, and numerical values. A single digit is called a bit, which is the smallest unit of data in computers. A byte, then, is worth 8 bits. Binary is essentially the “language” of computers, and everything gets converted into binary. At the most fundamental level, computers are dumb machines that don’t understand what numbers really are. A number for a computer is simply a collection of billions of microscopic switches that we call transistors.

Decimal

The decimal number system is a base-10 numeral system that uses digits from 0 to 9. The decimal number system is the most intuitive and easiest numeral system for us to understand, but computers cannot process straight decimals since it cannot be saved in memory, and registers do not operate in the same way.

Hexadecimal

The hexadecimal numbering system is a base-16 numeral system that uses digits from 0-9 (the deca part of the name) and letters from A-F. The letters from A-F represent the numbers from 10-15. Even though it seems really counterintuitive, hexadecimal is actually a really important way of representing data. Since raw binary can be quite draining to read and understand, we developed hex as a more compact way of showing this data. Hexadecimals have a prefix of 0x when written, so for example, the number 13 is equal to “0xD”.

 

4. How binary numbers relate to the physical operation of computer hardware and CPUs

As mentioned, Binary essentially just is the mathematical language we use to describe the physical state of the transistors (either high voltage, usually from 1.8-5V, or low voltage, which is close to 0 V). Binary values then form bits, and then bits form bytes, which represent data, instructions, and even memory. Additionally, when we combine transistors, we get Logic Gates, which are responsible for logical operations like AND, NOT, OR, NAND, NOR, XOR, and XNOR (essentially all Boolean operations).



5. Why humans commonly use decimal representation and its limitations in low-level computing

Even though Binary is practical for computer usage, it is highly non-intuitive for us humans. This is simply explained by our evolution. Since we had 10 fingers, and we started counting by using them, our reasoning developed to use a base-10 numbering system for everything, starting from our everyday calculations, to more complex algorithms such as high-end transactions.

Even though the Decimal system seems to be easier to use for us, this is not the case when it comes to low-level computing. Since transistors are simply electrical switches that work on 1/0s, or high and low voltage, binary is a perfect match for them since they only have to tell apart from 2 physical states of the electrical flow. If we wanted to implement transistors that use the decimal system even as a low-level language, we would have to build transistors that could tell apart from 10 distinct voltage levels, which would then be prone to errors and electrical noise or disruptions.  


6. Why is hexadecimal notation widely used in debugging, reverse engineering, and memory analysis

As we said beforehand, the hexadecimal notion is essentially just a way to make the binary data more representable and understandable for us to read when conducting analysis, whether that being debugging, reverse engineering, or memory analysis. Hex is easier to use because each digit of hex represents four binary bits, which means a byte can simply be represented in just 2 hexadecimal digits. It is worth mentioning that even though computers store information in binary, through various tools, the data will mostly be displayed in hex through all systems. In our previous labs, we saw various tools that represent the data in hex, such as PE Bear, PE Studio, x64dbg, etc.

 

7. How CPU instructions are ultimately stored and executed as binary values in memory

A CPU bit is represented by electrical signals. The hardware is then properly designed to tell apart from 2 levels of voltage mentioned above. The binary system is quite reliable, since the hardware can easily distinguish between only 2 voltage levels.

The CPU then uses transistors as electronic switches. When a voltage is applied to the gate of a transistor, it allows the electrical current to either flow (which is a 1 bit), or block it (a 0 bit). By managing the voltage in specific patterns, we can then create logic gates (AND, OR, NOT, XOR).

The CPU essentially is just a massive network of these logical gates organized into functional units. The ALU then uses a complex arrangement of these logic gates (called adders) to perform mathematical functions. For example, to add two binary numbers, the ALU sends electrical pulses through a series of XOR and AND gates that then physically carry bits to the next column.  Registers, on the other hand, physically trap a 1 and a 0 in a loop, further allowing the CPU to remember a value momentarily.

The CU then just acts as the conductor. It receives binary instructions from the memory, and then uses logic gates to route electricity to the correct part of the CPU to execute that specific command. This is done through the fetch, decode, and execute, which we previously mentioned.

Additionally, the CPU clock also plays an important role, where with every “tick” of the clock, the transistors update their states, which prevents the electrical pulses from crashing into each other.

 

8. The relationship between machine code, hexadecimal instruction bytes, and assembly language instructions

Binary, hexadecimal instruction bytes, and assembly are a simple hierarchy of languages that are at the heart of every computer, telling the CPU what to do.

As previously mentioned, machine code, also known as Binary, is the only language the CPU actually understands, consisting of only 1s and 0s. Since it's really difficult for us to understand them, we use other formats, such as Hex.

Hexadecimal allows us to write or read machine code in a more compact way. Instead of instructions in a byte, we group them into two characters (example 1011 0000 in binary would be Bx0 in hex, which is 176 in decimal). This doesn’t change the data or what the computer does in any way, it just makes the data easier to look at without getting lost in the 1s and 0s.

Assembly, on the other hand, is just the human-readable version of machine code. For example,  if we want to tell the computer to put the number 100 in a specific slot, we would have to remember specific commands in either hex or binary (B0 64 for hex, and 10110000 01100100 for binary). Instead, in assembly, we can use a short word like MOV to do the same thing.

We should think of them simply as just 3 ways to do the exact same thing. It's worth mentioning that we can turn Assembly into machine code with a tool called an assembler, and do the opposite with a tool called a disassembler.



Tuesday, February 17, 2026

LAB 5: Inter Process Communication – Shared Memory

Inter-Process Communication (IPCs)

            In every modern operating system, multiple programs run at the same time. Each of these programs have their own executing instances (think of them as an active unit of work which needs resources like CPU time, memory, files), which we call processes.

            By default, every process is independent (think of them as islands).  This simply means that they are isolated from one another and think that they are the sole process in the system. This is a direct result of the Memory Management Unit (MMU). The MMU is a computer hardware part that translates virtual memory addresses into physical addresses, as well as manages cache and enforces memory protection.  

            In order to keep up with the next parts of the lab, we will also need to explain what virtual memory, virtual memory address, and physical memory are.

            The “physical memory” is just the physical RAM of the computer, which is accessible for fast and precise data transfers or calculations within a system. 

            The virtual memory is simply a method that “extends” the RAM, meaning that it enables the system to compensate for physical memory shortages by temporarily transferring data from the RAM to the Disk storage.

            The virtual address is just a simple compartment of the whole virtual memory, as can be seen in the picture below.

As mentioned, all of this process is possible because of the MMU, which translates the virtual memory addresses into physical addresses with efficiency.

However, sometimes we also need these processes to communicate with each other. The processes that affect or are affected by one another are simply called Cooperating Processes.

Cooperating Processes are made possible by the Inter-Process Communication (IPC). IPC refers to the set of techniques that allows these processes to talk and share data with each other while theyre operating, so complex software can be run efficiently (a good example would be a shared database). We have a lot of IPC mechanisms, such as Named and Anonymous Pipes, mailslots, sockets, and even clipboards. However, in this lab, we will be focusing on a specific IPC named Shared Memory.  


Shared memory

            Among the various methods of IPCs, shared memory is one of the most efficient mechanisms, especially when it comes to performance-critical applications; think of it as the Ferrari of IPCs.

            Shared Memory, as a concept, is the memory region that can be accessed simultaneously by multiple processes. In an OS, each process has its own virtual address space. Shared memory then proceeds to create a shared region of memory that is accessible by multiple processes at the same time. This shared region is then mapped into the virtual address space of each of the processes, allowing them to read and write to the same physical memory location, as can be seen in the picture below.



            To fully grasp the concept of how shared memory initializes itself, we looked at the article below:

(https://www.geeksforgeeks.org/computer-organization-architecture/what-is-a-shared-memory/),  

Let's say we have two processes that want to communicate with each other, we'll call them P1 and P2. Let's also say that P1 has an address space which we’ll call A1, and P2 has an address space which we’ll call A2. In order for the shared memory to start, P1 will first take up some address space as a shared memory space, which we’ll also call S1. After P1 takes S1, it will then write into it the data that it wants to share. P2 will simply read from the shared data found in S1. Additionally, P1 can also give write rights to P2, as can be seen in the picture below:



An interesting fact about the shared memory is that only the creator process has the right to destroy the shared memory, which in our case means that only P1 can end the whole process.

While the concept of shared memory is easy, the process of how it actually works is a bit more challenging. For this part, we continued with another Geeks for Geeks article (https://www.geeksforgeeks.org/operating-systems/ipc-shared-memory/). From here, we understood the basic outline of how the shared memory operates:

Creation of Shared Memory Segment: The parent creates a shared memory segment. This can be done using system calls like shmget(), usually mostly done in Unix-like systems. This segment is then assigned the unique identifier (shmid).

Attaching to the Shared Memory Segment: The processes that need to access the shared memory attach themselves to this segment using shmat() system call. Once attached, the processes can directly read from and write to the shared memory, according to the privileges.

Synchronization: Since multiple processes can access the shared memory at the same time, synchronization between them is a must. This is done through synchronization mechanisms like semaphores. To put it simply, Semaphores are another IPC that are often used ensure data consistency.

Detaching and Deleting the Segment: When a process no longer needs access to the shared memory, it can detach from it using the shmdt() system call. Additionally, the parent process can also remove the shared memory by using the shmctl() system call.


Advantages

            The main benefit of Shared Memory is speed. Since this IPC processes directly read and write to the shared memory location.

            Additionally, it is highly efficient, since it eliminates the overhead associated with the message passing where data has to be copied from one process to another. This then also means that it can be particularly useful for transferring large amounts of data between the processes.


Disadvantages

            The main downside of shared memory is the complex synchronization. It requires explicit synchronizations to prevent race conditions (which make the code complex and error-prone). Additionally, it can sometimes be a tedious process since it requires manual cleanup by manually detaching and removing the segments.

            The manual cleanup can also lead to security risks, where unauthorized processes may access or modify data.


Security Exploitation cases

            Throughout history, there have been many cases where the Shared Memory IPC has been exploited by malware. Malware uses this IPC as a technique to split its functionality across multiple processes without writing data to the disk (which helps it evade antivirus file scans), or even generate network traffic (which helps it evade firewalls). This technique is called Shared Memory Injection.

The two main instances we have of shared memory injections are the SquidLoader and the RotaJakiro malware.

            SquidLoader is a very recent and relevant example that gained influence in late 2024. This specific malware used shared memory to create a complex graph of shared memory segments, in which it broke its encrypted payload into parts. This then would mean that analysis was made really hard since the full malicious code didn’t exist in one place until the exact moment of execution.

            RotaJakiro, on the other hand,d is classified as a Linux backdoor. Through the shmget() system call, it checked if other instances of the malware were already running as well as shared the PIDs between its various forked processes. This then allowed the malware's different parts to coordinate without sending any signals that could possibly be detected by the Endpoint Detection and Response tools.


Conclusion

            In conclusion, Shared Memory serves as a vital, high-performance Inter-Process Communication (IPC) mechanism that bridges the gap between isolated processes managed by the Memory Management Unit (MMU). By mapping a single physical memory region into the virtual address spaces of multiple distinct processes, it eliminates the overhead of data copying to achieve superior speed, effectively acting as the "Ferrari" of IPCs.  However, this efficiency necessitates rigorous implementation, requiring developers to manage manual memory lifecycles and enforce synchronization via semaphores to prevent race conditions. Ultimately, while shared memory is indispensable for performance-critical applications, it introduces a significant attack surface; as evidenced by malware like SquidLoader and RotaJakiro, the very features that facilitate direct access can be weaponized for stealthy injections and evasion, highlighting the critical trade-off between operational speed and system security.




LAB 10: APK Security Scanner - Multi-Agent Static Analysis pt.2

Taking Android Apps Apart: Building an AI-Powered Security Scanner Part 2: The Analysis, Execution and Findings In Part 1 we defined our obj...