[Chaos CD][Datenschleuder] [79]
  [Chaos CD]
  [Datenschleuder] [79] Multi Plattform Code
[ -- ] [ ++ ] [Suchen]  

 

Multi Plattform Code

Die Idee zu binärem Code, der auf mehreren Hardwarearchitekturen und Betriebssystemen ausgeführt werden kann, basiert auf dem Wunsch, mit einem exploit möglichst viele Plattformen attackieren zu können. Als gängigere bezeichung wird auch 'architecture spanning shellcode' verwendet.

Anwenden ließe sich derartigen shellcode, wenn einem angreifer die ausführung von eigenem code möglich wäre, er aber keine kenntnis darüber erlangen kann, für welche architektur der code geschrieben sein müsste. einem gezielten angriff geht in der praxis wohl ein port-scan mit os-fingerprinting oder die suche nach aussagekräftigen service-bannern voraus, bringt das jedoch nicht die notwendigen informationen, muss tendenziell mehr 'probiert' werden.
Beim injizieren von code über buffer-overflows muss in der regel bezüglich alignment und treffen einer korrekten rücksprungadresse sowieso probiert werden. existiert das zu exploitende programm auf verschiedenen plattformen, verhält es sich compiler-bedingt in bezug auf die programminterne speicherverwaltung, also z.b. an welche speicheradresse ein statischer puffer liegt, anders. bei der verwendung von multi-plattform-code lassen sich somit maximal die anzahl der versuche reduzieren.
das zentrale problem ist, dass die einzelnen cpus ganz unterschiedliche vorstellungen davon haben, als was eine bytesequenz interpretiert werden soll, denn im normalfall unterscheiden sich cpus in bezug auf instruktionssätze, befehlslänge und endianess.
um das interpretations-problem auf möglichst wenige bytes zu reduzieren, gestaltet man den code nun so, dass er am anfang einen block mit ausgewählten sprung-anweisungen hat. diese sprunganweisungen sollten nur von einem cpu-typ als sprunganweisung gewertet werden und für alle anderen prozessoren, auf denen der code funktionieren soll, nicht-verzweigende, harmlose, valide instruktionen darstellen. wird beim ausführen vom maschinencode verzweigt, soll zum architekturspezifischen teil des shellcodes gesprungen werden.
überlegen wir etwas konkreter, wie der sprungblock beschaffen sein muss bzw. was springende und harmlose befehle sind. dazu überlegen wir uns, in welche möglichen klassen sich bytesequenzen einteilen lassen. aufgrund der vielzahl von befehlen hier nur eine unscharfe kategorisierung:
elementar sind die sprungbefehle, die wie bereits erwähnt auf genau einem cpu-typ als sprungbefehl gewertet werden. in diese klasse fallen auch bytesequenzen, die bei ausführung implizit springen, z.b. durch manipulation des instruktionszeigers. das besondere augenmerk gilt hier den befehlen, die relativ zur aktuellen position im code springen, erspart das doch eine menge stress, da auf adressberechnungen verzichtet werden kann.
'harmlose' bytesequenzen stellen auf allen unterstützen cpus ein oder mehrere gültige instruktionen dar, die keinen sprung bewirken. stattdessen verändern sie z.b. registerinhalte.
in der gruppe der zu vermeidenden bytesequenzen sind diejenigen, welche bei der ausführung zur prozessbeendung führen können oder andersweitig unvorhersagbare auswirkungen haben, also z.b. indirekte adressierung über register. im allgemeinen ist also der unkontrollierte zugriff auf speicherbereiche oder io-ports kritisch.
eine bytesequenz aus dem sprungblock kann prinzipiell in mehrere klassen fallen, wie folgendes beispiel verdeutlichen soll. dann ist die reihenfolge bei der verwendung entscheidend: angenommen wir wollen einen sprungblock für drei marktübliche prozessoren a, b und c basteln. sprung[a] sei eine der sprungauslösenden instruktionen für den cpu-typ a (usw.). sprung[a] bewirkt auf den cpus b und c nichts schlimmes, weshalb sprung[a] als erstes in die sprungtabelle aufgenommen wird. jetzt müssen nur noch die beiden anderen cpus betrachtet werden - sprung[b] ist zwar auf a eine illegale instruktion, wird der sprungblock jedoch von a angegangen, dann hat die cpu bereits verzweigt. sprung[b] stellt auf c einen zu vermeidenden befehl dar, jedoch ist sprung[c] auf b harmlos, womit sich die reihenfolge im sprungblock sprung[c] und dann sprung[b] ergibt.
dass bei bei einer multilingualen reisegruppe ein teilnehmer versteht, er wäre ein 'blöder insel-puper' und deshalb den reiseführer massakriert, der gerade über bauwerke referiert, ist unwahrscheinlich. dass bytesequenzen von einem cpu-typ akzeptiert werden, einem anderen jedoch spanisch vorkommen und das os vorsichtshalber den prozess terminert, ist dagegen der normalfall. versuchen wir im folgenden, einen sprungblock für i386 und 68k durch geeignetes probieren zu finden, bei dem die reihenfolge der sprunganweisungen beliebig ist.

