Jak se stát hackerem

Z PostgreSQL
Verze z 31. 7. 2008, 20:56, kterou vytvořil 193.165.69.2 (diskuse) (→‎Správa paměti: vynechane pismeno)
(rozdíl) ← Starší verze | zobrazit aktuální verzi (rozdíl) | Novější verze → (rozdíl)
Skočit na navigaci Skočit na vyhledávání

aneb stručný úvod do hackingu PostgreSQL
9.2.2008 Autor: Pavel Stěhule

Hacking má dost podobného s tuningem. Na začátku máte starého varťáska a po několika týdnech a měsících svařování, broušení a lakování se můžete projet a předvést v žihadle v ničím nepodobnému původnímu objektu. Totéž platí i o hackingu. Na rozdíl od normálního programování nezačínáte na zelené louce, K dispozici máte tisíce řádek zdrojového kódu, které jen čekají na šikovné ruce. Pár drobných úprav a aplikace získají novou funkčnost. V následujícím článku je popsáno vytvoření obfuskátoru kódu PL/pgSQL. Tento článek se netváří seriózně. Hacking je hlavně zábava, která může být dalším lidem užitečná. Hacking není nutnost - je to stejné jako s auty. Před 40 lety každý dobrý řidič si dokázal opravit auto. Dnes většina řidičů dokáže nastartovat. Ale možná čím dál tím více mechaniků doma pečuje o veterána. Jinak důkladněji o pojmu hacker - http://www.security-portal.cz/clanky/kdo-je-to-hacker.html.

To je funkce, kterou uživatelé poměrně často žádají a které v PostgreSQL chybí. Důvody jsou v podstatě dva. První, neexistuje způsob jak napsat neprůstřelný obfuskátor (a proč psát něco, co reálně žádnou bezpečnost nepřináší). Pátral jsem jak je na tom konkurence. Na MSDN se můžete dočíst, že příznak ENCRYPTION procedur neochrání kód před uživateli kteří mohou použít DAC nebo debugger. Srovnatelné zabezpečení jsem již schopný zajistit i v PostgreSQL a skrýt kód před kýmkoliv, kdo nemá oprávnění superuser. Druhým důvodem pro chybějící podporu skrytí kódu je embargo na šifrovací technologie. Jelikož má PostgreSQL otevřený kód, tak nelze použít některou slabou metodu šifrování. A na ty kvalitní, dosud platí jistá omezení. Ne, že by mi to bylo jasné - PostgreSQL obsahuje modul pgcrypto, který tyto technologie obsahuje (nadstavba nad OpenSSL a PGP) a který je šířen s PostgreSQL bez omezení. Mimochodem tento modul použijeme. Stejně jako každý chirurg musí vědět, kam říznout, tak by i hacker měl mít představu, kde umístí svůj kód.

Strategie

Určitě budu modifikovat SQL parser. Chci rozšířit příkaz CREATE FUNCTION o další atribut OBFUSCATE a příkaz ALTER FUNCTION o novou akci - OBFUSCATE. V prvém případě se uloží zašifrovaný zdrojový kód, v druhém případě se vezme otevřený kód, zašifruje se a uloží. Další otázkou je, kam uložit zašifrovaný kód. V PostgreSQL se veškeré informace o procedurách uchovávají v tabulce pg_proc. Zdrojový kód procedury se uloží do sloupce prosrc.Nejlepší řešení by bylo přidat další příznak do tabulky, podle kterého by se poznalo, jestli je procedura zašifrovaná. Každá změna systémových tabulek je dost pracná, má poměrně velké dopady na kód (může způsobit nefunkčnost špatně napsaného kódu) a poměrně obtížně se obhajuje (každý patch by měl být co nejméně invazivní, tj. změny by měly být lokální, kompaktní). Tabulka pg_proc obsahuje také sloupec probin, se kterým se počítalo pro kompilované jazyky a který je v případě PL/pgSQL nevyužit. Vždy obsahuje jeden znak a to pomlčku. Jelikož se jedná o bytea položku, tak je navíc ještě vhodnější pro uložené zašifrovaného textu, který je také typu bytea. Dále musíme někam bezpečně uložit heslo. Tady přichází na pomoc PostgreSQL se systémovou proměnnou typu GUC_SUPERUSER_ONLY. Přístup k obsahu této proměnné má pouze superuser. Konečně budu muset mírně modifikovat interpret PL/pgSQL, a to tak, aby dokázal rozšifrovat zdrojový text uložené procedury.

Nástroje

Ještě než se pustím do vlastního hackingu, popíši nástroje, které se používají: gcc a make jsou základem (hacking je perfektní způsob, jak se perfektně naučit určitý programovací jazyk - pro PostgreSQL je to ANSI C). gdb a post mortem analýza (viz níže), ddd pro interaktivní ladění, editor, příkazy cporig a difforig. Poslední jmenované příkazy umožní snadné vytvoření patche - což je cílem každého hackingu. cporig - vytvoří kopii zadaného souboru s příponou orig, difforig prohledá zadaný adresář a všechny podadresáře, vyhledá soubory s příponou orig a na pro ně vytvoří diff soubor.

