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:

  1. Ensures neither integer is 0 or the second integer is 1.
  2. Computes the division of the first integer by the second.
  3. Compares the result of the division to the first integer.
  4. 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.
  1. Prompt for Input: The program prompts the user for two integers.
  2. Initial Check: It checks if neither integer is 0 or the second integer is 1. Our inputs (2147483648 and -1) bypass this check.
  3. Division and Comparison:
    • The division 2147483648 / -1 results in -2147483648.
    • The program then checks if -2147483648 equals 2147483648, which due to Signed/Unsigned Mismatch, effectively bypasses the check.

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