- předchozí článek - následující článek - obsah -

Linuxové noviny Březen 1998

S-bit - životu nebezpečno

Pavel Kaňkovský, 3. března 1998

Mnoho systémových programů v UNIXovém operačním systému - Linux nevyjímaje - potřebuje pro svůj běh zvláštní privilegia. Příkladem budiž program passwd, který musí být schopen zapisovat do souboru /etc/passwd resp. /etc/shadow, aniž by k tomu byl oprávněn sám uživatel (z pochopitelných důvodů). Nejčastěji používaným mechanismem, který toto umožňuje, je tzv. propůjčení identifikace.

U programu, jehož spustitelný soubor má nastavený aspoň jeden z s-bitů, například takto:

  -r-sr-xr-x  1 root   bin     15796 /usr/bin/passwd
dojde po jeho spuštění systémovým voláním execve() k tomu, že efektivní vlastník, efektivní skupina, případně oboje - podle toho, zda je v přístupových právech nastaven setuid bit (04000), setgid bit (02000), nebo oba dva - jsou nastaveny na hodnoty určené uživatelem resp. skupinou vlastnící soubor. (Množina doplňkových skupin (supplemental groups) je vždy ponechána beze změny.) Program pak pracuje se stejnými oprávněními, jako kdyby byl spuštěn svým vlastníkem resp. uživatelem v dané skupině.

Proč je to nebezpečné?

Výše popsaný mechanismus není prost rizik. Musí být zajištěno, aby zvláštní privilegia přidělená programu nemohla být zneužita. Část odpovědnosti leží na samotném jádře systému, které musí mj. zabránit jeho trasování nebo čtení jeho paměťového prostoru (kde se mohou nacházet důvěrná data) ze strany skutečného vlastníka, ovšem převážná část spočívá na bedrech samotného programu (a také jím používaných knihovnách).

Privilegovaný proces musí pečlivě kontrolovat prostředí, ve kterém je spuštěn, a vstupy, které jsou mu uživatelem předkládány. Naneštěstí mnoho programů (či spíše jejich autorů) toto podceňuje a výsledkem jsou závažné bezpečnostní problémy, které jsou ještě umocněny tím, že velká část takových programů pracuje rovnou s nejvyššími možnými privilegii - s právy superuživatele.

K nejčastějším chybám, které mají za následek bezpečnostní problémy (a které budou detailněji diskutovány níže) patří:

  • přetečení mezí pole
  • neopatrná manipulace se soubory
  • neopatrné spouštění dalších programů

Odstrašující příklad - Xserver

Jak si asi mnozí, kteří mají na svém Linuxu nainstalovaný balík XFree86, všimli, Xserver běží s právy superuživatele. Je tomu tak, aby mu bylo dovoleno přistupovat přímo ke grafickému hardware, k jehož ovládání neposkytuje standardní linuxové jádro prakticky žádnou podporu. Ovšem Xserver je velmi komplikovaný program a vyskytují se v něm (nyní bude řeč o aktuální verzi 3.3.1 založené na X11R6.3) různé problémy, dokonce ze všech tří výše vyjmenovaných kategorií. Jedná se o následek toho, že kód Xserveru (a do jisté míry to platí o celém X11) byl většinou navrhován a implementován, aniž byl kladen dostatečný důraz na jeho bezpečnost. Svou roli zde nepochybně hraje velký rozsah a složitost kódu, stejně jako jeho značná různorodost.

Přetečení mezí pole

Toto je snad nejfrekventovanější chyba vyskytující se v céčkových programech vůbec. Poněkud překvapivě se však chyba vyskytuje mnohem častěji nepřímo - při použití některých funkcí standardní knihovny. Zvláště se jedná o funkce sprintf(), strcpy() a strcat(), které neumožňují explicitně stanovit velikost pole, do kterého je ukládán výsledek, a bezstarostně zapisují za jeho konec. (Což vede k otázce, zda je celková koncepce standardní knihovny vhodná.) Podrobněji k této problematice v článku Přetečení bufferu.

Pomocí vhodně sestrojených vstupních dat pak může zlý uživatel přepsat návratovou adresu na zásobníku nebo jiné důležité údaje a donutit program k vykonání prakticky libovolných akcí - např. ke spuštění privilegovaného shellu. Detailní popis přináší například http://www-miaif.ibp.fr/willy/security/.

Příklad (xc/programs/Xserver/os/access.c) je na výpisu Nesprávné meze polí.


  void
  ResetHosts (display)
      char *display;
  {
      register HOST  *host;
      char lhostname[120], ohostname[120];
      char *hostname = ohostname;
      char fname[100];
  ...
      strcpy (fname, "/etc/X");
      strcat (fname, display);
      strcat (fname, ".hosts");
      if (fd = fopen (fname, "r"))
  ...
  }

Výpis 3: Nesprávné meze polí

Uživatel může při spuštění Xserveru specifikovat libovolný (i nesmyslný a hlavně libovolně dlouhý) název displaye pomocí parametru uvozeného dvojtečkou. Tato hodnota je předána do funkce ResetHosts(), která ji používá pro konstrukci jména souboru. Avšak příliš dlouhá hodnota způsobí přetečení mezí pole fname.

Neopatrná manipulace se soubory

Většina programů pracuje nějakým způsobem se souborovým systémem. Pokud program neprovádí dostatečné kontroly jmen souborů nebo jiných uživatelem poskytnutých dat, z nichž jsou jména souborů konstruována, lze snadno získat neoprávněný přístup k souborům, systémovým zvláště.

Příklad (xc/programs/Xserver/os/utils.c) je na výpisu Příklad nesprávného zacházení se souborem.


  static void
  InsertFileIntoCommandLine(resargc, resargv, prefix_argc, prefix_argv, filename,\
     suffix_argc, suffix_argv)

      int *resargc;
  ...
  {
      struct stat    st;
      FILE           *f;
      char           *p;
      char           *q;
      int            insert_argc;
      char           *buf;
      int            len;
      int            i;

      f = fopen(filename, "r");
  ...
  } /* end InsertFileIntoCommandLine */

  void
  ExpandCommandLine(pargc, pargv)
      int *pargc;
      char ***pargv;
  {
      int i;
      for (i = 1; i < *pargc; i++)
      {
          if ( (0 == strcmp((*pargv)[i], "-config")) && (i < (*pargc - 1)) )
          {
              InsertFileIntoCommandLine(pargc, pargv,
                                            i, *pargv,
                                      (*pargv)[i+1], /* filename */
                                      *pargc - i - 2, *pargv + i + 2);
              i--;
          }
      }
  } /* end ExpandCommandLine */

Výpis 4: Příklad nesprávného zacházení se souborem

Funkce ExpandCommandLine() interpretuje parametr -config tak, že načte obsah specifikovaného souboru a přidá ho k parametrům zadaným na příkazové řádce. Nicméně vůbec nekontroluje, zda je daný soubor uživateli, který Xserver spustil, přístupný pro čtení. Například, pokud zlý uživatel zadá soubor /etc/shadow, dostane se mu chybového hlášení typu:

Unrecognized option: root:BFLMpsvzABCD:12345:::::
a může začít luštit superuživatelovo heslo.

Zvláštní kapitolu představují dočasné soubory vytvářené v oblastech přístupných uživateli - zvláště v adresáři /tmp, ale i v domovském adresáři uživatele, v aktuálním adresáři apod. Zlý uživatel může vytvořením (sym)linků nebo přejmenováváním souborů lehce přesvědčit naivně implementovaný program, aby pracoval s úplně jiným souborem, než zamýšlel. Bohužel tomu opět napomáhá standardní knihovna - konkrétně funkce mktemp() a tmpnam(). V případě Xserveru lze problém tohoto typu demonstrovat na práci s adresářem /tmp/.X11-unix, i když konkrétní povaha těchto rizik poněkud vybočuje ze zaměření tohoto článku.

Neopatrné spouštění dalších programů

Příležitostně potřebuje jeden program využít služeb jiného programu. Pokud první program běží se zvláštními privilegii, pak jsou často tato privilegia přenášena i na programy z něj spuštěné. To pak znamená, že se na jedné straně nabízí možnost využít (či spíše zneužít) nedostatků jiného programu, na druhé straně pak možnost přímo donutit privilegovaný proces k spuštění libovolného programu dodaného zlým uživatelem (sem lze zařadit použití dynamicky linkované knihovny podle přání uživatele). Xserver vykazuje vzácnou kombinaci obou problémů, jak uvidíme v příkladu.

Příklad (xc/programs/Xserver/xkb/ddxLoad.c) je na výpisu Neopatrné spouštění dalších programů.


  Bool
  XkbDDXCompileKeymapByNames(XkbDescPtr              xkb,
                             XkbComponentNamesPtr    names,
                             unsigned                want,
                             unsigned                need,
                             char *                  nameRtrn,
                             int                     nameRtrnLen)
  {
  ...
      if (XkbBaseDirectory!=NULL) {
  ...
          sprintf(buf,
      "%s/xkbcomp -w %d -R%s -xkm -- -em1 %s -emp %s -eml %s \"%s%s.xkm\"",
                XkbBaseDirectory,
                ((xkbDebugFlags<2)?1:((xkbDebugFlags>10)?10:xkbDebugFlags)),
                XkbBaseDirectory,
                PRE_ERROR_MSG,ERROR_PREFIX,POST_ERROR_MSG1,
                xkm_output_dir,keymap);
  ...
      }
      else {
  ...
      }
  ...
      out= popen(buf,"w");
  ...
  }

