The return address doesn't have to point to code in the same buffer, it is just often easier to do it this way. If you can put the shell code in and return address into the same buffer then this is the simplest. If the buffer that can be overflowed is too small to fit the shell code, it is feasible to put the shell code into another buffer and then jump to that when the vulnerable buffer is overflowed.
Also, protections such as Data Execution Prevention or (NX) prevent code being executed from a stack. In this case, techniques such as Return-Oriented Programming can be used to circumvent DEP. This technique involves using legitimate, executable code segments to run code the attacker wants to.
This can be tricky and may require some fiddling around with the payload. Usually the start of a buffer is at an address that is
word
aligned. In this case, ensuring the return address is correctly aligned means writing a buffer that is a multiple of the CPU word (4 bytes for 32-bit machines, 8 bytes for 64-bit). If the buffer is not word aligned, then an attacker may just experiment by adding or removing byte at a time until he thinks it is.
The reason it is simpler to do everything in one buffer is because not much will change between the injection of the shell code and the jumping to the newly modified return address. At the point of attack, it is very unlikely that an attacker can reference memory of another process, so we must look at buffers in process.
Putting shell code into a different buffer requires the attacker to understand how long the buffer will stay in place. Do different function calls cause one of the buffers to be deallocated? Is one of the buffers on the heap instead of the stack? So long as your single buffer is large enough, it is much simpler to put your NOP sled and shell code near the start and then just fill the rest with the return address. Compared to finding one buffer to populate with shell code and another to populate with the address of the previous buffer. Some shell code may also reference the stack pointer which means it needs to be set correctly.