Hacking echod

about | archive

[ 2007-November-08 21:19 ]

This week, I attended an interesting talk by Nelson Elhage about security technologies in Linux, and how to work around them. The technologies discussed were address space layout randomization (ASLR), no-exec stacks, and stack guards. At the end of the presentation, Nelson posed a challenge: get the contents of /cookie, a world-readable file on a machine running a vulnerable echo server. The source code and binary were made available. The server had ASLR enabled, but no other protection. I took a quick look at the code and figured that it would be easy. Then I ended up wasting about five hours figuring getting my hack to work. Here is how I did it.


This server has a classic buffer overflow bug. Since the stack on the target was executable, I thought it would be easy to use a traditional stack smashing buffer overflow. To begin, I started the echo server on my machine, set a breakpoint in the handle_request. I then used netcat to send it a simple hello request: echo hello | nc localhost 20037. After discovered GDB's set follow-fork-mode child and got the code to hit the breakpoint, I disassembled the function and poked around to determine the stack layout:

echod stack layout in the handle_request function

To exploit the bug, simply write 112 bytes of NOPs and the shell code, then overwrite the return address to point into the buffer. It will then execute the code. I was planning on using dup2(fd, 0); dup2(fd, 1) to get the socket to replace standard input and output. Then calling execve("/bin/sh", ...) would give me a shell connected to the socket. While using GDB to figure out the value for fd, I noticed that it was always 1 (standard output). That means that instead of getting a shell, I could execute cat /cookie and it would send the file over the socket. A bit more hacking later I ended up with the exploit code. I then hacked a Python script to print a chunk of NOP instructions, the assembly code and the buffer address from GDB. Running ./hack.py | nc localhost 20037 gave me the flie on my machine.

The remaining challenge was to attack the remote machine when we don't know the stack address. I was short on time so I used the quick and dirty brute force approach: try every possible address until it works. However, a bit of intelligence makes the problem tractable. First, a 32-bit process running on a 32-bit Linux kernel has stacks in the address range 0xbf800000 - 0xbfffffff. This is because the kernel lives in the top 1GB of ram (0xc0000000 and up). On a 64-bit kernel, the kernel can give the process the entire 32-bit virtual memory space, so the stacks live in the range 0xff800000 and up. The target was running on a 32-bit kernel, so now we are down to ~8.4 million addresses. Next, we don't need to get the stack address exactly right. If we land anywhere in the NOP section of the buffer, we will get it. To improve my chances, I filled the entire 112 bytes of the buffer with JMP instructions with an offset of 0. This means that if we land anywhere in the buffer, the program will enter an infinite loop. We can detect this hang remotely, then we know we are in the right ballpark.

I wrote a Python program to try every 112 byte stack offset until it detected a hang from the remote end. This has to test ~75000 addresses. This is a large number, but feasible for a brute force approach. Unfortunately, the address was pretty close to the end (0xbfeb63a0), so it took a long time. Finally, I modified my program to probe at finer intervals and inject the exploit. This worked immediately, giving me the contents of /cookie.


Address space layout randomization doesn't provide much protection on 32-bit processors. There just isn't enough virtual address space, so while it slows an attacker down, it doesn't stop them. It should provide significantly more effective protection for 64-bit processes, and in combination with other techniques it does help. Finally, I now understand why people enjoy doing this. It is a fun puzzle to see how you can make something work within the limitations imposed by the system.