The real “unexpected” behavior is that setting the flag makes the heap executable as well as the stack. The flag is intended for use with executables that generate stack-based thunks (such as gcc when you take the address of a nested function) and shouldn’t really affect the heap. But Linux implements this by globally making ALL readable pages executable.
If you want finer-grained control, you could instead use the mprotect
system call to control executable permissions on a per-page basis — Add code like:
uintptr_t pagesize = sysconf(_SC_PAGE_SIZE);
#define PAGE_START(P) ((uintptr_t)(P) & ~(pagesize-1))
#define PAGE_END(P) (((uintptr_t)(P) + pagesize - 1) & ~(pagesize-1))
mprotect((void *)PAGE_START(shellcode), PAGE_END(shellcode+67) - PAGE_START(shellcode),
PROT_READ|PROT_WRITE|PROT_EXEC);