- Difficulty Level: Beginner
- URL: https://github.com/DownUnderCTF/Challenges_2024_Public/tree/main/beginner/number-mashing
- SHA-256 Hash:
f8b5c95e4b9d876abe0e6ca63467ceae84371740296f194cde106257bd62a92a
Introduction Link to heading
Number Mashing introduces an ARM binary with a Signed/Unsigned Mismatch vulnerability. The binary performs several checks on user inputs before attempting to read a file named “flag.txt”. By bypassing these checks, we can manipulate the program’s logic to read the file and retrieve the flag.
Tools Used Link to heading
- gdb - The GNU Debugger
- peda-arm - Python Exploit Development Assistance for GDB
- Ghidra - A software reverse engineering (SRE) suite of tools developed by NSA’s Research Directorate
Initial Analysis Link to heading
Binary File Information Link to heading
First, we check the file type of the binary to understand its properties.
kali@kali:~$ file ./number-mashing
./number-mashing: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=ab93f9bc0ec8c3d321da1b7e954e739e13ee8ab1, for GNU/Linux 3.7.0, not stripped
This indicates that the binary is an ARM 64-bit ELF executable for Linux.
Initial Execution Link to heading
Running the binary initially to observe its behavior:
kali@kali:~$ ./number-mashing
Give me some numbers: [num1] [num2]
Nope!
Entering random integers results in the “Nope!” message, indicating that the input validation failed.
Analyzing the Decompiled Code Link to heading
By using Ghidra to decompile the binary, we gain insights into the program’s flow:
/* Irrelevant information omitted */
/* Main function performing input checks and attempting to read flag.txt */
undefined8 main(void)
{
int local_11c;
int local_118;
int local_114;
FILE *local_110;
undefined8 local_108;
printf("Give me some numbers: ");
__isoc99_scanf("%d %d", &local_11c, &local_118);
if ((local_11c == 0) || (local_118 == 0) || (local_118 == 1)) {
puts("Nope!");
exit(1);
}
local_114 = 0;
if (local_118 != 0) {
local_114 = local_11c / local_118;
}
if (local_114 != local_11c) {
puts("Nope!");
exit(1);
}
local_110 = fopen("flag.txt", "r");
fread(&local_108, 1, 0x100, local_110);
printf("Correct! %s\n", &local_108);
return 0;
}
The program first prompts the user for two integers and performs the following checks:
- Ensures neither integer is 0 or the second integer is 1.
- Computes the division of the first integer by the second.
- Compares the result of the division to the first integer.
- If the checks are bypassed, it opens “flag.txt” and reads its contents.
Understanding Signed/Unsigned Mismatch Link to heading
A Signedness Error, also known as a Signed/Unsigned Mismatch, occurs when a program incorrectly interprets an integer’s sign. This vulnerability arises from improper handling or mixing of signed and unsigned integers, leading to unexpected behaviors and potential security flaws.
To understand this vulnerability better, we need to refresh our memory about C data types. Let’s quickly go over them.
Data Type | Format Specifiers | Size | Range |
---|---|---|---|
Signed char | %c | 1 Byte | -128 to 127 |
Unsigned char | %c | 1 Byte | 0-255 |
Int or Long int or Signed Long Int | %d | 4 Bytes | -2147483648 to 2147483647 |
Unsigned int or Unsigned long int | %u | 4 Bytes | 0 to 4 Gigabytes |
Short int | %hd | 2 Bytes | -32768 to 32767 |
Unsigned short int | %hu | 2 Bytes | 0 to 65535 |
float | %f | 4 Bytes | 3.4E-38 to 3.4E+38 |
double | %lf | 8 Bytes | 1.7E-308 to 1.7E+308 |
Long Double | %Lf | 10 Bytes | 3.4E-4932 to 1.1E+4932 |
Take a look at the range column in the table. This column shows the range of values that each data type can hold.
Signed types can include both positive and negative values, but unsigned types can only include positive values.
For example, the %d
data type, which is used for signed integers, can hold values from -2147483648 to 2147483647. This means the smallest value it can store is -2147483648, and the largest value it can store is 2147483647.
Detailed Walkthrough Link to heading
Step 1: Run the binary using The GNU Debugger (gdb) Link to heading
First, we need to start the GNU Debugger with the binary we want to analyze. This will allow us to inspect the program’s execution and debug it effectively.
kali@kali:~$ gdb ./number-mashing
Step 2: Identifying Functions Link to heading
Listing all defined functions to understand the binary structure.
peda-arm > info functions
All defined functions:
Non-debugging symbols:
0x0000000000000840 _init
0x0000000000000880 exit@plt
0x0000000000000890 __libc_start_main@plt
0x00000000000008a0 __cxa_finalize@plt
0x00000000000008b0 setvbuf@plt
0x00000000000008c0 fopen@plt
0x00000000000008d0 __stack_chk_fail@plt
0x00000000000008e0 __gmon_start__@plt
0x00000000000008f0 abort@plt
0x0000000000000900 puts@plt
0x0000000000000910 fread@plt
0x0000000000000920 __isoc99_scanf@plt
0x0000000000000930 printf@plt
0x0000000000000940 _start
0x0000000000000974 call_weak_fn
0x0000000000000990 deregister_tm_clones
0x00000000000009c0 register_tm_clones
0x0000000000000a00 __do_global_dtors_aux
0x0000000000000a50 frame_dummy
0x0000000000000a54 main
0x0000000000000bdc _fini
Step 3: Analyzing the Assembly Code with GDB Link to heading
Disassembling the main
function to understand the stack layout and variable placement.
peda-arm > disassemble main
Dump of assembler code for function main:
// Irrelevant information omitted
...
0x0000000000000ab0 <+92>: bl 0x930 <printf@plt> // Call printf("Give me some numbers: ")
0x0000000000000acc <+120>: bl 0x920 <__isoc99_scanf@plt> // Call __isoc99_scanf("%d %d", &local_11c, &local_118)
0x0000000000000ad0 <+124>: ldr w0, [sp, #20] // Load local_11c
0x0000000000000ad4 <+128>: cmp w0, #0x0 // Compare local_11c with 0
0x0000000000000ad8 <+132>: b.eq 0xaf4 <main+160> // If local_11c == 0, branch to 0xaf4 ("Nope!" and exit)
0x0000000000000adc <+136>: ldr w0, [sp, #24] // Load local_118
0x0000000000000ae0 <+140>: cmp w0, #0x0 // Compare local_118 with 0
0x0000000000000ae4 <+144>: b.eq 0xaf4 <main+160> // If local_118 == 0, branch to 0xaf4 ("Nope!" and exit)
0x0000000000000ae8 <+148>: ldr w0, [sp, #24] // Load local_118
0x0000000000000aec <+152>: cmp w0, #0x1 // Compare local_118 with 1
0x0000000000000af0 <+156>: b.ne 0xb08 <main+180> // If local_118 != 1, branch to 0xb08
0x0000000000000af4 <+160>: adrp x0, 0x0 // Load page address of "Nope!" string
0x0000000000000af8 <+164>: add x0, x0, #0xc18 // Load offset of "Nope!" string
0x0000000000000afc <+168>: bl 0x900 <puts@plt> // Call puts("Nope!")
0x0000000000000b00 <+172>: mov w0, #0x1 // Prepare argument 1 for exit()
0x0000000000000b04 <+176>: bl 0x880 <exit@plt> // Call exit(1)
0x0000000000000b08 <+180>: ldr w1, [sp, #20] // Load local_11c into w1
0x0000000000000b0c <+184>: ldr w0, [sp, #24] // Load local_118 into w0
0x0000000000000b10 <+188>: sdiv w0, w1, w0 // local_114 = local_11c / local_118
0x0000000000000b14 <+192>: str w0, [sp, #28] // Store local_114
0x0000000000000b18 <+196>: ldr w0, [sp, #20] // Load local_11c into w0
0x0000000000000b1c <+200>: ldr w1, [sp, #28] // Load local_114 into w1
0x0000000000000b20 <+204>: cmp w1, w0 // Compare local_114 with local_11c
0x0000000000000b24 <+208>: b.eq 0xb3c <main+232> // If local_114 == local_11c, branch to 0xb3c
0x0000000000000b28 <+212>: adrp x0, 0x0 // Load page address of "Nope!" string
0x0000000000000b2c <+216>: add x0, x0, #0xc18 // Load offset of "Nope!" string
0x0000000000000b30 <+220>: bl 0x900 <puts@plt> // Call puts("Nope!")
0x0000000000000b34 <+224>: mov w0, #0x1 // Prepare argument 1 for exit()
0x0000000000000b38 <+228>: bl 0x880 <exit@plt> // Call exit(1)
0x0000000000000b68 <+276>: adrp x0, 0x0 // Load page address of "flag.txt" string
0x0000000000000b6c <+280>: add x1, x0, #0xc20 // Load offset of "flag.txt" string
0x0000000000000b70 <+284>: adrp x0, 0x0 // Load page address of "r" string
0x0000000000000b74 <+288>: add x0, x0, #0xc28 // Load offset of "r" string
0x0000000000000b78 <+292>: bl 0x8c0 <fopen@plt> // Call fopen("flag.txt", "r")
0x0000000000000b7c <+296>: str x0, [sp, #32] // Store FILE* (local_110)
0x0000000000000b80 <+300>: add x0, sp, #0x28 // Load address of local_108
0x0000000000000b84 <+304>: ldr x3, [sp, #32] // Load FILE* (local_110)
0x0000000000000b88 <+308>: mov x2, #0x100 // Size 256
0x0000000000000b8c <+312>: mov x1, #0x1 // Count 1
0x0000000000000b90 <+316>: bl 0x910 <fread@plt> // Call fread(&local_108, 1, 256, local_110)
0x0000000000000b94 <+320>: add x0, sp, #0x28 // Load address of local_108
0x0000000000000b98 <+324>: mov x1, x0 // Move address of local_108 to x1
0x0000000000000b9c <+328>: adrp x0, 0x0 // Load page address of format string
0x0000000000000ba0 <+332>: add x0, x0, #0xc38 // Load offset of format string ("Correct! %s\n")
0x0000000000000ba4 <+336>: bl 0x930 <printf@plt> // Call printf("Correct! %s\n", &local_108)
...
End of assembler dump.
Step 4: Setting Breakpoints and Observing Memory Link to heading
In this step, breakpoints are strategically placed within the main
function to observe specific points of execution and memory state. The first breakpoint is set before the calculation at the instruction located at main+184
(0xb0c), and the second after the calculation and storage at main+196
(0xb18). The program is then run, initiating a prompt for user input where the numbers 8
and 4
are entered. After hitting the first breakpoint, the memory content at the stack pointer plus an offset of 20 bytes is examined, displaying three words of memory: 0x00000008
, 0x00000004
, and 0x0000007f
. Continuing the program to the second breakpoint, the same memory location is checked again, showing a change in the last word to 0x00000002
.
peda-arm > break *main+184
Breakpoint 1 at 0xb0c
peda-arm > break *main+196
Breakpoint 2 at 0xb18
peda-arm > r
Starting program: /root/number-mashing
Give me some numbers: 8 4
peda-arm > x/3wx $sp+20
0x7ffffff8e4: 0x00000008 0x00000004 0x0000007f
peda-arm > c
peda-arm > x/3wx $sp+20
0x7ffffff8e4: 0x00000008 0x00000004 0x00000002
peda-arm > c
Nope!
Selecting Appropriate Inputs Link to heading
To bypass the checks, we need to provide inputs that manipulate the program’s logic:
- First Integer (
local_11c
):2147483648
(0x80000000) - Second Integer (
local_118
):-1
These values are chosen for the following reasons:
2147483648
is the minimum negative number when interpreted as a signed 32-bit integer.- Dividing by
-1
results in-2147483648
, which exploits the properties.
- Prompt for Input: The program prompts the user for two integers.
- Initial Check: It checks if neither integer is 0 or the second integer is 1. Our inputs (
2147483648
and-1
) bypass this check. - Division and Comparison:
- The division
2147483648 / -1
results in-2147483648
. - The program then checks if
-2147483648
equals2147483648
, which due to Signed/Unsigned Mismatch, effectively bypasses the check.
- The division
Example Execution GDB Link to heading
To exploit the signed/unsigned mismatch vulnerability, we input 2147483648
and -1
into the binary using gdb. Here, 2147483648
is interpreted as -2147483648
in a signed 32-bit context, and -1
leverages the mismatch. This bypasses the checks, leading to the successful retrieval of the flag:
peda-arm > r
Starting program: /root/number-mashing
Give me some numbers: 2147483648 -1
peda-arm > x/3wx $sp+20
0x7ffffff8e4: 0x80000000 0xffffffff 0x0000007f
peda-arm > c
peda-arm > x/3wx $sp+20
0x7ffffff8e4: 0x80000000 0xffffffff 0x80000000
peda-arm > c
Correct! DUCTF{w0w_y0u_just_br0ke_math!!}
Example Execution Link to heading
kali@kali:~$ ./number-mashing
Give me some numbers: 2147483648 -1
Correct! DUCTF{w0w_y0u_just_br0ke_math!!}
Conclusion Link to heading
By carefully selecting the inputs 2147483648
and -1
, we can exploit the Signed/Unsigned Mismatch vulnerability in the ARM binary. This allows us to bypass the program’s checks and reach the fopen
function to read “flag.txt”. This exercise demonstrates the importance of proper handling of signed and unsigned integers to prevent vulnerabilities.
References and Further Reading Link to heading
Documentation Link to heading
- GDB Documentation: Comprehensive guide to the GNU Debugger, essential for understanding and debugging binary programs.
- Ghidra: Official website and documentation for Ghidra.
Educational Articles Link to heading
- Signed number representations: In computing, signed number representations are required to encode negative numbers in binary number systems.