Что­бы сло­мать запуск прог­раммы в Linux, необя­затель­но тро­гать сам бинар­ник или пра­ва дос­тупа. Мож­но поп­робовать перег­рузить дан­ные, которые переда­ются про­цес­су при стар­те, — аргу­мен­ты коман­дной стро­ки и перемен­ные окру­жения. В этой статье раз­берем­ся, как это мож­но реали­зовать.

Для это­го нуж­но понять, как Linux огра­ничи­вает раз­меры envp и argc, а так­же при каких усло­виях execve() воз­вра­щает E2BIG. Обла­дая эти­ми зна­ниями, мы добь­емся ста­биль­ного перепол­нения и огра­ничим мак­сималь­ный раз­мер вво­димых команд в Bash с помощью перемен­ных окру­жения.

 

E2BIG и ARG_MAX

При изу­чении кни­ги по Linux API я нашел инте­рес­ную ошиб­ку для фун­кции execve() — E2BIG. Она воз­ника­ет, если спи­сок аргу­мен­тов ока­зал­ся слиш­ком длин­ным. Инте­рес­но. А что зна­чит «слиш­ком длин­ный»? Как вооб­ще узнать этот лимит?

 

Максимальный размер переменных

Для опре­деле­ния мак­сималь­ного раз­мера есть кон­стан­та ARG_MAX. Эти зна­чения зада­ют мак­сималь­ный раз­мер argc + envp, переда­ющий­ся в сис­темные вызовы 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 + envp. Для прос­тоты экспе­римен­та я буду исполь­зовать argc.

info

Ли­мит ARG_MAX отно­сит­ся не толь­ко к стро­кам аргу­мен­тов и перемен­ных окру­жения. При запус­ке про­цес­са ядро так­же раз­меща­ет слу­жеб­ные дан­ные: мас­сивы ука­зате­лей, вырав­нивание и вспо­мога­тель­ную информа­цию. Поэто­му в прак­тичес­ких экспе­римен­тах дос­тупный раз­мер полез­ных дан­ных может быть мень­ше, чем прос­то argv + envp.

Ру­ками такое делать не очень удоб­но (хотя я бы гля­нул, как кто‑нибудь занима­ется этим). Мы для это­го исполь­зуем 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)

Ло­гика работы этой фун­кции сле­дующая:

  1. Соз­дает­ся pipe для обще­ния меж­ду родитель­ским и дочер­ним про­цес­сами.
  2. Вы­зыва­ется fork для соз­дания дочер­него про­цес­са.
  3. В дочер­нем про­цес­се я зак­рываю rfd, который исполь­зует­ся для чте­ния, а затем вызываю execve для нуж­ного бинар­ного фай­ла.
  4. С помощью try/except я лов­лю ошиб­ку дочер­него про­цес­са, если он завер­шился некор­рек­тно, и записы­ваю ее код в wfd.
  5. В это же вре­мя родитель­ский про­цесс пыта­ется про­читать код ошиб­ки дочер­него про­цес­са из rfd, а затем ожи­дает завер­шения его работы.
  6. Да­лее воз­вра­щает­ся кор­теж с дву­мя зна­чени­ями: запус­тился дочер­ний про­цесс или нет и код его ошиб­ки.

info

Сис­темный вызов execve() запус­кает новую прог­рамму, заменяя образ текуще­го про­цес­са. Вмес­те с этим в ядро переда­ются аргу­мен­ты коман­дной стро­ки (argv) и перемен­ные окру­жения (envp). Их сум­марный раз­мер огра­ничен: если дан­ных слиш­ком мно­го, запуск пре­рыва­ется еще на эта­пе execve(). Поэто­му в Linux есть лимит на общий объ­ем argv и envp.

 

Получаем ошибку E2BIG

Сле­дующий логичес­кий этап — получе­ние ошиб­ки E2BIG. В качес­тве испы­туемо­го я буду исполь­зовать бинар­ный файл /bin/true. Поп­робую запус­тить его с аргу­мен­том дли­ны 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

Ло­гика работы прос­тая:

  1. По­луча­ем ARG_MAX.
  2. За­пус­каем про­цесс /bin/true с аргу­мен­том 'A' * ARG_MAX.
  3. Про­веря­ем код ошиб­ки.

За­пущу с strace -e trace=execve,execveat -f, что­бы уви­деть код ошиб­ки, который вер­нет сам сис­темный вызов.

info

Ути­лита strace показы­вает сис­темные вызовы прог­раммы.

Па­раметр -f нужен для отсле­жива­ния дочер­них про­цес­сов, а -e trace=* исполь­зует­ся для вывода информа­ции об ука­зан­ных сис­темных вызовах.

За­пус­каем код:

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, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    2 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии