Moje odkazy
Obsah článku:
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).
Ř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.
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
).
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.
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).
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()
a 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
/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
.
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.
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.
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.
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 :)
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é.