Recentemente participei de uma CTF promovida pelo ELT (Epic Leet Team). Uma das challs que consegui resolver completamente foi a matroshka, e aqui está um breve write-up sobre a mesma.

Dado um arquivo matroshka.tar.gz, precisávamos encontrar a flag.

Não era difícil desconfiar do que esse arquivo / chall se tratava: matroshkas são aquelas bonecas russas que se encaixam umas dentro das outras. Então…de cara, logo já desconfiei: provavelmente existe um arquivo compactado dentro de outro, dentro de outro, dentro de outro, e assim por diante…

Por experiência, não valeria a pena tentar descompactar tudo manualmente, pois sabe-se lá quantos níveis de compactação esse negócio iria ter (provavelmente mais do que 100).

De cara logo pensei em usar o dtrx, que é um excelente programa (não perco tempo e sempre rodo um port install dtrx) para extrair arquivos sem ter que ficar se lembrando das sintaxes individuais de cada programa. Nesse caso, não iria rolar: os arquivos eram renomeados de forma a trickear o dtrx, que funciona através de heurísticas, uma delas é a ’extensão’ do nome do arquivo. Por exemplo, vários arquivos (após descompactados) eram renomeados na forma *.elt.

A segunda alternativa foi (serendipidade, não conhecia essa ferramenta antes) tentar utilizar o atool. Por motivos similares ao dtrx, não rolou.

Pois bem, então o jeito ia ser descompactar tudo na marra. Pensei em escrever um programa que faria o seguinte:

try {
	unzip <file>
}
catch {
	try {
		tar xf <file>
	}
	catch {
		// ...e assim por diante
	}
}

Obviamente eu utilizaria os programas diretamente, então a coisa poderia ficar um pouco mais simples, utilizando os return codes dos mesmos para detectar se descompactaram o arquivo com sucesso. Por exemplo, tar xf <file> retorna 0 se rodou corretamente, do contrário ele retorna algo diferente de zero. Isso se mostrou válido para todos os programas de descompactação que utilizei, exceto o lha, que insistia em retornar 0 de qualquer jeito, mesmo quando falhava.

Para automatizar essa tarefa, resolvi utilizar python2. C/C++ provavelmente também seriam bons candidatos, mas eu queria praticar o meu python.

Após algumas inspeções, notei que cada arquivo continha um e somente um arquivo dentro dele, então a ideia base seria:

  • mantenha uma lista com todos os arquivos conhecidos até então (no começo, só haveria um);
  • descompacte esse arquivo;
  • detecte qual arquivo acabou de ser descompactado
  • continue fazendo isso até encontrar a flag

Meu código ficou assim:

#!/usr/bin/env python

import os
import subprocess

TARGET_DIR = 'mat'

def uncompress_kgb(file):
    return subprocess.call(["kgb", file])

def uncompress_gzip(file):
    return subprocess.call(["gunzip", "-S", '.' + file.split('.')[-1], file])

def uncompress_tar(file):
    return subprocess.call(["tar", "xvf", file])

def uncompress_rar(file):
    return subprocess.call(["unrar", "x", file])

def uncompress_lha(file):
    return subprocess.call(["lha", "e", file])

def uncompress_zip(file):
    return subprocess.call(["unzip", file])

def uncompress_arj(file):
    subprocess.call(["cp", file, file + ".arj"])
    err = subprocess.call(["arj", "x", file])
    subprocess.call(["rm", file + ".arj"])
    return err

def uncompress_7z(file):
    subprocess.call(["7z", "x", file])

def colorprint(s):
    print '\033[93m' + repr(s) + '\033[0m'

os.chdir(TARGET_DIR)

base = set()

while True:
    newbase = set(os.listdir('.'))

    diff = newbase - base
    colorprint(diff)

    if len(diff) > 1:
        raise Exception("len(diff) > 1")
    elif len(diff) == 0:
        print "len(diff) == 0"
        break

    for file in diff:
        err = uncompress_kgb(file)
        if err != 0:
            err = uncompress_gzip(file)
            if err != 0:
                err = uncompress_tar(file)
                if err != 0:
                    err = uncompress_rar(file)
                    if err != 0:
                        err = uncompress_zip(file)
                        if err != 0:
                            err = uncompress_arj(file)
                            if err != 0:
                                err = uncompress_7z(file)
                                if err != 0:
                                    err = uncompress_lha(file)
                                    if err != 0:
                                        print "lha fail"

    base = newbase

Essa ideia funcionou bastante bem. A única coisa overkill foi que eu não deletei arquivos anteriores; isso poderia ter simplificado significativamente o problema (e os sets no python).

Ademais, uma das coisas chatas do arj é que ele só é capaz de extrair arquivos que terminam em *.arj, então fui obrigado a renomear/copiar um arquivo antes de tentar utilizá-lo para extrair seu conteúdo.