Výpis 5: Neopatrné spouštění dalších programů

Proměnná XkbBaseDirectory obsahuje buď implicitní hodnotu (/usr/lib/X11/xkb), nebo hodnotu specifikovanou pomocí parametru -xkbdir. (Z tohoto důvodu mi není zcela jasný význam testu na nulovou hodnotu, ale to ponechme stranou.) Uživatel má nyní dvě možnosti jak tento parametr zneužít: jednak může vytvořit libovolný program pod jménem xkbcomp a předat jméno adresáře, ve kterém se nachází, nebo může využít faktu, že funkce popen() způsobí spuštění shellu, a zadat hodnotu, která obsahuje znaky mající pro shell zvláštní význam (např. mezery, obrácené apostrofy aj.).

Východiska z nouze

Rozsáhlé a těžko prověřitelné programy (jako zmíněný Xserver) neměly být nikdy spouštěny se zvláštními privilegii. Principiálně vzato by těžko prověřitelné programy neměly vůbec vznikat. Často používané bezpečnostní obálky (wrappers) a podobná opatření jsou pouze nápravou následků, nikoli příčin, které spočívají v špatné konstrukci samotných programů, i když za určitých okolností mohou mít své výhody. Návod ke správné konstrukci bezpečných programů lze hledat ve dvou "základních bezpečnostních pravidlech", která zní:

  • Nikdy nikomu nevěř.
  • Co není dovoleno, je zakázáno.

První pravidlo říká, že v programu nesmí být chyby, ale navržen musí být s ohledem na to, že tam chyby budou. To například znamená, že ty části programů, které zvláštní privilegia vyžadují, je vhodné separovat do malých samostatných programů s přesně definovaným rozhraním, jejichž správnou funkci bude možno ověřit - ideálně formálním důkazem.

Druhé pravidlo pak doporučuje, aby každý program měl přidělena pouze ta privilegia, která ke své práci potřebuje a pokud možno žádná jiná. To bohužel často naráží na omezení operačního systému - zvláště v UNIXovém světě (Linux opět nevyjímaje), kde vládne silná dichotomie: vše (superuživatel) nebo nic (ostatní uživatelé). Není-li možno dostatečně jemně rozlišovat privilegia, dochází k tomu, že jsou přidělována širší oprávnění, než je zapotřebí, a otevírá se prostor pro jejich zneužití. Jediným skutečným řešením tohoto problému je upravit operační systém tak, aby programy nemusely mít větší oprávnění než je nutno. Komplexnější projekty jako LinuxPrivs http://www.kernel.org/pub/linux/libs/security/linux-privs/ (implementace privilegií podle POSIX.1e) jsou vykročení správným směrem, ale existují i částečná řešení, jako například patch http://www.phrack.com/52/P52-06, který mj. eliminuje dosti nesmyslnou (v dnešní době) potřebu superuživatelských práv pro některé síťové služby.

Poněkud paradoxní je, že popisovaná opatření (pomineme-li obecné doporučení, že v programech nemají být chyby) nejsou příliš vhodná pro náš Xserver. Ten totiž ve skutečnosti žádná speciální privilegia nepotřebuje, pouze musí být schopen ovládat grafický hardware. Stačí, aby pro to byla v jádře dostatečná podpora - jako jí poskytuje například GGI http://www.ggi-project.org/.

Závěr

Tento článek popisuje pouze malou část z rozsáhlého repertoáru možných rizik: zmiňované problémy (nebo problémy jim podobné) mohou vznikat (a bohužel také vznikají) všude, kde dochází k interakci mezi procesy pracujícími s různými oprávněními, ať už jsou to diskutované programy využívající mechanismus propůjčení identifikace, programy komunikující po síti (na serverové i na klientské straně), nebo dokonce uživatelské aplikace, jsou-li jim předloženy vstupy pocházející z potenciálně nedůvěryhodného zdroje. Proto nelze než doporučit, aby tvůrce libovolného programu zvážil při jeho návrhu a implementaci možná bezpečnostní rizika a snažil se je úplně eliminovat, nebo alespoň maximálně omezit. *


- předchozí článek - následující článek - obsah -