FK~

Moje odkazy

Ostatní odkazy

Close Windows
Nenajdete mě na Facebooku ani Twitteru
Rozpad EU
Jsem členem FSF
There Is No Cloud …just other people's computers.
Sane software manifesto / Manifest příčetného softwaru

Přepisování parametrů příkazové řádky

vydáno: 25. 11. 2017 20:26, aktualizováno: 16. 12. 2017 22:07

Obecně se nedoporučuje předávat citlivá data (např. hesla) programům jako parametry na příkazové řádce. Důvodem je jednak to, že se spuštěné příkazy ukládají do historie, a jednak to, že by se k parametrům mohli dostat jiní uživatelé resp. procesy na témže počítači. Bezpečnější je proto citlivé údaje předávat buď ve formě souborů (kterým lze nastavit práva) nebo přes standardní vstup (rouru).

htop – výpis procesu s heslem jako parametrem

Řada programů možnost zadat heslo na příkazové řádce zrušila nebo nikdy nenabízela – podporují pouze interaktivní režim nebo předání souborem či rourou. Některé programy zadání hesla parametrem stále umožňují a některé v sobě mají jakousi ochranu, která zabrání tomu, aby takto zadané heslo šlo snadno zjistit z výpisu procesů. V následujícím článku se podíváme, jak si takový program skrývající hesla můžeme napsat sami.

Výpis parametrů procesu

Po spuštění programu vznikne proces a jeho parametry si můžeme vypsat klasicky pomocí příkazu ps. Následujícím příkazem si vyfiltrujeme řádek s procesem, který nás zajímá (místo ./přepis-cli-parametrů si dosaďte libovolný název programu, který zrovna běží):

ps aux | grep '[ ]./přepis-cli-parametrů'

Trik s regulárním výrazem '[ ]…' slouží k tomu, aby grep odfiltroval svůj vlastní proces.

Parametry se nám vypíší na jednom řádku hned za názvem programu a jsou oddělené mezerami. Pokud by samotné parametry obsahovaly mezery, nepoznáme, kde jsou hranice mezi parametry. To většinou moc nevadí, ale pokud by nás to zajímalo a chtěli bychom přesný výsledek, můžeme se podívat do syntetického souborového systému /proc, kde ve virtuálním souboru cmdline v adresáři příslušného procesu máme parametry příkazu, ovšem tentokrát oddělené nikoli mezerou, ale nulovým bajtem \0. Tento soubor tak můžeme předat rourou příkazu xargs, který umí číst parametry oddělené mj. nulovým bajtem a předávat je dalšímu příkazu (v tomto případě obyčejné echo, které jednotlivé parametry vypíše):

cat /proc/$(pidof ./přepis-cli-parametrů)/cmdline | xargs -0 -n1 echo

n.b. když si soubor cmdline vypíšeme jen příkazem cat, tak to bude vypadat, jako by parametry vůbec oddělené nebyly – nulové bajty se nezobrazí. Vidět budou např. v hexadecimálním tvaru – nasměrujeme obsah rourou do hd (zkratka pro hexdump -C).

Přepisování parametrů v C/C++

V jazyce C nebo C++ můžeme parametry z příkazové řádky (svého vlastního procesu) přepsat jednoduše tak, že přepíšeme danou proměnnou, kterou nám systém předal jako parametr funkce main().

for (int i = 1; i < argc; i++) {
	char * arg = argv[i];
	while (*arg) *arg++= 'x';
}

Tento kód nahradí všechny znaky ve všech parametrech znakem x. V reálném programu by to bylo složitější – chtěli bychom přepsat jen parametr s heslem, tzn. např. prvek následující po --password, zatímco hodnoty jiných parametrů bychom chtěli zachovat, aby byly ve výpisu procesů normálně vidět.

Celý kód ukázky:

/**
 * Přepis CLI parametrů
 * Copyright © 2017 František Kučera (frantovo.cz)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <iostream>
#include <wchar.h>
#include <locale.h>


using namespace std;

void cekej() {
	wprintf(L"Stiskněte Enter pro pokračování…");
	getwchar();
}

void napoveda(const string nazevProgramu) {
  wprintf(L"Vypište si parametry jedním z následujících příkazů:\n");
  wprintf(L"\tcat /proc/$(pidof %s)/cmdline | xargs -0 -n1 echo\n", nazevProgramu.c_str());
  wprintf(L"\tps aux | grep '[ ]%s'\n", nazevProgramu.c_str());
}

/**
 * 1) Program vypíše zadané parametry (argumenty příkazového řádku) a zastaví se (čeká na potvrzení uživatelem).
 * 2) Uživatel si vypíše proces a jeho parametry v druhém terminálu.
 * 3) Program přepíše svoje parametry – např. 'heslo' → 'xxxxx'.
 * 4) Uživatel si opět vypíše informace o procesu a vidí, že původní informace (v praxi např. hesla) jsou pryč.
 * 5) Program se ukončí.
 */
int main(int argc, char* argv[]) {

	setlocale(LC_ALL,"");

	string nazevProgramu = argv[0];

	if (argc == 1) { // 1 = jen název programu, ale žádné parametry
		wprintf(L"Není, co přepisovat – příště prosím zadejte nějaké parametry – např.\n");
		wprintf(L"\t%s aaa bbb ccc\n", nazevProgramu.c_str());
		return 1;
	}

	wprintf(L"Přehled zadaných parametrů:\n");
	for (int i = 1; i < argc; i++) {
		wprintf(L"\t%d = %s\n", i, argv[i]);
	}

	napoveda(nazevProgramu);
	cekej();

	// Nahradíme všechny znaky ve všech parametrech, aby nebylo možné přečíst původní hodnoty (např. hesla).
	// Pracujeme zde na úrovni bajtů, takže pokud parametr obsahoval vícebajtové znaky, bude mít po nahrazení více znaků (ale stejně bajtů).
	// Stále bude možné zjistit délku původních parametrů (v bajtech).
	for (int i = 1; i < argc; i++) {
		char * arg = argv[i];
		while (*arg) *arg++= 'x';
	}
	// Pokud bychom začínali od nuly (i = 0), přepsali bychom i samotný název programu!
	// Možná už jste ve výpisu procesů někdy viděli názvy, které neodpovídají žádné binárce.
	// Program se tak může maskovat – nicméně i tak bude symbolický odkaz v /proc/…/exe ukazovat na jeho binárku.

	wprintf(L"Parametry byly přepsány!\n");
	napoveda(nazevProgramu);
	cekej();
}

Program se zastaví a poskytne uživateli instrukce k vypsání parametrů – nejdříve před přepisem parametrů a pak po něm. Reálný program by se nezastavoval, takže by okamžik mezi spuštěním programu a přepsáním byl velice krátký (ovšem ne nulový).

Makefile pro kompilaci a spuštění:

CXX=g++
CXXFLAGS += -std=c++17 -Wall

APP=přepis-cli-parametrů
PROGNAME=$(APP)
SRC=$(APP).cpp

ALL: $(PROGNAME)

$(PROGNAME): $(SRC)
        $(CXX) $(CXXFLAGS) -o $(PROGNAME) $(SRC)

clean:
        rm -f $(PROGNAME)

run: $(PROGNAME)
        ./$(PROGNAME) první druhý třetí

Aktuální verzi programu najdete na hg.frantovo.cz.

Další experimenty

Při přepisování (výše uvedeným způsobem) se nemění celková délka parametrů – před přepisem:

$ cat /proc/$(pidof ./přepis-cli-parametrů)/cmdline | hd
00000000  2e 2f 70 c5 99 65 70 69  73 2d 63 6c 69 2d 70 61  |./p..epis-cli-pa|
00000010  72 61 6d 65 74 72 c5 af  00 70 72 76 6e c3 ad 00  |rametr...prvn...|
00000020  64 72 75 68 c3 bd 00 74  c5 99 65 74 c3 ad 00     |druh...t..et...|
0000002f

Po přepisu:

$ cat /proc/$(pidof ./přepis-cli-parametrů)/cmdline | hd
00000000  2e 2f 70 c5 99 65 70 69  73 2d 63 6c 69 2d 70 61  |./p..epis-cli-pa|
00000010  72 61 6d 65 74 72 c5 af  00 78 78 78 78 78 78 00  |rametr...xxxxxx.|
00000020  78 78 78 78 78 78 00 78  78 78 78 78 78 78 00     |xxxxxx.xxxxxxx.|
0000002f

Můžeme ale změnit počet parametrů – textové řetězce jsou v ukončené nulovým bajtem, a ve svém programu tak můžeme přemazat hranice mezi parametry a vytvořit nové tím, že na příslušná místa v paměti vložíme \0.

Z pohledu programu pracujeme s polem ukazatelů na textové řetězce, ovšem Jádro pracuje se souvislým úsekem paměti – zná jen adresu začátku a konce; hranice mezi jednotlivými parametry moc neřeší. Pokud tedy v programu upravíme pole, aby obsahovalo ukazatele směřující do jiných koutů paměti (na nové textové řetězce), Jádro si toho nebude všímat a bude stále číst tu původní paměť.

Pokud si s tím chcete hrát víc, přidejte si do Makefile řádek CXXFLAGS += -g a podívejte se na program v debuggeru (GDB + třeba Netbeans jako GUI).

ladění přes GDB a Netbeans

Jádro (fs/proc) čte parametry procesu přímo z jeho paměti. A hned za parametry následují proměnné prostředí. Když si do kódu přidáme něco jako:

char * arg = argv[0];
for (int i = 0; i < 300; i++) {
	*arg++ = '_';
}

tak program vesele překročí hranici parametrů a začne přepisovat proměnné prostředí. Když přemažeme nulový bajt za posledním parametrem, tak do cmdline (a výstupu příkazu ps) začnou „prosakovat“ proměnné prostředí:

$ cat /proc/20770/cmdline | hd
00000000  5f 5f 5f 5f 5f 5f 5f 5f  5f 5f 5f 5f 5f 5f 5f 5f  |________________|
*
00000070  5f 5f 5f 58 44 47 5f 56  54 4e 52 3d 37           |___XDG_VTNR=7|
0000007d

A už nám nefunguje $(pidof ./přepis-cli-parametrů), protože název procesu jsme taky přepsali (int i = 0). Musíme si tedy poznamenat PID předem nebo ho dodatečně dohledat pomocí:

pstree -p | less

Také se nám změnila délka cmdline:

$ cat /proc/21459/cmdline | hd
00000000  2e 2f 70 c5 99 65 70 69  73 2d 63 6c 69 2d 70 61  |./p..epis-cli-pa|
00000010  72 61 6d 65 74 72 c5 af  00 70 72 76 6e c3 ad 00  |rametr...prvn...|
00000020  64 72 75 68 c3 bd 00 74  c5 99 65 74 c3 ad 00     |druh...t..et...|
0000002f

takže jsme to trochu rozbili:

$ cat /proc/21459/cmdline | hd
00000000  5f 5f 5f 5f 5f 5f 5f 5f  5f 5f 5f 5f 5f 5f 5f 5f  |________________|
*
000000a0  5f 5f 5f 5f 5f 5f 5f 5f  5f 5f 5f 5f 43 5f 46 49  |____________C_FI|
000000b0  4c 45 53 3d 2f 65 74 63  2f 67 74 6b 2f 67 74 6b  |LES=/etc/gtk/gtk|
000000c0  72 63 3a 2f 68 6f 6d 65  2f 66 69 6b 69 2f 2e 67  |rc:/home/hack/.g|
000000d0  74 6b 72 63 3a 2f 68 6f  6d 65 2f 66 69 6b 69 2f  |tkrc:/home/hack/|
000000e0  2e 63 6f 6e 66 69 67 2f  67 74 6b 72 63           |.config/gtkrc|
000000ed

Více se dozvíme ve zdrojových kódech Jádra: fs/proc/base.c, funkce proc_pid_cmdline_read()get_task_mm().

tsk = get_proc_task(file_inode(file));
mm = get_task_mm(tsk);
arg_start = mm->arg_start;
arg_end = mm->arg_end;
env_start = mm->env_start;
env_end = mm->env_end;

Mimochodem, když přemažeme nulový bajt na konci proměnných prostředí, žádná data do /proc/$PID/environ „prosakovat“ nebudou – dle mého pozorování se Jádro zastaví na env_end a další data ven nepustí, zatímco v případě parametrů příkazové řádky se na arg_end nezastaví a dojde v „vyzrazení“ proměnných prostředí. V GDB jsem viděl, že za koncem proměnných prostředí následuje cesta k binárce našeho programu a pak několik nulových bajtů (vycpávka, zarovnání) a za tím už je paměť, kterou daný proces nemůže číst.

Pokusy jsem dělal v Ubuntu 16.04.3 LTS s touto verzí Jádra:

$ uname -srmo
Linux 4.4.0-98-generic x86_64 GNU/Linux

Skrytí parametrů na úrovni souborového systému /proc

Fakt, že si libovolný uživatel může vypsat všechny procesy běžící na daném systému, je pouze výchozí stav. Tradičně to tak bylo a dodnes je to obvyklá praxe. V systému GNU/Linux můžeme toto výchozí chování změnit a nastavit systém tak, aby uživatel viděl jen svoje procesy.

Stačí si znovu připojit souborový systém /proc s volbou hidepid:

mount -o remount,hidepid=1 /proc/

Případně můžeme nastavit skupinu, jejíž členové budou mít k výpisu procesů ostatních uživatelů i nadále přístup (jinak by cizí procesy viděl jen root):

mount -o remount,hidepid=1,gid=spravci /proc/

Také můžeme nastavit hidepid=2 a pak neprivilegovaní uživatelé v /proc neuvidí ani názvy složek cizích procesů (v případě hidepid=1 tyto složky vidí, ale nemají právo do nich vstoupit a číst jejich soubory jako cmdline). Více viz manuálová stránka man procfs.

Aby nastavení vydrželo i po restartu počítače, uložíme si příslušné volby do souboru /etc/fstab.

Mimochodem, pokud by vás napadlo, že si to nejdřív vyzkoušíte někde bokem a připojíte si procfs podruhé do jiného adresáře, třeba /mnt/proc, tak vám to nebude fungovat – procfs se totiž chová jako singleton a sice ho můžete připojit do více adresářů současně, ale nové parametry se aplikují i na původní /proc.

Závěr

Možnost přepsat parametry svého vlastního procesu je zajímavá technika, o které jsem dlouho nevěděl. V některých případech může zvýšit bezpečnost a uchránit citlivá data, nicméně stále tu určité riziko je – útočník by mohl např. přetížit systém a tím prodloužit dobu mezi startem programu a okamžikem, kdy stihne promazat svoje parametry… nebo se jednoduše při větším počtu opakování trefit a heslo si ve vhodnou chvíli přečíst. Stále je tedy dobré dodržovat zásadu, že hesla nezadáváme jako parametr na příkazové řádce a předáváme je programu jinak (soubor se správně nastavenými právy, standardní vstup…). Z pohledu správce systému stojí za zvážení použití hidepid > 0. Ale podle mého to na většině systémů není nutné a měly by být bezpečné, i když je seznam procesů viditelný všem. Co by stálo za zlepšení: možnost dohledat původní parametry, se kterými byl proces spuštěn – toto právo by měl mít root a vlastník procesu. Totéž platí o názvu programu, což je první prvek pole parametrů (viz komentář v kódu). Pokud někdo víte, jak zjistit původní parametry, nebo jestli se v jádře na takové možnosti pracuje, dejte prosím vědět v komentářích.

Přílohy:

  • mysql.cc – Reálný příklad použití této techniky – MariaDB/MySQL (řádek 1853) (153 766 bajtů) [1269]

Odkazy a zdroje:

Témata: [GNU/Linux] [C++] [C]

Komentáře čtenářů


MiSi, 28. 11. 2017 00:33, hesla předávejte přes environment [odpovědět]

Citlivé údaje můžeme předat i pomocí proměnné prostředí. Např.:

PASSWORD=mypass ./mujprogram

Je to pohodlné a bezpečné vzhledem k ostatním uživatelům.


Roman, 7. 12. 2017 00:37 [odpovědět]

To jsem se zrovna chystal napsat. Jen u toho je treba davat pozor na dalsi procesy spustene pod timto prostredim, protoze dokud se neudela unset, budou ty dalsi procesy moct taky moznost cist to heslo. Jinak je predani hesla env parametrem v pohode.


Franta, 2. 12. 2017 21:11 [odpovědět]

Přidal jsem podkapitolu „Další experimenty“.


RoBe, 29. 4. 2019 23:01 [odpovědět]

Pokud nechceme aby byl prikaz videt v historii staci ho spustit s jednou mezerou pred zadavanim prikazu. Pak se v bash historii neobjevi. Samozrejme pokud tuto funkci nemame potlacenou :)


Franta, 19. 7. 2020 12:24, Hesla v historii vs. ve výpisu procesů [odpovědět]

Ano, viz BASH: historie příkazů a citlivé údaje. Ale tím zabráníme pouze tomu, aby se heslo zapsalo do historie Bashe. Stále tu však zůstane problém s tím, že heslo bude vidět ve výpisu procesů, kde si ho (ve výchozím nastavení) můžou přečíst i ostatní uživatelé.

Přidat komentář

reagujete na jiný komentář (zrušit)
jméno nebo přezdívka
název příspěvku
webová stránka, blog
e-mailová adresa
nápověda: možnosti formátování
ochrana proti spamu a špatným trollům

Náhled komentáře