I'd consider myself technical, but I've seen some of what others can do, so I'll just call myself a dabbler.
@LainTrain Yes, but "in practice" this simple approach worked 20 years ago. Modern processors, compilers and operating systems make exploitation of stack buffer overflows a lot more difficult.
@LainTrain The simplest case is overwriting the return address on the stack. If your stack layout looks like this (B for buffer, R for return address, A for function arguments):
BBBBBBBBRRRRAAAA
and you give a pointer to the first B byte to gets(), the input can overwrite the bytes of R.
You can try this with a 32-bit program complied with disabled mitigations. Run the program in a debugger, break in the function, inspect the stack pointer value. With ASLR disabled the addresses will remain the same for every program execution assuming the call graph at this point doesn't change. You can then overwrite the bytes of R with the buffer address (assuming no stack canary), and overwrite the buffer bytes with machine code instructions. When the function attempts to return, it instead jumps to the instructions you left in the buffer, and executes them (assuming no W^X).
@LainTrain There used to be approximately a million examples floating around in the web. You could just write a simple program with a fixed-size stack buffer at a repeatable address, overflow a return address with a crafted string, return to the overwritten stack buffer full of shellcode. All of the mitigations (stack canaries, W^X, ASLR, CFI, canonical addresses, ...) mean that you have to either use much more elaborate techniques (ROP/return to libc, address leaks, ...) or you have to disable the mitigations to see a working exploit example, which is pretty unimpressive.