Содержание статьи
Для этого нужно понять, как Linux ограничивает размеры envp и argc, а также при каких условиях execve( возвращает E2BIG. Обладая этими знаниями, мы добьемся стабильного переполнения и ограничим максимальный размер вводимых команд в Bash с помощью переменных окружения.
E2BIG и ARG_MAX
При изучении книги по Linux API я нашел интересную ошибку для функции execve( — E2BIG. Она возникает, если список аргументов оказался слишком длинным. Интересно. А что значит «слишком длинный»? Как вообще узнать этот лимит?
Максимальный размер переменных
Для определения максимального размера есть константа ARG_MAX. Эти значения задают максимальный размер argc , передающийся в системные вызовы exec*. Для просмотра их значений в системе можно использовать
getconf -a | grep ARG_MAX
Посмотрю у себя:
root@c340bf6c8c3e:/rev# getconf -a | grep ARG_MAX
ARG_MAX 2097152
_POSIX_ARG_MAX 2097152
Отлично. Я получил стартовую точку.
Как передаются аргументы
Прежде чем двигаться дальше, уточним, что именно скрывается за argc, argv и envp:
-
argc— количество аргументов командной строки. -
argv— сами аргументы, то есть массив строк. Например, при запускеlsв-l / tmp argvпопадут строкиls,-lи/.tmp -
envp— массив переменных окружения в виде строкNAME=value.
На практике, когда говорят про размер argc, почти всегда имеют в виду суммарный объем аргументов из argv.
Переполнение argc
ARG_MAX ограничивает размеры argc . Для простоты эксперимента я буду использовать argc.
info
Лимит ARG_MAX относится не только к строкам аргументов и переменных окружения. При запуске процесса ядро также размещает служебные данные: массивы указателей, выравнивание и вспомогательную информацию. Поэтому в практических экспериментах доступный размер полезных данных может быть меньше, чем просто argv .
Руками такое делать не очень удобно (хотя я бы глянул, как кто‑нибудь занимается этим). Мы для этого используем Python. Понадобится следующее:
- запуск стороннего процесса с моими
argcиenvp; - получение информации об ошибке.
Обертка вокруг execve
Обычного execve недостаточно, так как он заменяет память текущего процесса, значит, дальнейший код не будет выполнен. В качестве хелпера я написал себе функцию try_exec, которая запускает процесс с переданными bin, argc и envp:
def try_exec( bin: str, argv: list = None, envp: dict[str, str] = None) -> tuple[bool, int | None]: if argv is None: argv = [] if envp is None: envp = {} rfd, wfd = os.pipe() pid = os.fork() if pid == 0: try: os.close(rfd) os.execve(bin, argv, envp) os._exit(0) except OSError as e: try: os.write(wfd, str(e.errno).encode('ascii')) finally: os._exit(111) except Exception: try: os.write(wfd, b'-8163') finally: os._exit(112) os.close(wfd) data = os.read(rfd, 64) os.close(rfd) _, status = os.waitpid(pid, 0) if data: err = int(data.decode('ascii', errors='replace')) return (False, err) return (True, None)Логика работы этой функции следующая:
- Создается
pipeдля общения между родительским и дочерним процессами. - Вызывается
forkдля создания дочернего процесса. - В дочернем процессе я закрываю
rfd, который используется для чтения, а затем вызываюexecveдля нужного бинарного файла. - С помощью
try/я ловлю ошибку дочернего процесса, если он завершился некорректно, и записываю ее код вexcept wfd. - В это же время родительский процесс пытается прочитать код ошибки дочернего процесса из
rfd, а затем ожидает завершения его работы. - Далее возвращается кортеж с двумя значениями: запустился дочерний процесс или нет и код его ошибки.
info
Системный вызов execve( запускает новую программу, заменяя образ текущего процесса. Вместе с этим в ядро передаются аргументы командной строки (argv) и переменные окружения (envp). Их суммарный размер ограничен: если данных слишком много, запуск прерывается еще на этапе execve(. Поэтому в Linux есть лимит на общий объем argv и envp.
Получаем ошибку E2BIG
Следующий логический этап — получение ошибки E2BIG. В качестве испытуемого я буду использовать бинарный файл /. Попробую запустить его с аргументом длины ARG_MAX:
def get_e2big_crash() -> bool: val_len = os.sysconf(os.sysconf_names["SC_ARG_MAX"]) ok, err = try_exec('/bin/true', ['true', 'A' * val_len]) if ok: return False if err != errno.E2BIG: raise RuntimeError('Failed to find val_len') return TrueЛогика работы простая:
- Получаем
ARG_MAX. - Запускаем процесс
/с аргументомbin/ true 'A'.* ARG_MAX - Проверяем код ошибки.
Запущу с strace , чтобы увидеть код ошибки, который вернет сам системный вызов.
info
Утилита strace показывает системные вызовы программы.
Параметр -f нужен для отслеживания дочерних процессов, а -e используется для вывода информации об указанных системных вызовах.
Запускаем код:
root@24c59f638617:/rev# strace -e trace=execve,execveat -f ./e2big_crash.py
execve("./e2big_crash.py", ["./e2big_crash.py"], 0x7ffcc8a434f8 /* 10 vars */) = 0
execve("/usr/local/sbin/python3", ["python3", "./e2big_crash.py"], 0x7ffc5d4998e8 /* 10 vars */) = -1 ENOENT (No such file or directory)execve("/usr/local/bin/python3", ["python3", "./e2big_crash.py"], 0x7ffc5d4998e8 /* 10 vars */) = -1 ENOENT (No such file or directory)execve("/usr/sbin/python3", ["python3", "./e2big_crash.py"], 0x7ffc5d4998e8 /* 10 vars */) = -1 ENOENT (No such file or directory)execve("/usr/bin/python3", ["python3", "./e2big_crash.py"], 0x7ffc5d4998e8 /* 10 vars */) = 0
strace: Process 30 attached
[pid 30] execve("/bin/true", ["true", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"...], 0x75f471364230 /* 0 vars */) = -1 E2BIG (Argument list too long)[pid 30] <span class=nobr>+ exited with 111 </span>+
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=30, si_uid=0, si_status=111, si_utime=0, si_stime=5 /* 0.05 s */} ---Success
<span class=nobr>+ exited with 0 </span>+
Вот в этой строчке можно увидеть нужную ошибку:
[pid 30] execve("/bin/true", ["true", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"...], 0x75f471364230 /* 0 vars */) = -1 E2BIG (Argument list too long)Отлично. Переходим к следующему шагу.
Подтверждаем значение ARG_MAX
Ошибку мы получили. Но передача одного большого аргумента мне ни о чем не говорит. Теперь я хочу найти длину аргумента, при которой процесс будет завершаться с ошибкой E2BIG.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
