GNU/Linux >> Znalost Linux >  >> Linux

Jak rozebrat, upravit a poté znovu sestavit spustitelný soubor Linuxu?

Nemyslím si, že existuje nějaký spolehlivý způsob, jak to udělat. Formáty strojového kódu jsou velmi komplikované, složitější než soubory sestav. Ve skutečnosti není možné vzít zkompilovaný binární soubor (řekněme ve formátu ELF) a vytvořit zdrojový program sestavení, který se zkompiluje do stejného (nebo dostatečně podobného) binárního souboru. Abyste pochopili rozdíly, porovnejte výstup kompilace GCC přímo s assemblerem (gcc -S ) oproti výstupu objdump ve spustitelném souboru (objdump -D ).

Napadají mě dvě velké komplikace. Za prvé, samotný strojový kód neodpovídá 1:1 kódu sestavení, kvůli věcem, jako jsou posuny ukazatelů.

Zvažte například kód C pro Hello world:

int main()
{
    printf("Hello, world!\n");
    return 0;
}

Toto se zkompiluje do kódu sestavení x86:

.LC0:
    .string "hello"
    .text
<snip>
    movl    $.LC0, %eax
    movl    %eax, (%esp)
    call    printf

Kde .LCO je pojmenovaná konstanta a printf je symbol v tabulce symbolů sdílené knihovny. Porovnejte s výstupem objdump:

80483cd:       b8 b0 84 04 08          mov    $0x80484b0,%eax
80483d2:       89 04 24                mov    %eax,(%esp)
80483d5:       e8 1a ff ff ff          call   80482f4 <[email protected]>

Za prvé, konstanta .LC0 je nyní jen nějaký náhodný offset někde v paměti -- bylo by obtížné vytvořit zdrojový soubor sestavení, který by tuto konstantu obsahoval na správném místě, protože assembler a linker si mohou vybrat umístění pro tyto konstanty.

Za druhé, nejsem si tím úplně jistý (a záleží na věcech, jako je kód nezávislý na pozici), ale věřím, že odkaz na printf není ve skutečnosti zakódován na adrese ukazatele v tomto kódu, ale hlavičky ELF obsahují vyhledávací tabulka, která dynamicky nahrazuje svou adresu za běhu. Rozložený kód proto zcela neodpovídá kódu zdrojového sestavení.

Stručně řečeno, zdrojové sestavení má symboly zatímco kompilovaný strojový kód má adresy které je obtížné zvrátit.

Druhou hlavní komplikací je, že zdrojový soubor sestavení nemůže obsahovat všechny informace, které byly přítomné v původních hlavičkách souboru ELF, jako jsou knihovny, proti kterým se má dynamicky odkazovat, a další metadata, která tam původní kompilátor umístil. Bylo by těžké to rekonstruovat.

Jak jsem řekl, je možné, že speciální nástroj dokáže manipulovat se všemi těmito informacemi, ale je nepravděpodobné, že lze jednoduše vytvořit kód sestavení, který lze znovu sestavit zpět do spustitelného souboru.

Pokud máte zájem upravit jen malou část spustitelného souboru, doporučuji mnohem jemnější přístup než rekompilaci celé aplikace. Použijte objdump k získání kódu sestavení pro funkce, které vás zajímají. Převeďte jej na „syntaxi zdrojového sestavení“ ručně (a tady bych si přál, aby existoval nástroj, který by skutečně produkoval rozebrání ve stejné syntaxi jako vstup) a upravte jej, jak si přejete. Až budete hotovi, překompilujte pouze tyto funkce a použijte objdump k nalezení strojového kódu vašeho upraveného programu. Poté pomocí hex editoru ručně vložte nový strojový kód přes horní část odpovídající části původního programu, přičemž dbejte na to, aby váš nový kód měl přesně stejný počet bajtů jako starý kód (nebo by všechny offsety byly nesprávné ). Pokud je nový kód kratší, můžete jej doplnit pomocí instrukcí NOP. Pokud to trvá déle, můžete mít potíže a možná budete muset vytvořit nové funkce a místo toho je zavolat.


Dělám to pomocí hexdump a textový editor. Musíte být skutečně pohodlné se strojovým kódem a formátem souboru, který jej ukládá, a flexibilní s tím, co se počítá jako „rozebrat, upravit a poté znovu sestavit“.

Pokud vám projde provádění pouze „bodových změn“ (přepisování bajtů, ale ne přidávání ani odstraňování bajtů), bude to snadné (relativně řečeno).

Vy opravdu nechcete přemístit žádné existující instrukce, protože pak byste museli ručně upravit jakýkoli ovlivněný relativní offset v rámci strojového kódu pro skoky/větve/načtení/uložení vzhledem k počítadlu programu, obojí v pevně okamžitém hodnoty a ty vypočítané prostřednictvím registrů .

Vždy byste měli být schopni se dostat pryč bez odstranění bajtů. Přidání bajtů může být nezbytné pro složitější úpravy a je mnohem obtížnější.

Krok 0 (příprava)

Poté, co ve skutečnosti správně rozebral soubor pomocí objdump -D nebo cokoli, co obvykle používáte jako první, abyste tomu skutečně porozuměli a našli místa, která potřebujete změnit, budete muset vzít na vědomí následující věci, které vám pomohou najít správné bajty k úpravě:

  1. Adresa (odsazená od začátku souboru) bajtů, které potřebujete změnit.
  2. Nezpracovaná hodnota těchto bajtů v současné podobě (--show-raw-insn možnost objdump je zde opravdu užitečné).

Budete také muset zkontrolovat, zda hexdump -R funguje na vašem systému. Pokud ne, pak pro zbytek těchto kroků použijte xxd příkaz nebo podobný namísto hexdump ve všech níže uvedených krocích (prostudujte si dokumentaci jakéhokoli nástroje, který používáte, vysvětluji pouze hexdump prozatím v této odpovědi, protože to je ta, kterou znám).

Krok 1

Vypište nezpracovanou hexadecimální reprezentaci binárního souboru pomocí hexdump -Cv .

Krok 2

Otevřete hexdump ed a najděte bajty na adrese, kterou chcete změnit.

Rychlý rychlokurz v hexdump -Cv výstup:

  1. V levém sloupci jsou adresy bajtů (ve vztahu k začátku samotného binárního souboru, stejně jako objdump poskytuje).
  2. Sloupec úplně vpravo (obklopený | znaků) je pouze "čitelná" reprezentace bajtů - je tam zapsán znak ASCII odpovídající každému bajtu s . zastupuje všechny bajty, které se nemapují na tisknutelný znak ASCII.
  3. To důležité je mezi tím – každý bajt jako dvě hexadecimální číslice oddělené mezerami, 16 bajtů na řádek.

Pozor:Na rozdíl od objdump -D , který vám poskytne adresu každé instrukce a zobrazí nezpracovaný hex instrukce podle toho, jak je zdokumentována jako zakódovaná, hexdump -Cv vypíše každý bajt přesně v pořadí, v jakém se objeví v souboru. To může být trochu matoucí jako první na počítačích, kde jsou bajty instrukcí v opačném pořadí kvůli rozdílům v endianness, což může být také dezorientující, když očekáváte konkrétní bajt jako konkrétní adresu.

Krok 3

Upravte bajty, které je třeba změnit – zjevně musíte zjistit nezpracované kódování strojových instrukcí (nikoli mnemotechnické pomůcky sestavení) a ručně zapsat správné bajty.

Poznámka:Ne potřeba změnit lidsky čitelné zobrazení ve sloupci úplně vpravo. hexdump bude jej ignorovat, když jej „zrušíte z výpisu“.

Krok 4

"Zrušte výpis" upraveného souboru hexdump pomocí hexdump -R .

Krok 5 (kontrola zdravého rozumu)

objdump vaše nově unhexdump ed souboru a ověřte, že demontáž, kterou jste změnili, vypadá správně. diff to proti objdump originálu.

Vážně, tento krok nepřeskakujte. Při ruční úpravě strojového kódu dělám chyby častěji než ne a většinu z nich zachytím.

Příklad

Zde je příklad ze skutečného života, když jsem nedávno upravil binární soubor ARMv8 (little endian). (Já vím, otázka je označena tagem x86 , ale nemám po ruce příklad x86 a základní principy jsou stejné, jen se liší pokyny.)

V mé situaci jsem potřeboval deaktivovat konkrétní ruční kontrolu „toto bys neměl dělat“:v mém příkladu binárního kódu v objdump --show-raw-insn -d výstup řádku, na kterém jsem se staral, vypadal takto (jedna instrukce před a po pro kontext):

     f40:   aa1503e3    mov x3, x21
     f44:   97fffeeb    bl  af0 <[email protected]>
     f48:   f94013f7    ldr x23, [sp, #32]

Jak můžete vidět, náš program se "užitečně" ukončí skokem do error funkce (která ukončí program). Nepřijatelný. Takže z tohoto pokynu uděláme zákaz. Takže hledáme bajty 0x97fffeeb na adrese/offsetu souboru 0xf44 .

Zde je hexdump -Cv řádek obsahující tento posun.

00000f40  e3 03 15 aa eb fe ff 97  f7 13 40 f9 e8 02 40 39  |[email protected]@9|

Všimněte si, jak jsou příslušné bajty ve skutečnosti převráceny (kódování little endian v architektuře platí pro strojové instrukce jako pro cokoli jiného) a jak to trochu neintuitivně souvisí s tím, jaký bajt je v jakém bajtovém offsetu:

00000f40  -- -- -- -- eb fe ff 97  -- -- -- -- -- -- -- --  |[email protected]@9|
                      ^
                      This is offset f44, holding the least significant byte
                      So the *instruction as a whole* is at the expected offset,
                      just the bytes are flipped around. Of course, whether the
                      order matches or not will vary with the architecture.

Každopádně z pohledu na jiné rozebrání vím, že 0xd503201f rozebere na nop takže to vypadá jako dobrý kandidát na můj neoperativní pokyn. Upravil jsem řádek v hexdump ed soubor odpovídajícím způsobem:

00000f40  e3 03 15 aa 1f 20 03 d5  f7 13 40 f9 e8 02 40 39  |[email protected]@9|

Převedeno zpět do binárního formátu pomocí hexdump -R , rozebral nový binární soubor s objdump --show-raw-insn -d a ověřili, že změna byla správná:

     f40:   aa1503e3    mov x3, x21
     f44:   d503201f    nop
     f48:   f94013f7    ldr x23, [sp, #32]

Potom jsem spustil binární soubor a získal chování, které jsem chtěl - příslušná kontrola již nezpůsobila přerušení programu.

Úprava strojového kódu úspěšná.

!!! Upozornění !!!

Nebo jsem byl úspěšný? Všimli jste si toho, co mi v tomto příkladu uniklo?

Jsem si jistý, že ano - protože se ptáte, jak ručně upravit strojový kód programu, pravděpodobně víte, co děláte. Ale ve prospěch všech čtenářů, kteří možná čtou, aby se dozvěděli, upřesním:

Změnil jsem pouze poslední instrukce ve větvi error-case! Skok do funkce, která ukončí program. Ale jak vidíte, zaregistrujte x3 byl upravován mov těsně nad! Ve skutečnosti celkem čtyři (4) registry byly upraveny jako součást preambule na volání error , a jeden registr byl. Zde je úplný strojový kód pro danou větev, počínaje podmíněným skokem přes if blok a končí tam, kde přejde skok, pokud je podmínka if není obsazeno:

     f2c:   350000e8    cbnz    w8, f48
     f30:   b0000002    adrp    x2, 1000
     f34:   91128442    add x2, x2, #0x4a1
     f38:   320003e0    orr w0, wzr, #0x1
     f3c:   2a1f03e1    mov w1, wzr
     f40:   aa1503e3    mov x3, x21
     f44:   97fffeeb    bl  af0 <[email protected]>
     f48:   f94013f7    ldr x23, [sp, #32]

Veškerý kód po větvi vygeneroval kompilátor za předpokladu, že stav programu byl jako před podmíněným skokem ! Ale tím, že uděláte poslední skok na error kód funkce je nefunkční, vytvořil jsem cestu kódu, kde se k tomuto kódu dostaneme s nekonzistentním/nesprávným stavem programu !

V mém případě to ve skutečnosti vypadalo nezpůsobuje žádné problémy. Tak jsem měl štěstí. Velmi štěstí:až poté, co jsem již spustil svůj upravený binární soubor (který byl mimochodem kritickým binárním souborem :měl schopnost setuid , setgid a změňte kontext SELinux !) Uvědomil jsem si, že jsem zapomněl skutečně sledovat cesty kódu, zda tyto změny registru ovlivnily cesty kódu, které přišly později!

To mohlo být katastrofální - kterýkoli z těchto registrů mohl být použit v pozdějším kódu s předpokladem, že obsahuje předchozí hodnotu, která byla nyní přepsána! A já jsem ten typ člověka, kterého lidé znají pro pečlivé přemýšlení o kódu a jako pedanta a přívržence, který si vždy uvědomuje počítačovou bezpečnost.

Co kdybych volal funkci, kde se argumenty vysypaly z registrů do zásobníku (jak je velmi běžné například na x86)? Co když ve skutečnosti bylo v instrukční sadě více podmíněných instrukcí, které předcházely podmíněnému skoku (jak je běžné například u starších verzí ARM)? Byl bych v ještě bezohledněji nekonzistentním stavu poté, co bych provedl tu nejjednodušší, zdánlivě změnu!

Takže toto moje varovné připomenutí: Ruční pohrávání s binárními soubory doslova zbavíte každého bezpečnost mezi vámi a tím, co vám stroj a operační systém dovolí. Doslova vše pokroky, které jsme udělali v našich nástrojích, abychom automaticky zachytili chyby v našich programech, pryč .

Jak to tedy napravíme správně? Čtěte dál.

Odebrání kódu

Chcete-li efektivně /logicky „odebrat“ více než jednu instrukci, můžete první instrukci, kterou chcete „smazat“, nahradit bezpodmínečným skokem na první instrukci na konci „smazaných“ instrukcí. Pro tento binární soubor ARMv8 to vypadalo takto:

     f2c:   14000007    b   f48
     f30:   b0000002    adrp    x2, 1000
     f34:   91128442    add x2, x2, #0x4a1
     f38:   320003e0    orr w0, wzr, #0x1
     f3c:   2a1f03e1    mov w1, wzr
     f40:   aa1503e3    mov x3, x21
     f44:   97fffeeb    bl  af0 <[email protected]>
     f48:   f94013f7    ldr x23, [sp, #32]

V podstatě kód „zabijete“ (proměníte ho na „mrtvý kód“). Vedlejší poznámka:Něco podobného můžete udělat s doslovnými řetězci vloženými do binárního kódu:pokud jej chcete nahradit menším řetězcem, můžete téměř vždy obejít přepsání řetězce (včetně ukončovacího nulového bajtu, pokud je to "C- řetězec") a v případě potřeby přepsání pevně zakódované velikosti řetězce ve strojovém kódu, který jej používá.

Všechny nechtěné pokyny můžete také nahradit ne-ops. Jinými slovy, můžeme změnit nechtěný kód na to, čemu se říká „no-op sled“:

     f2c:   d503201f    nop
     f30:   d503201f    nop
     f34:   d503201f    nop
     f38:   d503201f    nop
     f3c:   d503201f    nop
     f40:   d503201f    nop
     f44:   d503201f    nop
     f48:   f94013f7    ldr x23, [sp, #32]

Očekával bych, že je to jen plýtvání cykly CPU vzhledem k jejich přeskakování, ale je to jednodušší a tím bezpečnější proti chybám , protože nemusíte ručně zjišťovat, jak zakódovat instrukci skoku včetně zjišťování offsetu/adresy, kterou v ní použít – nemusíte tolik přemýšlet pro neoperativní saně.

Aby bylo jasno, chyba je snadná:zpackal jsem dvě (2) časy při ručním kódování této nepodmíněné větvené instrukce. A není to vždy naše chyba:poprvé to bylo proto, že dokumentace, kterou jsem měl, byla zastaralá/nesprávná a říkala, že jeden bit byl v kódování ignorován, i když ve skutečnosti tomu tak nebylo, takže jsem ho na první pokus nastavil na nulu.

Přidání kódu

Mohli byste teoreticky použijte tuto techniku ​​k přidání strojové instrukce také, ale je to složitější a nikdy jsem to nemusel dělat, takže v tuto chvíli nemám zpracovaný příklad.

Z pohledu strojového kódu je to docela snadné:vyberte si jednu instrukci na místě, kam chcete přidat kód, a převeďte ji na instrukci skoku do nového kódu, který potřebujete přidat (nezapomeňte přidat instrukce, které takto nahrazeny do nového kódu, pokud jste to nepotřebovali pro vaši přidanou logiku a pro skok zpět k instrukci, ke které se chcete vrátit na konci přidání). V podstatě „spojujete“ nový kód.

Ale musíte najít místo, kam ten nový kód skutečně umístit, a to je ta nejtěžší část.

Pokud skutečně Naštěstí stačí přidat nový strojový kód na konec souboru a bude to „prostě fungovat“:nový kód se načte spolu se zbytkem do stejných očekávaných strojových instrukcí, do vašeho adresního prostoru, který spadá na stránku paměti správně označenou jako spustitelný soubor.

Podle mých zkušeností hexdump -R ignoruje nejen sloupec zcela vpravo, ale i sloupec zcela vlevo – takže můžete doslova zadat nulové adresy pro všechny ručně přidané řádky a bude to fungovat.

Pokud budete mít méně štěstí, po přidání kódu budete muset skutečně upravit některé hodnoty záhlaví ve stejném souboru:pokud zavaděč vašeho operačního systému očekává, že binární soubor obsahuje metadata popisující velikost spustitelné sekce (z historických důvodů často nazývaná „textová část“), budete ji muset najít a upravit. Za starých časů byly binární soubory pouze surovým strojovým kódem - dnes je strojový kód zabalen do hromady metadat (například ELF na Linuxu a některých dalších).

Pokud máte stále trochu štěstí, můžete mít v souboru nějaké „mrtvé“ místo, které se správně načte jako součást binárního souboru se stejnými relativními posuny jako zbytek kódu, který je již v souboru (a to mrtvý bod se vejde do vašeho kódu a je správně zarovnán, pokud váš procesor vyžaduje zarovnání slov pro instrukce CPU). Poté jej můžete přepsat.

Pokud máte opravdu smůlu, nemůžete jen přidat kód a není tam žádné mrtvé místo, které byste mohli vyplnit svým strojovým kódem. V tu chvíli musíte být v podstatě důvěrně obeznámeni se spustitelným formátem a doufat, že v rámci těchto omezení dokážete přijít na něco, co je v lidských silách vytáhnout ručně v rozumném čase a s rozumnou šancí, že to nepokazíte. .


@mgiuca správně odpověděl na tuto odpověď z technického hlediska. Ve skutečnosti není demontáž spustitelného programu do snadno překompilovatelného zdroje sestavení snadný úkol.

Abychom do diskuse přidali pár drobností, existuje několik technik/nástrojů, které by mohly být zajímavé prozkoumat, i když jsou technicky složité.

  1. Statické/dynamické vybavení . Tato technika zahrnuje analýzu spustitelného formátu, vložení/vymazání/nahrazení specifických instrukcí sestavení pro daný účel, opravu všech odkazů na proměnné/funkce ve spustitelném souboru a vydání nového upraveného spustitelného souboru. Některé nástroje, o kterých vím, jsou:PIN, Hijacker, PEBIL, DynamoRIO. Zvažte, že konfigurace takových nástrojů pro jiný účel, než pro jaký byly navrženy, může být složitá a vyžaduje pochopení jak spustitelných formátů, tak instrukčních sad.
  2. Úplná spustitelná dekompilace . Tato technika se pokouší rekonstruovat úplný zdroj sestavení ze spustitelného souboru. Možná budete chtít mrknout na Online Disassembler, který se o to pokouší. V každém případě ztratíte informace o různých zdrojových modulech a případně funkcích/názvech proměnných.
  3. Dekompilace s možností opětovného cílení . Tato technika se pokouší extrahovat více informací ze spustitelného souboru pomocí otisků kompilátoru (tj. vzory kódu generované známými kompilátory) a další deterministické věci. Hlavním cílem je rekonstruovat zdrojový kód vyšší úrovně, jako je zdroj C, ze spustitelného souboru. To je někdy schopno znovu získat informace o názvech funkcí/proměnných. Zvažte, že kompilace zdrojů s -g často nabízí lepší výsledky. Možná budete chtít zkusit Retargetable Decompiler.

Většina z toho pochází z oblastí výzkumu hodnocení zranitelnosti a analýzy provedení. Jsou to složité techniky a nástroje často nelze použít ihned po vybalení. Přesto poskytují neocenitelnou pomoc při pokusu o zpětnou analýzu nějakého softwaru.


Linux
  1. Jak nainstalovat a otestovat Ansible na Linuxu

  2. Jak zkompilovat a nainstalovat software ze zdrojového kódu na Linuxu

  3. Jak kódovat modul jádra Linuxu?

  1. Jak spravovat a vypisovat služby v Linuxu

  2. Jak nainstalovat a používat Flatpak v Linuxu

  3. Linux – Jak zkontrolovat, zda je linuxová distribuce bezpečná a neobsahuje škodlivý kód?

  1. Jak zakázat letní čas (DST) a upravit časové pásmo v systému Linux

  2. Jak rozebrat binární spustitelný soubor v Linuxu, abyste získali kód sestavení?

  3. Jak udržet spustitelný kód v paměti i pod tlakem paměti? v Linuxu