[pavel@okbob-bb test]$ echo '#include' > ukazka.c
[pavel@okbob-bb test]$ cporig ukazka.c
[pavel@okbob-bb test]$ ls ukazka.*
ukazka.c  ukazka.c.orig
[pavel@okbob-bb test]$  echo ' "stdio.h"' >> ukazka.c
[pavel@okbob-bb test]$ difforig >> patch.diff
./ukazka.c
[pavel@okbob-bb test]$ cat patch.diff 
*** ./ukazka.c.orig     2008-01-29 10:40:09.000000000 +0100
--- ./ukazka.c  2008-01-29 10:40:20.000000000 +0100
***************
*** 1 ****
--- 1,2 ----
  #include
+  "stdio.h"

Ladění hackované aplikace připomíná crash testy. Hodně práce ušetří backtrace výpis založený na analýze core souboru. Pro to je potřeba: a) přeložit PostgreSQL tak, aby obsahoval ladící informace (a určitě se vyplatí aktivovat aserts), b) povolit generování core souborů. Trochu odbočím - pokud chcete nahlásit chybu libovolné aplikace, tak zasláním backtrace výpisu neskutečně pomůžete s jejím odstraněním.

cd src/pgsql
./configure --enable-debug --enable-cassert
make all
su -
make install

v /etc/profile nahradte

ulimit -S -c 0 > /dev/null 2>&1
ulimit -c unlimited

a doporučuji ještě v /etc/sysctl.conf nastavit cestu k adresáři, kam chcete, aby systém ukládal core soubory.

kernel.core_pattern = /tmp/core

Pro názornost jeden pád PostgreSQL předvedu - použitím makra Assert. Pokud se neorientuji ve zdrojovém kódu, hledám, podobně jako ve fulltextu, určitá klíčová slova. Našel jsem funkci, která se jmenuje ProcedureCreate - což je jedno z vhodných míst, kam uložit kód, který bude šifrovat zdrojový kód procedury.

/* ---------------------------------------------------------------- 
 *              ProcedureCreate 
 * 
 * Note: allParameterTypes, parameterModes, parameterNames, and proconfig 
 * are either arrays of the proper types or NULL.  We declare them Datum, 
 * not "ArrayType *", to avoid importing array.h into pg_proc.h. 
 * ---------------------------------------------------------------- 
 */
Oid
ProcedureCreate(const char *procedureName,
                                Oid procNamespace,
                                bool replace,
                                bool returnsSet,

....
        int                     i;

        /* 
         * sanity checks 
         */
        Assert(PointerIsValid(prosrc));
        Assert(PointerIsValid(probin));

        Assert(false);  // NEPROJDE //

        parameterCount = parameterTypes->dim1;

--------------------------------------------
make all
make install
/etc/init.d/pgsql restart
--------------------------------------------
[pavel@okbob-bb ~]$ psql postgres
Welcome to psql 8.3RC2, the PostgreSQL interactive terminal.

postgres=# create or replace function fx() returns int as $$begin return -1; end; $$ language plpgsql obfuscate;
server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.
The connection to the server was lost. Attempting reset: Succeeded.
postgres=# 
-------------------------------------------
[pavel@okbob-bb catalog]$ su
Heslo: 
[root@okbob-bb catalog]# gdb /usr/local/pgsql/bin/postgres /tmp/core.27127 
GNU gdb Red Hat Linux (6.6-40.fc8rh)
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
.....
(gdb) bt
#0  0x0012d402 in __kernel_vsyscall ()
#1  0x001b7690 in raise () from /lib/libc.so.6
#2  0x001b8f91 in abort () from /lib/libc.so.6
#3  0x082d12de in ExceptionalCondition (conditionName=0x8317dcb "!(((bool) 0))", errorType=0x8301dc2 "FailedAssertion", fileName=0x8317d7c "pg_proc.c", lineNumber=109)
    at assert.c:57
#4  0x081003ec in ProcedureCreate (procedureName=0x8704f58 "fx", procNamespace=2200, replace=1 '\001', returnsSet=0 '\0', returnType=23, languageObjectId=16421, 
    languageValidator=16420, prosrc=0x870505c "begin return -1; end; ", probin=0x83b04c6 "-", isAgg=0 '\0', security_definer=0 '\0', isStrict=0 '\0', 
    volatility=118 'v', parameterTypes=0x872ef5c, allParameterTypes=0, parameterModes=0, parameterNames=0, proconfig=0, procost=100, prorows=0, obfuscate=1 '\001')
    at pg_proc.c:109
#5  0x08149b9f in CreateFunction (stmt=0x87051a0) at functioncmds.c:801
#6  0x0822cc5d in PortalRunUtility (portal=0x872ad34, utilityStmt=0x87051a0, isTopLevel=1 '\001', dest=0x87051fc, completionTag=0xbffb4d0a "") at pquery.c:1173
#7  0x0822db4c in PortalRunMulti (portal=0x872ad34, isTopLevel=-9 '�', dest=0x87051fc, altdest=0x87051fc, completionTag=0xbffb4d0a "") at pquery.c:1268
#8  0x0822e3f4 in PortalRun (portal=0x872ad34, count=2147483647, isTopLevel=1 '\001', dest=0x87051fc, altdest=0x87051fc, completionTag=0xbffb4d0a "") at pquery.c:813
#9  0x082294b3 in exec_simple_query (query_string=0x87046b4 "create or replace function fx() returns int as $$begin return -1; end; $$ language plpgsql obfuscate;")
    at postgres.c:963
