💾 Archived View for any-key.press › esoteric › bggp5_pyc.gmi captured on 2024-08-18 at 17:14:26. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
Сегодня на работе у меня выдался такой день, когда я больше времени жду результатов от коллег. А в голове вертится соревнование "Binary Golf Grand Prix 5", для которого я уже сделал один вариант решения:
Binary Golf Grand Prix 5 - openssl s_client
И на глаза мне попалось очень элегантное решение:
https://github.com/binarygolf/BGGP/blob/main/2024/entries/Aaron%20DeVera/Aaron%20DeVera.py.txt
Суть его в том, что создается файл с именем `curl -L binary.golf --request-target %2F5%2F5`, и содержимым:
import os;os.system(__file__[-45:])
По шагам:
Красиво же? Но есть два фатальных недостатка:
Что бы скачать байты по HTTPS python'у не нужен curl, есть встроенный модуль `urllib.request` с методом `urlopen`:
https://docs.python.org/3/library/urllib.request.html#urllib.request.urlopen
Но нам для начала нужно его импортировать, а затем уже вызвать, передав туда URL. Что бы импортировать модуль в python есть встроенная функция `__import__`:
https://docs.python.org/3/library/functions.html#import__
Однако есть одна тонкость: что бы импортировать и получить на выходе из функции под-модуль (`urllib.request`, а не просто `urllib`) нужно обязательно указать аргумент `fromlist`:
$ python3 -c "print(__import__('urllib.request'))" <module 'urllib' from '/usr/lib/python3.10/urllib/__init__.py'> $ python3 -c "print(__import__('urllib.request', fromlist=(None,)))" <module 'urllib.request' from '/usr/lib/python3.10/urllib/request.py'>
Второе препятствие заключается в том, что в URL нужно указать слеши. В чужом решении, которое вызывает curl, это элегантно обошли опцией `--request-target`. Ну а у нас вся python'овская мощь под руками. Cимвол слеша можно получить, например, вызовом `chr`:
https://docs.python.org/3/library/functions.html#chr
А полный URL можно собрать из кортежа подстрок методом `join`:
https://docs.python.org/3/library/stdtypes.html#str.join
Собрав всё вместе получаем такой "однострочик":
$ python3 -c "print(__import__('urllib.request',fromlist=(None,)).urlopen(chr(47).join(('https:','','binary.golf','5','5'))).read())" b'Another #BGGP5 download!! @binarygolf https://binary.golf\n'
Эта строка и должна быть именем файла. Так как длина этой строки 118 символов, то что бы её исполнить содержимое .py-файла должно быть примерное таким:
eval(__file__[-118:])
Это и есть решение для формата .py в 22 байта без дополнительных аргументов командной строки.
Но python умеет запускать не только текстовые .py-файлы, но и бинарные .pyc-файлы с байт-кодом. Получить такой файл просто: формируем файл eval.py с нужным нам содержимым:
$ cat eval.py eval(__file__[-118:])
Импортируем его в REPL режиме python, игнорируя ошибки:
$ python3 Python 3.10.14 (main, Apr 13 2024, 11:52:53) [Clang 16.0.6 ] on openbsd7 Type "help", "copyright", "credits" or "license" for more information. >>> import eval Traceback (most recent call last): File "<stdin>", line 1, in <module> File "eval.py", line 1, in <module> eval(__file__[-118:]) File "<string>", line 1 eval.py ^ SyntaxError: invalid syntax >>>
И вот скомпилированный файл в 162 байта готов:
$ ls -l __pycache__ total 8 -rw-r--r-- 1 user user 162 Jul 9 15:54 eval.cpython-310.pyc
Что бы посмотреть содержимое .pyc файла я нашёл небольшой скрипт show_pyc.py:
https://github.com/nedbat/coveragepy/blob/master/lab/show_pyc.py
$ python3 show_pyc.py __pycache__/eval.cpython-310.pyc magic b'6f0d0d0a' flags 0x000000 moddate b'da328d66' (Tue Jul 9 15:53:46 2024) pysize b'16000000' (22) code name '<module>' argcount 0 nlocals 0 stacksize 4 flags 0040: CO_NOFREE code 6500650164006401850219008301010064015300 1 0 LOAD_NAME 0 (eval) 2 LOAD_NAME 1 (__file__) 4 LOAD_CONST 0 (-118) 6 LOAD_CONST 1 (None) 8 BUILD_SLICE 2 10 BINARY_SUBSCR 12 CALL_FUNCTION 1 14 POP_TOP 16 LOAD_CONST 1 (None) 18 RETURN_VALUE consts 0: -118 1: None names ('eval', '__file__') varnames () freevars () cellvars () filename 'eval.py' firstlineno 1 lnotab 1:0 linetable 1400 co_lines 1:0-20
Немного поразмыслив я пришёл к выводу, что поля name, filename и linetable мне для запуска не нужны. Поэтому я написал свой небольшой скрипт-оптимизатор:
import marshal def main(pyc_file_path): with open(pyc_file_path, "rb") as opened_file: header = opened_file.read(16) code = marshal.load(opened_file) new_code = code.replace(co_name="", co_filename="", co_linetable=b"") with open(pyc_file_path, "wb") as opened_file: opened_file.write(header) marshal.dump(new_code, opened_file) if __name__ == '__main__': import sys if len(sys.argv) != 2: print("Usage: python3 opt.py <pyc_file>") sys.exit(1) main(sys.argv[1])
Таким нехитрым путём мне удалось сократить .pyc-файл до 120 байт:
$ python3 opt.py __pycache__/eval.cpython-310.pyc $ ls -l __pycache__ total 8 -rw-r--r-- 1 user user 120 Jul 9 16:36 eval.cpython-310.pyc
Переименовываем его в "print(__import__('urllib.request',fromlist=(None,)).urlopen(chr(47).join(('https:','','binary.golf','5','5'))).read())" (без кавычек) и готово еще одно решение для Binary Golf Grand Prix 5 на 120 байт. Расширение .pyc файлу не нужно, как я понял python3.10 детектирует формат по первым четырём байтам файла. Естественно, что скомпилированный одной версией python файл будет корректно запускаться только этой же версией (в моем случае это именно python3.10).
Комментарии через ActivityPub (Fediverse) можно оставить здесь: