Shellcode/Environment
It is possible use shellcode to determine instruction set architecture, determine the process counter, determine the location last returned to, or bypass and detect int3 breakpoints within the current execution environment.
Contents
x86/x64 GetCPU (any OS)
Architecture can only be determined when compatible channels between the target instruction set architectures can be isolated. As long as the instructions perform valid behavior and do not cause access faults on operating systems native to the architecture, it is possible to use a single bytecode sequence in order to determine architecture across a variety of processors. It takes a high amount of familiarity and experience with two or more given instruction sets to write shellcode for multiple architectures.
x64 does not vastly differ from x86 because AMD stepped in to correct intel's calling convention and architecture.
Instruction Comparison
This chart was derived by cross referencing available alphanumeric 64 bit instructions with available printable 32 bit instructions.
Hex | ASCII | Assembler Instruction |
---|---|---|
0x64, 0x65 | d,e | [fs | gs] prefix |
0x66, 0x67 | f,g | 16bit [operand | ptr] override |
0x68, 0x6a | h,j | push |
0x69, 0x6b | i,k | imul |
0x6c-0x6f | l-o | ins[bwd], outs[bwd] |
0x70-0x7a | p-z | Conditional Jumps |
0x30-0x35 | 0-5 | xor |
0x36 | 6 | %ss segment register |
0x38-0x39 | 8,9 | cmp |
0x50-0x57 | P-W | push *x, *i, *p |
0x58-0x5a | XYZ | pop [*ax, *cx, *dx] |
Inter-compatibility theory
The opcodes which are specifically not compatible are limited to the 32 and 64 bit opcodes 0x40-0x4f, which allow a 32 bit processor to increment or decrement its general-purpose registers, but become prefixes for manipulation of 64 bit registers and 8 additional 64 bit general purpose registers in x64 environments, %r8-%r15.
Because not all opcodes are intercompatible, yet comparisons and conditional jumps are intercompatible, it is possible to determine the architecture of an x86 processor using exclusively alphanumeric opcodes.
By making use of these additional registers (which 32 bit processors do not have), one can perform an operation that will set a value on a different register in the two processors.
Following this, a conditional statement can be made against one of the two registers to determine if the value was set.
Using the pop instruction is the most effective way to set the value of a register due to instructional limitations (to keep the code alphanumeric). Using an alternative register to %rsp or %esp as a placeholder for the stack pointer enables the use of an effective conditional statement to determine if the value of a register is equal to the most recent thing pushed or popped from the stack.
Practically Applied: Code
This simple alphanumeric bytecode is 15 bytes long, ending in a comparison which returns equal on a 32 bit system and not equal on a 64 bit system.
When implementing this bytecode, a conditional jump afterwards may be best reserved for the t and u instructions, jump if equal and jump if not equal, respectively.
- Assembled:
- TX4HPZTAZAYVH92
- Disassembly:
OpCodes | x86 | x64 |
---|---|---|
54 | push %esp |
push %rsp |
58 | pop %eax |
pop %rax |
34 48 | xor $0x48, %al |
xor $0x48, %al |
50 | push %eax |
push %rax |
5a | pop %edx |
pop %rdx |
54 | push %esp |
push %rsp |
41
5a |
inc %ecx pop %edx |
pop %r10 |
41
59 |
inc %ecx pop %ecx |
pop %r9 |
56 | push %esi |
push %rsi |
48
39 32 |
dec %eax cmp %esi,(%edx) |
cmp %rsi,(%rdx) |
This code executes similarly on both a 32-bit and 64-bit system - similarly, but not identically. The key discrepancy between how the two architectures execute this code is contained in the opcodes "0x41 - 0x4f".
Under a 32-bit architecture, this code pops the value of esp (which was pushed onto the stack previously) into edx and increments ecx. The esp register is a pointer to the top of the stack, so now rdx contains a pointer to the top of the stack. After this is done, the shellcode goes on to push esi onto the stack - so the value of esi now resides at the top of the stack. When the code executes its final comparison - "cmp %esi, (%edx)" - it is comparing the value of esi to the value that edx points to. As edx points to the top of the stack, and as we have just pushed esi onto the top of the stack, we are comparing the value of esi with the value of esi. For this reason, it returns equal.
This shellcode is not equal under a 64-bit architecture, however. The opcodes "41 5a 41 59" have a different function - instead of popping the value of esp into rdx, it instead pops it into the 64-bit register %r10 without incrementing %ecx or %rcx, as 0x41 is used as a prefix to indicate the access to the 64-bit architecture. As a result, when the final comparison is made between rsi and the value referenced by rdx, it returns not equal, as rdx does not point to the top of the stack.
On a 64-bit system, this will not cause a segfault because (%rdx) points to somewhere inside the stack.
GetPc
The GetPc technique is any technique implementing code which obtains the current instruction pointer. This can be useful when writing self-modifying shellcode, or other code that must become aware of its environment because environment information cannot be supplied prior to execution of the code.
x86 (32 bit)
jmp startup getpc: mov (%esp), %eax ret startup: call getpc ; the %eax register now contains %eip on the next line |
x64
jmp startup getpc: mov (%rsp), %rax ret startup: call getpc ; the %rax register now contains %rip on the next line |
Last call
Typically, when shellcode is being executed at the time of buffer overflow, assuming that the nop sled does not modify the stack, the pointer to the beginning of the executing code is at -0x8(%rsp), or -0x4(%esp), because it was just returned to as a result of the call stack being overwritten during the overflow process. In many cases, this can be used in place of a GetPc for polymorphic shellcode.
32-bit
mov -0x4(%esp), %eax |
64-bit
mov -0x8(%rsp), %rax |
Alphanumeric
- Assembled x64:
XTX4e4uHcp4H3p4H30
.text .global _start _start: pop %rax push %rsp pop %rax xor $0x65, %al xor $0x75, %al movslq 0x34(%rax), %rsi xor 0x34(%rax), %rsi xor (%rax), %rsi |
int3 breakpoints
.text .global _start _start: jmp startup go_retro: pop %rcx inc %rcx jmp *%rcx startup: call go_retro volatile_segment: push $0x3458686a push $0x0975c084 nop |
{} shellcode gdb loaders/loader-64 Reading symbols from /home/user/loaders/loader-64...(no debugging symbols found)...done. (gdb) break ret_to_shellcode Breakpoint 1 at 0x4000b1 (gdb) run "$(generators/shellcode-generator.py --file=int3 --raw)" Starting program: /home/user/loaders/loader-64 "$(generators/shellcode-generator.py --file=int3 --raw)" Breakpoint 1, 0x00000000004000b1 in ret_to_shellcode () (gdb) x/24i $rax 0x7ffff7fbe000: jmp 0x7ffff7fbe008 0x7ffff7fbe002: pop %rcx 0x7ffff7fbe003: inc %rcx 0x7ffff7fbe006: jmpq *%rcx 0x7ffff7fbe008: callq 0x7ffff7fbe002 0x7ffff7fbe00d: pushq $0x3458686a 0x7ffff7fbe012: pushq $0x975c084 0x7ffff7fbe017: nop ... (gdb) break *0x7ffff7fbe017 Breakpoint 2 at 0x7ffff7fbe017 (gdb) c Continuing. Breakpoint 2, 0x00007ffff7fbe017 in ?? () (gdb) quit A debugging session is active. Inferior 1 [process 9760] will be killed. Quit anyway? (y or n) y
{} shellcode gdb loaders/loader-64 Reading symbols from /home/user/loaders/loader-64...(no debugging symbols found)...done. (gdb) break ret_to_shellcode Breakpoint 1 at 0x4000b1 (gdb) run "$(generators/shellcode-generator.py --file=int3 --raw)" Starting program: /home/user/loaders/loader-64 "$(generators/shellcode-generator.py --file=int3 --raw)" Breakpoint 1, 0x00000000004000b1 in ret_to_shellcode () (gdb) x/24i $rax 0x7ffff7fbe000: jmp 0x7ffff7fbe008 0x7ffff7fbe002: pop %rcx 0x7ffff7fbe003: inc %rcx 0x7ffff7fbe006: jmpq *%rcx 0x7ffff7fbe008: callq 0x7ffff7fbe002 0x7ffff7fbe00d: pushq $0x3458686a 0x7ffff7fbe012: pushq $0x975c084 0x7ffff7fbe017: nop ... (gdb) break *0x7ffff7fbe012 Breakpoint 2 at 0x7ffff7fbe012 (gdb) c Continuing. [Inferior 1 (process 9778) exited normally] (gdb)