#10 0x0822af71 in PostgresMain (argc=4, argv=<value optimized out>, username=0x86ac56c "pavel") at postgres.c:3535
#11 0x081f6d32 in ServerLoop () at postmaster.c:3207
#12 0x081f7a36 in PostmasterMain (argc=3, argv=0x86a9530) at postmaster.c:1029
#13 0x081a9850 in main (argc=3, argv=Cannot access memory at address 0x69fb
) at main.c:188
(gdb) list pg_proc.c:109
104              * sanity checks
105              */
106             Assert(PointerIsValid(prosrc));
107             Assert(PointerIsValid(probin));
108
109             Assert(false);
110
111             parameterCount = parameterTypes->dim1;
112             if (parameterCount < 0 || parameterCount > FUNC_MAX_ARGS)
113                     ereport(ERROR,

Všimněte si, že v gdb jednoduše určím důvod a místo pádu, a ještě k tomu mám parametry volných procedur procedur.

Parser

Parser zajišťuje rozpoznání SQL příkazu - je založen na použití generátorů parserů bison a yacc. Musím definovat nový lexikální symbol OBFUSCATE (změna v souboru keywords.c) a příslušným způsobem upravit soubor s gramatikou. Po překladu a restartu serveru mohu příznak OBFUSCATE použít aniž by PostgreSQL hlásil syntaktickou chybu. Klíčová slova v souboru keywords.c (ta nemusí být klíčovými slovy v SQL - jde o to, která slova rozpozná parser) musí být abecedně uspořádána (lexikální analýza vyhledává známá slova půlením intervalu, a pokud slovo není na správné pozici, tak nebude nalezeno):

Upravím gramatiku - zavedu nový token OBFUSCATE:

%token <keyword> ... OBFUSCATE OBJECT_ OFF ..

který použiji v definici příkazu CREATE FUNCTION (konkrétně v pravidle common_func_opt_item:):

/*
 * Options common to both CREATE FUNCTION and ALTER FUNCTION
 */
common_func_opt_item:
                        CALLED ON NULL_P INPUT_P
                                {
                                        $$ = makeDefElem("strict", (Node *)makeInteger(FALSE));
                                }
                        | RETURNS NULL_P ON NULL_P INPUT_P
                                {
                                        $$ = makeDefElem("strict", (Node *)makeInteger(TRUE));
                                }       

                        | SetResetClause
                                {
                                        /* we abuse the normal content of a DefElem here */
                                        $$ = makeDefElem("set", (Node *)$1);
                                }
                        | OBFUSCATE
                                {
                                        $$ = makeDefElem("obfuscate", (Node *)makeInteger(TRUE));
                                }

Atributy funkce jsou uložené v seznamu pojmenovaných hodnot. Při hackingu nemusím na začátku rozumět hackovanému kódu. Nový kód vytvářím analogicky s existujícím. V tomto případě se mohu přidržet operací s atributem STRICT - aniž bych přesně věděl, jak pracuje funkce makeDefElem, použil jsem tuto funkci (pouze s jiným parametrem) - rozšířím pravidlo o novou možnost a odpovídající akci přeberu z existující jiné a upravím.

postgres=# create or replace function fx() 
                       returns int as $$
                       begin return -1; end; 
                       $$ language plpgsql obfuscate;
ERROR:  option "obfuscate" not recognized

Nyní se již nejedná o syntaktickou chybu. Pokud nastavíme "ukecanější" režim v psql:

postgres=# \set VERBOSITY verbose
postgres=# create or replace function fx() returns int as $$begin return -1; end; $$ language plpgsql obfuscate;
ERROR:  XX000: option "obfuscate" not recognized
LOCATION:  compute_attributes_sql_style, functioncmds.c:470
postgres=# 

Získáme tak podstatně více informací (hlavně kde došlo k výjimce). K této chybě dojít muselo. Změnil jsem pouze parser (dalším krokům zpracování SQL příkazu jsem se zatím nevěnoval).

Správa paměti

PostgreSQL má svůj vlastní systém datových typů a navíc zavádí další způsob volání funkcí. Důvody jsou v zásadě tři: a) jazyk C nenabízí introspekci, b) vývojáři PostgreSQL sice používají neobjektové ANSI C, nicméně píši objektově (ne nepodobné je GTK+ pozn: pokud by PostgreSQL vznikla o něco později, je možné, že by převzala objektový model GTK+), c) správa paměti - aby se minimalizovaly úniky paměti, PostgreSQL alokuje paměť prostřednictvím hierarchicky členěných segmentů (používá se termín kontext). V okamžiku, kdy se dealokuje paměť, tak dochází k uvolnění celého segmentu a zároveň všech podřízených segmentů. Pokud některá funkce běží déle, může sama explicitně dealokovat paměť, kterou si vyžádala (která může být opakovaně použita), nicméně to není nutné. Veškerá alokovaná paměť funkce se automaticky uvolní po jejím dokončení (pokud je funkce spuštěna executorem) - což je jednodušší a rychlejší než např. Garbage collector. Organizace segmentů je zhruba následující: Session->transakce->volání funkce executorem.

Pokud budu v dlouhém cyklu volat SQL funkci, která vrací jako výsledek text nebo Datum, a pouze 10% použiji, pak se již vyplatí řešit explicitní uvolnění paměti. Za uvolnění segmentu zodpovídá vždy volající funkce. Bez uvolnění by zbytečně dlouho byla alokována paměť, což by znamenalo vyšší nároky na paměť (pokud funkce neobsahuje cyklus nebo si nealokuje neobvykle velké bloky paměti, tak explicitní uvolnění paměti není nutné).