i386 und 68k

--------------------------------------------- 
#!/bin/sh 
# simple hack to convert 
# bytes to instructions 


GCC=gcc2 
if [ ! "$1" ] ; then 
   echo usage: instruction_babel.sh '<asm-statement>' 
   echo e.g. '.byte 0x40' 
   echo '.long 0x40404040' 
   echo 'b 4' 
   exit 1 
fi 

cat << ASM > _tmp.c 
   int main(void) { 
   __asm__("mylabel: $1"); 
   return 1; 
   } 

ASM 
   rm _tmp.bin 2> /dev/null 
   $GCC -ggdb -o _tmp.bin _tmp.c && \
   cat << GDB > _gdb_cmd 
      disassemble mylabel mylabel+4 
      x/4x mylabel 
      quit 

GDB 
   if [ -f _tmp.bin ]; then 
      gdb -x _gdb_cmd _tmp.bin | 
      perl -ne 'if(m!<mylabel(\+\d+)?>!){print}' 
   fi 

--------------------------------------------- 

idee #1

  • bedingungsloser sprung auf 68k
  • 68k: bras foo; nop;nop;nop;foo:
  • code: 0x60064e71
  • i386: jno 0x8048500 <_fini+16>; or %ah,0xffffffb8(%eax)
  • erkenntnis: 68k-nop (0x4e71) ist auf intel ein bedingter sprung; 7? am ende der bytesequenz ist ungünstig, denn in diesem bereich wertet intel alles als sprung

idee #2

  • wie in #1 ein bedingungsloser sprung, jedoch wird das nop ersetzt
  • 68k: bras foo; lsr %d0,%d1;nop;nop;foo:
  • code: 0x6006e069
  • i386: imul $0x1b86008,%eax,%esp
  • erkenntnis: dumped

idee #3

  • kombinierter bedingter sprung; die bedingung ist carry-flag gesetzt bzw. gelöscht
  • 68k: bccs foo; bcss foo;nop;nop;foo:
  • code: 0x64066504
  • i386: add $0x65,%al; push %es; fs
  • erkenntnis: es gibt eine 1-byte instruktion fs (opcode: 64), deren bedeutung mir unklar ist (scheint jedoch zu funktionieren)

idee #4

  • kombinierter bedingter sprung (bedingung ist zero-flag gesetzt/gelöscht)
  • 68k: beqs foo; bnes foo;nop;nop;foo:
  • code: 0x67066604
  • i386: add $0x66,%al; push %es; addr16 mov $0x1,%eax
  • erkenntnis: harmlose instruktion auf intel

versuchen wir jetzt den umgekehrten weg - eine sprunganweisung für intel-cpus, die harmlose instruktionen auf 68k-prozessoren darstellt. ferner wird der 2-byte umfassende relative sprung auf i386 geeignet auf 4 bytes gepadded.

idee #5

  • wir benötigen zuerst eine 2-byte lange instruktion, die sowohl unter 68k als auch intel harmlos ist
  • 68k: addq #2, %d0
  • code: 0x5440
  • i386: inc %eax; push %esp
  • erkenntnis: solange wir keinen korrupten stackpointer haben, erfüllt die sequenz die anforderungen

idee #6

  • relativer, nicht bedingter sprung (opcode: 0xeb) gekoppelt mit einer 2-byte harmlosen instruktion
  • i386: .word 0x03eb;inc %eax; push %esp
  • code: 0x544003eb
  • 68k: addqw #2,%d0; bset %d1,%a3@(28673)
  • erkenntnis: schlecht, da unkontrollierter speicherzugriff

idee #7

  • wie eben, nur mit verändertem sprungziel
  • i386: .word 0x41eb; inc %eax; push %esp
  • code: 0x544041eb
  • 68k: addqw #2,%d0;lea %a3@(28673),%a0
  • erkenntnis: geht, denn lea führt nur eine adressberechnung durch, greift jedoch nicht auf den speicher zu

somit wir haben eine sprungtabelle für die beiden prozessoren 68k und i386:

  • 0x544041eb -> i386 '41' bytes nach vorne
  • 0x67066604 -> 68k '4' bytes nach vorne
erscheint uns die sprungweite aus #6 zu wenig, können wir alternativ auch die sequenz 0x670e660c verwenden. unter intel ergibt sich damit
'or $0x66,%al; push %cs;addr16 mov $0x1,%eax'.

ulrasparc2

beim versuch eine weitere cpu zu unterstützen wird klar, wie haarig das ganze vorhaben ist, denn die bisher ermittelten bytesequenzen werden auf ultrasparc zu call-anweisungen.
d.h. wir müssten den sparc-jump zuerst handlen und dafür eine sequenz finden, die sowohl unter intel als auch 68k harmlos ist. jeder ultrasparc-befehl umfasst 4 byte und sprungbefehle
werden folgendermaßen encoded:

 ----------------------------|------------------------|------------------ 
     31 30 |29|28 27 26 25|24|23 22|21 20 19 18 17 16 | 15 .. 0 
    instr. |af| condition |condit. |<--------address----------> 
     class |  |           | code   | 
 ----------------------------|------------------------|------------------ 
       0 0 | 1|.......... | x| x  x| 0  0  0  0  0  0 | ................. 
 ----------------------------|------------------------|------------------ 
                             |                        | möglichst klein 
         erstes byte muss    | 
         0x2? oder 0x3? sein | 
                  | 
                     0| 0 0 => 0 schlecht, weil '\0' 
                 0| 0 1 => 4 ungültige instruktion 
                 0| 1 0 => 8 b,a 
                 0| 1 1 => c ungültige instruktion 
                 1| 0 0 => 0 schlecht, weil '\0' 
                 1| 0 1 => 4 ungültige instruktion 
                 1| 1 0 => 8 f-branch 
                 1| 1 1 => c cb??? 
die höheren bits vom displacement setzen wir auf 0, da wir uns den luxus, über 64k zu springen, nicht leisten wollen. die bits 15 bis 0 stellen den anderen teil der sprungweite dar. an der stelle sind wir bzgl. einer konkreten wahl flexibel. um ein '\0'-byte in der instruktion zu vermeiden, müssen wir mindestens 0x104 * 4 bytes springen. 0x0104 wird auf 68k als 'btst %d0,%d4' und auf i386 als 'add $0x1,%al' interpretiert. der sparc-sprung 0x3080???? wird zwar unter 68k zu 'movew %d0,%a0@', allerdings hält intel das für einen impliziten speicherzugriff 'xorb $0xb8,(%eax)'. eine sprungweitenangabe, die von intel als harmlose 4-byte-instruktion gewertet wird und wertmäßig nicht zu hoch ist, lässt sich trotz viel probieren nicht finden. gehen wir nocheinmal einen schritt zurück und überlegen uns einen alternativen weg, indem wir die reihenfolge der sprunginstruktionen veränden:
  • 0.) sprung zum intel-code
  • 1.) sprung zum ultrasparc2-code
  • 2.) sprung zum 68k-code

den intel-sprung ganz oben anzuordnen, hätte folgenden vorteil: im gegensatz zu 68k/ultrasparc2/ppc gibt es harmlose 1-byte instruktionen und wir haben mehr spielraum bezüglich der gestaltung. in eugenes artikel für die phrack wird die 1-byte intel-instruktion 'aaa' (bcd-korrektur nach addition, opcode: 0x37) zum padden benutzt. der befehl ist ideal, wenn man intel und ultrasparc2-code mischen möchte, denn man kann damit einen validen sethi-befehl konstruieren. um auf ein 2-byte padding zu kommen, kann man jedoch nicht zweifach 'aaa' verwenden, da diese kombination auf 68k zu problemen führt. als workaround suchen wir für '??' in 0x37??41eb einen geeigneten ersatz und mit etwas probieren stoßen wir auf 0x27:

#8

  • intel: .word 0x41eb;pop %ss; aaa
  • code: 0x372741eb
  • ultrasparc: sethi %hi(0x9d07ac00), %i3
  • 68k: movew %sp@-,%a3@-; lea %a3@(28673),%a0
  • anmerkung : harmlose adressberechnung
damit ergibt sich folgender sprungblock:
  • intel: 0x372741eb
  • ultrasparc: 0x30800101
  • 68k: 0x67066604
jetzt versuchen wir noch powerpc zu unterstützen und bieten die vorhandenen bytesequenzen der cpu an:
  • 0x372741eb -> addic. r25,r7,16875
  • 0x30800101 -> addic r4,r0,257
  • 0x67066604 -> oris r6,r24,26116
die bisher gefundenen sequenzen werden als harmlose befehle interpretiert. den architekturspezifischen code für powerpc könnten im anschluss an den sprungblock schreiben oder weiterschauen, wie wir ppc-sprünge integrieren können: einige sprungbefehle für ppc:
  • bedingungslos: b mylabel+0x4 -> 0x48000004
  • bedingungslos (mit link-register): bl mylabel+0x4 -> 0x48000005
  • bedingter sprung: beq mylabel+0x4 -> 0x41820004
  • bedingter sprung: bne mylabel+0x4 -> 0x40820004
bei den bedingten sprung-instruktionen ist die verteilung der gesetzten bits in bezug auf die vermeidung von null-bytes günstiger. eine geringfügige anpassung der sprungweite ist dennoch erforderlich.

zuletzt versuchen wir noch eine 4-byte-sequenz zu finden, die auf allen zu unterstützenden prozessoren einen harmlosen befehl darstellt. mit dieser bytefolge können wir alle undefinierten stellen im bisher konstruierten code-gerüst auffüllen. mit minimalem probieren findet man z.b. 0x21040104

  • intel: add $0x1,%al; add $0x21,%al
  • ppc: subfic r8,r4,260
  • 68k: movel %d4,%a0@-
  • ultrasparc: sethi %hi(0x10041000), %l0
 xxxxx: 0x21040104 ... 
 xxxxx: 0x21040104 
 0x000: 0x372741eb 
 0x004: 0x30800101 
 0x008: 0x670e660c 
 0x00c: 0x41820104 
 0x010: 0x40820104 
 0x014: 
 0x018: <68k_code> 
 0x040: 0x90909090 
 0x044: <intel_code> 
 0x110: <ppc_code> 
 0x114: 
 0x408: <ultrasparc2_code> 
für den architekturspezifischen teil bietet sich an, zuerst zu überprüfen, gegen welche betriebssystem-api programmiert werden muss. da mir gerade nur ein eingeschränktes set an plattformen zur verfügung stehen, betrachten wir das exemplarisch bei intel-typischen systemen. die generelle idee ist, die registerbelegung auszuwerten. hängt man sich mit einem debugger an verschiedene prozesse, so stellt man fest, dass die segmentregister typische werte speichern. anhand dieser merkmale lässt sich keine eindeutige unterscheidung treffen, und somit stellt dieser ansatz nur eine heuristik dar.
  • freebsd: es=0x2f
  • netbsd: es=0x1f
  • openbsd: es=0x1f
  • windows_xp: es=0x23
  • linux: es=0x2b
im code könnte das so aussehen:
 mov %es,%eax cmp $0x2f,%eax je fbsd cmp $0x23,%eax je win ... 
die sprünge verzweigen dann zum code, der os-spezifisch ist. die konventionen für systemaufrufe unterscheiden sich von betriebssystem zu betriebssystem. unter linux auf x86 werden parameter über allzweckregister übergeben, unter bsd über den stack. die syscall-nummern unter aktuellem net-/open-/freebsd und auch darwin sind für gängige funktionen wie open, execve, bind und listen - wahrscheinlich historisch bedingt - gleich.

fazit

in der praxis findet multi-plattform-shellcode keine richtige anwendung, da i.d.r. vorher das zielsystem bekannt ist. für stack-exploits ist die kenntnis einer ungefähren rücksprungadresse erforderlich, die stark von gesetzten compiler-flags abhängt und mit einem debugger erforscht oder durch probieren gefunden werden muss. ferner erfordert kombinierter shellcode mehr 'platz', besonders durch die angepassten sprungweiten.

links

 

  [Chaos CD]
  [Datenschleuder] [79] Multi Plattform Code
[ -- ] [ ++ ] [Suchen]