#include "utils/memutils.h"

MemoryContext oldcxt;
MemoryContext workcxt;

workcxt = AllocSetContextCreate(CurrentMemoryContext, "MyFuncContext",
					ALLOCSET_DEFAULT_MINSIZE,
					ALLOCSET_DEFAULT_INITSIZE,
					ALLOCSET_DEFAULT_MAXSIZE);
oldcxt = MemoryCotextSwitchTo(workcxt);

for (i = 0; i<1000; i++)
{
	text *str;

	str = DatumGetTextP(
			DirectFunctionCall1(anyfce, CStringGetDatum(parametr)));

	/* zajímá mne pouze každý desátý řetězec */
	if (i % 10 == 0)
	{
		int len;
		text *dest;
		/* 
		 * před uvolněním podřízeného kontextu musím zkopírovat 
		 * výsledek do kontextu, který zůstává.
		 */ 
		len = VARSIZE_ANY_EXHDR(str);
		
		dest = (text *) MemoryContextAlloc(oldcxt, len);
		memcpy(dest, VARDATA_ANY(str), len);
		....
	}
	
	/* explicitně uvolním celý kontext (bez jeho odstranění) */
	MemoryContextReset(workcxt);
}

MemoryContextSwitchTo(oldcxt);
MemoryContextDelete(workcxt);

Toto řešení správy paměti se osvědčilo, úniků paměti je řádově méně a navíc zpříjemňuje psaní jednodušších funkcí, protože se nemusíme starat o uvolnění alokované paměti. Návrhu datových typů v PostgreSQL se věnoval seriál, který vyšel před několika lety na rootu (http://www.root.cz/clanky/datove-typy/) a který je v aktualizované podobě uložen zde - Návrh vlastních datových typů- proto toto téma rychle přejdu. Velice stručně: PostgreSQL se rozeznávají dvě třídy typů - typy s pevnou délkou do 8bytů a typy s pevnou délkou nad 8Bytů a typy s variabilní délkou. Druhá třída se souhrnně označuje jako varlena typy. Obě tyto třídy zastřešuje 8Byt typ Datum (nemá žádnou souvislost s kalendářem), který buďto obsahuje data (pokud si vystačí s 8Byty), nebo obsahuje ukazatel na data typu varlena. Toto členění má jen pramálo společného s tím, jak jsou data uložena fyzicky na disku - je ale dostatečné pro pochopení operací s daty v operační paměti.

V1 volající konvence

Vlastní způsob volání se používá pro SQL funkce - to jsou ty funkce, které můžeme používat v prostředí SQL. Tento způsob se označuje jako V1 volající konvence a spočívá v odlišném způsobu předávání parametrů. Hlavní přínos této konvence je možnost předávat hodnotou základní číselné typy a přitom podporovat NULL hodnotu jak v parametrech, tak ve výsledku. V1 funkce má pouze jediný parametr a to typu FunctionCallInfoData. Tato datová struktura obsahuje pole argumentů arg typu Datum a pole logických hodnot argnull. Položka nargs obsahuje počet parametrů. Pokud je funkce spuštěna z SQL executoru, tak jsou vyplněny i další položky - např. flinfo, která zpřístupňuje další informace o argumentech funkce (lze dohledat typ parametru). Hodnoty parametrů jsou uložené v poli, tudíž velice snadno lze iterovat přes všechny parametry. Tato iterace se vyskytuje např. při spouštění PL/pgSQL funkce, kdy se hodnoty parametrů kopírují do příslušných proměnných.

/*
 * This struct is the data actually passed to an fmgr-called function.
 */
typedef struct FunctionCallInfoData
{
        FmgrInfo   *flinfo;                     /* ptr to lookup info used for this call */
        fmNodePtr       context;                /* pass info about context of call */
        fmNodePtr       resultinfo;             /* pass or return extra info about result */
        bool            isnull;                 /* function must set true if result is NULL */
        short           nargs;                  /* # arguments actually passed */
        Datum           arg[FUNC_MAX_ARGS];             /* Arguments passed to function */
        bool            argnull[FUNC_MAX_ARGS]; /* T if arg[i] is actually NULL */
} FunctionCallInfoData;

Proč zmiňuji V1 volající konvenci? Pro šifrování a dešifrování chci použít funkce z modulu pgcrypto, který obsahuje právě SQL funkce. SQL funkci lze spustit jednak prostřednictvím executoru jako SQL příkaz, nebo pomocí přímého volání. K tomu slouží funkce DirectFunctionCallX (kde X je číslice určující počet paramerů). Přímé volání je několikanásobně efektivnější. Na druhou stranu parametr nebo výsledek nesmí být NULL (nejsem si tak úplně jistý z jakého důvodu - tipoval bych, že podpora NULL by jednak zkomplikovala API, a pak by si hlavně vynutila přísnější kontrolu parametrů u STRICT funkcí, které bezelstně předpokládají, že žádný parametr nebude NULL.

Datum
DirectFunctionCall2(PGFunction func, Datum arg1, Datum arg2)
{
        FunctionCallInfoData fcinfo;
        Datum           result;

        InitFunctionCallInfoData(fcinfo, NULL, 2, NULL, NULL);

        fcinfo.arg[0] = arg1;
        fcinfo.arg[1] = arg2;
        fcinfo.argnull[0] = false;
        fcinfo.argnull[1] = false;
 
        result = (*func) (&fcinfo);

        /* Check for null result, since caller is clearly not expecting one */
        if (fcinfo.isnull)
                elog(ERROR, "function %p returned NULL", (void *) func);

        return result;
}

Opět SQL funkce jsou v zásadě dvojího druhu: vestavěné a zákaznické. Ty vestavěné se dají volat přímo pomocí ukazatele na funkci. Pro zákaznické žádný ukazatel není k dispozici a volají se prostřednictvím unikátního identifikačního čísla - toto číslo je každé funkci přiřazeno při registraci SQL funkce, a je jej nutné dynamicky dohledat (známe název funkce, počet a typ parametrů).

Často používanou funkcí je funkce textin, která se používá pro převod z CString typu (C nulou ukončený řetězec) na typ text (varlena typ obsahující text - v hlavičce je uložena délka - podobně byl text ukládán v TurboPascalu). Můžete narazit na fragment:

text * mujtext;
mujtext = DatumGetTextP(DirectFunctionCall1(textin, CStringGetDatum("Ahoj")));

případně reverzní operace:

char *str;
str = DatumGetCString(DirectFunctionCall1(textout, TextPGetDatum(mujtext)));
elog(NOTICE, "%s svete", str);

Přímé volání SQL funkce vrací hodnotu typu Datum a přebírá hodnoty typu Datum - intenzivně používám připravená makra pro konverze mezi typem Datum a nějakým konkrétním typem. Tato makra dodržují konvenci: Výchozí_typGETCílový_typ. Pokud volám SQL funkci z C musím použít její binární jméno (nikoliv SQL název) - pozn. SQL funkce mohou být přetížené, tj. pod jedním názvem existuje více funkcí lišících se v počtu a typech parametrů. Ukázkou může být funkce to_char. V ANSI C funkce nepřetěžujeme, tudíž musíme mít více funkcí - nebo polymorfní funkce, které se dokáží adaptovat na konkrétní typy zadaných parametrů:

postgres=# \df to_char
                              List of functions
   Schema   |  Name   | Result data type |        Argument data types        
------------+---------+------------------+-----------------------------------
 pg_catalog | to_char | text             | bigint, text
 pg_catalog | to_char | text             | double precision, text
 pg_catalog | to_char | text             | integer, text
 pg_catalog | to_char | text             | interval, text
 pg_catalog | to_char | text             | numeric, text
 pg_catalog | to_char | text             | real, text
 pg_catalog | to_char | text             | timestamp without time zone, text
 pg_catalog | to_char | text             | timestamp with time zone, text
(8 rows)

Zkrácený výpis binární název neposkytne (je potřeba použít rozšířený výpis):

postgres=# \x
Expanded display is on.
postgres=# \df+ encode
List of functions
-[ RECORD 1 ]-------+-----------------------------------------------------
Schema              | pg_catalog
Name                | encode
Result data type    | text
Argument data types | bytea, text
Volatility          | immutable
Owner               | postgres
Language            | internal
Source code         | binary_encode
Description         | convert bytea value into some ascii-only text string

postgres=# \df+ decode
List of functions
-[ RECORD 1 ]-------+---------------------------------------------------
Schema              | pg_catalog
Name                | decode
Result data type    | bytea
Argument data types | text, text
Volatility          | immutable
Owner               | postgres
Language            | internal
Source code         | binary_decode
Description         | convert ascii-encoded text string into bytea value

Pokud voláme SQL funkci prostřednictvím SQL executoru, tak je zajištěno, že funkce dostane parametry ve správném formátu. Pokud použijeme přímé volání, musíme si být jisti, že funkce obdrží parametry ve formátu, který očekává (popřípadě musíme zajistit explicitní přetypování - pozn. většinou voláním in a out funkcí pro ten určitý datový typ). V podstatě kód procedur Obfuscate a Deobfuscate jsou hlavně tyto konverze.

Zatemnění (obfuscace) kódu

Když už popisuji interface pro volání SQL funkcí, rovnou popíši kód procedur. Budu se věnovat pouze proceduře Obfuscate - Deobfuscate je její symetrické dvojče. Proceduru umístím do souboru backend/catalog/pg_proc.c - tam je nejblíž funkci ProcedureCreate.

extern char *obfuscator_password;  /* odkaz na GUC proměnnou s heslem */

Datum
Obfuscate(const char *prosrc)
{
        Datum encrypted_src;

        FuncDetailCode fdresult;
	/* Funkce encrypt má tři parametry: bytea, bytea, text */
        Oid encrypt_argtypes[] = {BYTEAOID, BYTEAOID, TEXTOID};
        bool retset;
        Oid *true_oid_array;
        Oid     fnOid = InvalidOid;
        Oid     rettype;

	/* 
	 * Hledám Oid funkce encrypt, a testuji dostupnost funkce.
	 * Nelze použít ukazatel, jelikož funkce je linkována v separátní knihovně, 
	 * a registrována dodatečně a zaváděna dynamicky - nikoliv staticky
	 */
        fdresult = func_get_detail(list_make1(makeString("encrypt")),
                                                        NIL, 3, encrypt_argtypes,
                                                        &fnOid, &rettype, &retset,
                                                        &true_oid_array);

	/* Kontroluji pouze zda-li jsem našel nějakou funkci daného jména. pro
	 * kterou mohu přetypovat parametry - správně bych ještě měl přidat kontrolu,
	 * zda-li true_oid_array obsahuje stejné hodnoty jako encrypt_argtypes.
	 */
        if (fdresult != FUNCDETAIL_NORMAL || !OidIsValid(fnOid))
                ereport(ERROR,
                            (errcode(ERRCODE_UNDEFINED_FUNCTION),
                             errmsg("function %s does not exist",
                                            func_signature_string(list_make1(
						makeString("encrypt")), 3, encrypt_argtypes)),
                             errhint("Install pgcrypto first.")));

	
	/* Spouštím funkci identifikovanou Oid */
        encrypted_src = OidFunctionCall3(fnOid,
                                    DirectFunctionCall2(binary_decode,
                                        DirectFunctionCall1(textin, CStringGetDatum(prosrc)),
                                        DirectFunctionCall1(textin, CStringGetDatum("escape"))),
                                    DirectFunctionCall2(binary_decode,
                                        DirectFunctionCall1(textin, 
						CStringGetDatum(obfuscator_password)),
                                        DirectFunctionCall1(textin, CStringGetDatum("escape"))),
                                    DirectFunctionCall1(textin, CStringGetDatum("bf")));

        /* probin is bytea datatype */

        return  encrypted_src;
}

Procedura by odpovídala kódu v SQL (který ovšem reálně implementovat - SQL a PL/pgSQL nepodporuje typ cstring):

CREATE OR REPLACE FUNCTION Obfuscate(cstring)
RETURNS bytea AS $$
SELECT encrypt(decode($1, 'escape'),
               decode(current_setting('obfuscator_password'), 'escape'),
	       'bf');
$$ LANGUAGE SQL SECURITY DEFINER STABLE;   

Uložení hesla - konfigurační proměnné

Nyní již narážím na problém s uložením hesla. Konfigurační záležitosti se v PostgreSQL řeší pomocí tzv. GUC (Grand Unified Configuration) proměnných. Systém se postará o načtení proměnných z konfiguračních souborů a jejich případnou aktualizaci z uživatelského rozhraní. K GUC proměnné můžeme přistupovat prostřednictvím funkcí (kontroluje se uživatelský přístup) nebo prostřednictvím externí proměnné (což je způsob, který použiji - chci aby přístup z UI byl omezen pouze na superusera, ale obfuscaci chci umožnit i neprivilegovaným uživatelům). Přidám řádek do pole ConfigureNamesString v souboru backend/utils/misc/guc.c:

char *obfuscator_password;  /* adresa proměnné */

static struct config_string ConfigureNamesString[] = 
{
	...

        {
                {"obfuscator_password", PGC_POSTMASTER, FILE_LOCATIONS,
                        gettext_noop("Sets password for obfuscator procedure."),
                        NULL,
                        GUC_SUPERUSER_ONLY | GUC_NOT_IN_SAMPLE
                },
                &obfuscator_password,
                NULL, NULL, NULL
        },

Systém zajistí existenci řetězce - ukazatel obfuscator_password se odkazuje vždy na validní adresu.

Odkrytí zatemněného kódu

V okamžiku, kdy zatemníme (zašifrujeme kód) bez protikusu v interpretu PL/pgSQL, tak kód nedokážeme ani uložit. Před uložením se kontroluje syntaxe skriptu, a kontrola na šifrovaném kódu nemá šanci. Dešifrovací kód umístím na začátek procedury do_compile. Tato procedura se volá vždy, když je potřeba přeložit kód (který je potom do ukončení session nebo do změny kódu procedury umístěn v cache). K dispozicí má vektor atributů z pg_proc.

static PLpgSQL_function *
do_compile(FunctionCallInfo fcinfo,
                   HeapTuple procTup,
                   PLpgSQL_function *function,
                   PLpgSQL_func_hashkey *hashkey,
                   bool forValidator)
{
        Form_pg_proc procStruct = (Form_pg_proc) GETSTRUCT(procTup);
        int                     functype = CALLED_AS_TRIGGER(fcinfo) ? T_TRIGGER : T_FUNCTION;
       
	....
        prosrcdatum = SysCacheGetAttr(PROCOID, procTup,
                                                       Anum_pg_proc_prosrc, &isnull);
        if (isnull)
                elog(ERROR, "null prosrc");
	/*
	 * Získej zdrojový kód a převeď jej na cstring. Pokud začíná pomlčkou, pak
	 * se nemůže jednat o validní plpgsql kód, tj. signál, že validní kód je zašifrovaný
	 * v sloupci probin.
	 */
        proc_source = DatumGetCString(DirectFunctionCall1(textout, prosrcdatum));
        if (strncmp(proc_source, "-", 1) == 0)
        {
                prosrcdatum = SysCacheGetAttr(PROCOID, procTup,
                                                          Anum_pg_proc_probin, &isnull);

                /* deobfuscate source code if it is necessary */
                proc_source = Deobfuscate(prosrcdatum);
        }

Uložení zatemněného kódu do tabulky pg_proc

Nyní je chvíle, kdy mohu upravit uložení kódu procedury. V souboru backend/catalog/pg_proc.c je procedura ProcedureCreate, ve které se sestavuje n-tice záznamu v tabulce pg_proc. Této proceduře přidám bool parametr obfuscate a upravím kód:

        if (!obfuscate) 
        {
                values[Anum_pg_proc_prosrc - 1] = DirectFunctionCall1(textin,CStringGetDatum(prosrc));
                values[Anum_pg_proc_probin - 1] = DirectFunctionCall1(textin,CStringGetDatum(probin));
        }
        else
        {
                values[Anum_pg_proc_prosrc - 1] = DirectFunctionCall1(textin,CStringGetDatum("-"));
                values[Anum_pg_proc_probin - 1] = Obfuscate(prosrc);
        }

Vyhodnocení atributů funkce

Nyní jde jen o to, abych tuto funkci volal se správným parametrem obfuscate. Vyhledám všechna volání funkce a jako výchozí hodnotu pro tento parametr nastavím false. Nyní musím spojit parsování flagu OBFUSCATE s procedurou ProcedureCreate. Lokalizovat místo pro změnu mi pomůže výše zmíněné chybové hlášení:

LOCATION:  compute_attributes_sql_style, functioncmds.c:470

V proceduře compute_attributes_sql_style se v cyklu prochází seznam flagů funkce a přiřazuje k proměnným. Je třeba rozšířit hlavičku funkce compute_common_attribute a přidat test na nový flag:

static bool
compute_common_attribute(DefElem *defel,
                                                 DefElem **volatility_item,
                                                 DefElem **strict_item,
                                                 DefElem **security_item,
                                                 List **set_items,
                                                 DefElem **cost_item,
                                                 DefElem **rows_item,
                                                 DefElem **obfuscate_code)
{
...
       else if (strcmp(defel->defname, "obfuscate") == 0)
        {       
                if (*obfuscate_code)
                        goto duplicate_error;

                *obfuscate_code = defel;
        }
        else
                return false;

V proceduře compute_attributes_sql_style jen krátkou podmínku:

     if (obfuscate_code_item)
                *obfuscate_code  = intVal(obfuscate_code_item->arg);

ALTER FUNCTION, změna záznamu v tabulce pg_proc

Poslední zbývající konstrukcí je ALTER FUNCTION. Podporována je zatím pouze varianta zatemnění kódu. Pozměním kód procedury AlterFunction. Update záznamu spočívá v naplnění změnových tabulek a posloupnosti volání funkcí heap_modifytuple a simple_heap_update (Za povšimnutí stojí skutečnost, že důležité systémové tabulky mají i identifikátory každého sloupce, např. Anum_pg_proc_prosrc a že známe i počet sloupců viz Natts_pg_proc (toto je interní způsob změny záznamu systémové tabulky:

       if (obfuscate_item) 
       { 
               bool    isnull; 
               char    *proc_src; 
               Datum           prosrc; 
               Datum           repl_val[Natts_pg_proc];  // obsahuje hodnoty, tj Datum
               char            repl_null[Natts_pg_proc]; // mezera nebo 'n'
               char            repl_repl[Natts_pg_proc]; // mezera nebo 'r'
  
               prosrc =  SysCacheGetAttr(PROCOID, tup, Anum_pg_proc_prosrc, &isnull); 
  
               if (isnull) 
                       elog(ERROR, "null prosrc"); 
  
               proc_src = DatumGetCString(DirectFunctionCall1(textout, prosrc)); 
               if (strncmp(proc_src, "-", 1) != 0) 
               { 
                       memset(repl_repl, ' ', sizeof(repl_repl)); 
                       memset(repl_null, ' ', sizeof(repl_null));                       
  
                       repl_repl[Anum_pg_proc_prosrc - 1] = 'r'; 
                       repl_repl[Anum_pg_proc_probin - 1] = 'r'; 
  
                       repl_val[Anum_pg_proc_prosrc - 1] = DirectFunctionCall1(textin, CStringGetDatum("-")); 
                       repl_val[Anum_pg_proc_probin - 1] = Obfuscate(proc_src); 
  
                       tup = heap_modifytuple(tup, RelationGetDescr(rel), 
                                                          repl_val, repl_null, repl_repl); 
               } 
       } 
   
        /* Do the update */ 
        simple_heap_update(rel, &tup->t_self, tup); 

Vytvoření patche

Cílem je hackingu je získat patch. Ten vygeneruje výše zmíněný příkaz difforig. Je dobré znát postup aplikace záplaty. Patch zkopíruji do hlavního adresáře zdrojových kódů Pg. Pak použiji příkaz patch (v níže uvedeném příkladu je použit patch na optimalizaci čtení bitmap. indexů).

[pavel@localhost pgsql]$ patch -p 0 < bitmap-preread-v8.diff 
patching file src/backend/access/nbtree/nbtsearch.c
patching file src/backend/commands/explain.c
patching file src/backend/executor/nodeBitmapHeapscan.c
patching file src/backend/executor/nodeIndexscan.c
patching file src/backend/nodes/tidbitmap.c
patching file src/backend/storage/buffer/bufmgr.c
patching file src/backend/storage/file/fd.c
patching file src/backend/storage/smgr/md.c
patching file src/backend/storage/smgr/smgr.c
patching file src/backend/utils/misc/guc.c
Hunk #2 succeeded at 1767 (offset 2 lines).
patching file src/include/nodes/execnodes.h
patching file src/include/nodes/tidbitmap.h
patching file src/include/storage/bufmgr.h
patching file src/include/storage/fd.h
patching file src/include/storage/smgr.h

Po aplikaci patche je určitě dobré vymazat přeložené moduly: make clean (pokud došlo k modifikaci hlavičkových souborů, tak je to nutnost).

Ověření funkčnosti

postgres=# show obfuscator_password;
 obfuscator_password
-----------------------
 moje supertajne heslo
(1 row)

postgres=# \x
Expanded display is on.
postgres=# create or replace function fx() returns int as $$begin
return -1; end; $$ language plpgsql;
CREATE FUNCTION
postgres=# \df+ fx
List of functions
-[ RECORD 1 ]-------+-----------------------
Schema              | public
Name                | fx
Result data type    | integer
Argument data types |
Volatility          | volatile
Owner               | bob
Language            | plpgsql
Source code         | begin return -1; end;
Description         |

postgres=# ALTER FUNCTION fx() obfuscate;
NOTICE:  begin return -1; end;
ALTER FUNCTION
postgres=# \df+ fx
List of functions
-[ RECORD 1 ]-------+---------
Schema              | public
Name                | fx
Result data type    | integer
Argument data types |
Volatility          | volatile
Owner               | bob
Language            | plpgsql
Source code         | -
Description         |

postgres=# select fx();
-[ RECORD 1 ]
fx | -1

postgres=# create or replace function fx() returns int as $$begin
return -1; end; $$ language plpgsql obfuscate;
CREATE FUNCTION
postgres=# select fx();
-[ RECORD 1 ]
fx | -1

postgres=# \df+ fx
List of functions
-[ RECORD 1 ]-------+---------
Schema              | public
Name                | fx
Result data type    | integer
Argument data types |
Volatility          | volatile
Owner               | bob
Language            | plpgsql
Source code         | -
Description         |

postgres=# select * from pg_proc where proname = 'fx';
-[ RECORD 1 ]--+----------------------------------------------------------------------------
proname        | fx
pronamespace   | 2200
proowner       | 16385
prolang        | 16421
procost        | 100
prorows        | 0
proisagg       | f
prosecdef      | f
proisstrict    | f
proretset      | f
provolatile    | v
pronargs       | 0
prorettype     | 23
proargtypes    |
proallargtypes |
proargmodes    |
proargnames    |
prosrc         | -
probin         |
\231\003_\266\361\214}\231\240L/\020\232\036c\234\315P\236\266I\370\324\222
proconfig      |
proacl         |

[pavel@okbob-bb ~]$ psql -U bob postgres
Welcome to psql 8.3RC2, the PostgreSQL interactive terminal.

Type:  \copyright for distribution terms
      \h for help with SQL commands
      \? for help with psql commands
      \g or terminate with semicolon to execute query
      \q to quit

postgres=> \x
Expanded display is on.
postgres=> show obfuscator_password;
ERROR:  must be superuser to examine "obfuscator_password"
postgres=> select fx();
-[ RECORD 1 ]
fx | -1

postgres=> \df+ fx
List of functions
-[ RECORD 1 ]-------+---------
Schema              | public
Name                | fx
Result data type    | integer
Argument data types |
Volatility          | volatile
Owner               | bob
Language            | plpgsql
Source code         | -
Description         |

postgres=> select * from pg_proc where proname = 'fx';
-[ RECORD 1 ]--+----------------------------------------------------------------------------
proname        | fx
pronamespace   | 2200
proowner       | 16385
prolang        | 16421
procost        | 100
prorows        | 0
proisagg       | f
prosecdef      | f
proisstrict    | f
proretset      | f
provolatile    | v
pronargs       | 0
prorettype     | 23
proargtypes    |
proallargtypes |
proargmodes    |
proargnames    |
prosrc         | -
probin         |
\231\003_\266\361\214}\231\240L/\020\232\036c\234\315P\236\266I\370\324\222
proconfig      |
proacl         |

Proces schválení a aplikace patche do jádra

Pokud chcete svojí práci publikovat, a svoji práci protlačit do hlavního stromu, nezačínejte zasláním patche. To se chápe jako jistá mladická nerozvážnost, případně neslušnost. Prvním krokem je zaslání návrhu (proposal) do konference pg_hackers. Pokud Váš návrh bude odmítnut nebo projde bez povšimnutí, tak nemáte moc šancí uspět. Chce to trochu bojovat, a regulérně obhajovat návrh a akceptovat připomínky. Nejjednodušší cesta je vyrobit externí modul, který umístíte např. na http://pgfoundry.org. Ten je zcela pod Vaší kontrolou a zároveň viditelný a dostupný ostatním uživatelům (některé funkce nicméně vyžadují modifikaci základního kódu - tam to chce pevné nervy, obrnit se trpělivostí a komunikovat). Pokud patch řeší funkci uvedenou v ToDo, tak máte podstatně lepší pozici, než když přicházíte s vlastní novou funkcí. Každá nová funkce dostupná z UI musí být popsána v dokumentaci. Patch bez dokumentace nebude aplikován. Totéž platí o regresních testech. Je nutné respektovat BSD C styl zápisu (odsazování). Pokud máte odsouhlasený návrh, lze posílat i neúplné patche - je však nutné je označit (např. tagem WIP (work-in-progress). Jinak se místo konstruktivních připomínek dočkáte pouze povrchního odsouzení za lajdáckou práci. Nečekejte zázraky - je běžné, že od prvního zaslání patche do jeho aplikace uplyne několik měsíců - dost často se patch s připomínkami, které je třeba akceptovat. A opět je nutné komunikovat a hledat řešení. Zamítnutí patche není životní tragédií. Nestane se tak bezdůvodně. Pak to chce brát z rezervou - tak jako tak, hacking je jednou z mála způsobů jak se naučit perfektně ovládat PostgreSQL.