mirror of
https://github.com/jtalamini/shadowstep
synced 2026-06-06 15:54:35 +00:00
360 lines
12 KiB
C++
360 lines
12 KiB
C++
#include <windows.h>
|
||
#include <stdio.h>
|
||
#include <cstdint>
|
||
#include "shadowmakers.h"
|
||
#include "../crypt/crypt.h"
|
||
#include "core.h"
|
||
|
||
/*
|
||
* high-level classification of x64 instructions handled explicitly
|
||
* by the dispatcher. This allows specialized control-flow handling
|
||
* without executing instructions blindly.
|
||
*/
|
||
enum InstructionType {
|
||
CALL_DIRECT, // E8 rel32
|
||
CALL_INDIRECT, // FF /2
|
||
JMP_INDIRECT, // FF /4
|
||
JMP_REL32, // E9 rel32
|
||
JMP_REL8, // EB rel8
|
||
LOOP, // E2
|
||
LOOPNE, // E0
|
||
LOOPE, // E1
|
||
RET, // C2 / C3
|
||
CONDITIONAL_JUMP, // Jcc variants
|
||
GENERIC // All other instructions
|
||
};
|
||
|
||
// ==========================================================
|
||
// UTILITY FUNCTIONS
|
||
// ==========================================================
|
||
|
||
/*
|
||
* determines whether the given opcode sequence represents
|
||
* any form of conditional jump (short, near, or CX-based).
|
||
*/
|
||
BOOL IsConditionalJump(PBYTE bytes) {
|
||
return
|
||
(bytes[0] >= 0x70 && bytes[0] <= 0x7F) || // Short Jcc
|
||
(bytes[0] == 0x0F && bytes[1] >= 0x80 && bytes[1] <= 0x8F) || // Near Jcc
|
||
(bytes[0] == 0xE3) || // JCXZ
|
||
(bytes[0] == 0x67 && bytes[1] == 0xE3) || // JECXZ
|
||
(bytes[0] == 0x66 && bytes[1] == 0xE3); // JRCXZ
|
||
}
|
||
|
||
/*
|
||
* evaluates a SHORT conditional jump (0x70–0x7F) based on EFLAGS.
|
||
* Returns TRUE if the jump condition is satisfied.
|
||
*/
|
||
bool EvaluateJccCondition(BYTE opcode, DWORD eflags) {
|
||
bool ZF = (eflags & (1 << 6)) != 0;
|
||
bool SF = (eflags & (1 << 7)) != 0;
|
||
bool OF = (eflags & (1 << 11)) != 0;
|
||
bool CF = (eflags & (1 << 0)) != 0;
|
||
bool PF = (eflags & (1 << 2)) != 0;
|
||
|
||
switch (opcode) {
|
||
case 0x70: return OF; // JO
|
||
case 0x71: return !OF; // JNO
|
||
case 0x72: return CF; // JB
|
||
case 0x73: return !CF; // JAE
|
||
case 0x74: return ZF; // JE
|
||
case 0x75: return !ZF; // JNE
|
||
case 0x76: return CF || ZF; // JBE
|
||
case 0x77: return !CF && !ZF; // JA
|
||
case 0x78: return SF; // JS
|
||
case 0x79: return !SF; // JNS
|
||
case 0x7A: return PF; // JP
|
||
case 0x7B: return !PF; // JNP
|
||
case 0x7C: return SF != OF; // JL
|
||
case 0x7D: return SF == OF; // JGE
|
||
case 0x7E: return ZF || (SF != OF); // JLE
|
||
case 0x7F: return !ZF && (SF == OF); // JG
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* same as EvaluateJccCondition, but for NEAR Jcc instructions
|
||
* (0F 80–8F encoding).
|
||
*/
|
||
bool EvaluateJccConditionNear(BYTE subOpcode, DWORD eflags) {
|
||
return EvaluateJccCondition(subOpcode - 0x10, eflags);
|
||
}
|
||
|
||
/*
|
||
* performs a lightweight opcode-based classification to determine
|
||
* whether an instruction requires custom control-flow handling.
|
||
*/
|
||
InstructionType ClassifyInstruction(PBYTE bytes) {
|
||
BYTE opcode = bytes[0];
|
||
|
||
if (opcode == 0xE8) return CALL_DIRECT;
|
||
if (opcode == 0xE9) return JMP_REL32;
|
||
if (opcode == 0xEB) return JMP_REL8;
|
||
if (opcode == 0xE2) return LOOP;
|
||
if (opcode == 0xE0) return LOOPNE;
|
||
if (opcode == 0xE1) return LOOPE;
|
||
if (opcode == 0xC2 || opcode == 0xC3) return RET;
|
||
if (IsConditionalJump(bytes)) return CONDITIONAL_JUMP;
|
||
|
||
// FF /2 = CALL r/m64, FF /4 = JMP r/m64
|
||
if (opcode == 0xFF) {
|
||
BYTE reg = (bytes[1] >> 3) & 0x07;
|
||
if (reg == 2) return CALL_INDIRECT;
|
||
if (reg == 4) return JMP_INDIRECT;
|
||
}
|
||
|
||
return GENERIC;
|
||
}
|
||
|
||
/*
|
||
* returns a pointer to the 64-bit register specified by
|
||
* the ModRM r/m field.
|
||
*/
|
||
const uint64_t* GetRegisterPointer(CONTEXT* ctx, BYTE rm) {
|
||
static const uint64_t* regs[] = {
|
||
&ctx->Rax, &ctx->Rcx, &ctx->Rdx, &ctx->Rbx,
|
||
&ctx->Rsp, &ctx->Rbp, &ctx->Rsi, &ctx->Rdi
|
||
};
|
||
return regs[rm];
|
||
}
|
||
|
||
/*
|
||
* resolves an indirect register-based target address and checks
|
||
* whether it falls inside the shellcode memory range.
|
||
*/
|
||
BOOL IsAddressInShellcode(CONTEXT* ctx, ShellcodeContext* sc, BYTE modrm, OUT uint64_t* address) {
|
||
BYTE rm = modrm & 0x7;
|
||
*address = *GetRegisterPointer(ctx, rm);
|
||
|
||
uint64_t base = (uint64_t)sc->shellcode;
|
||
return (*address >= base && *address <= base + sc->shellcodeSize);
|
||
}
|
||
|
||
// ==========================================================
|
||
// INSTRUCTION HANDLERS
|
||
// ==========================================================
|
||
|
||
/*
|
||
* emulates a direct CALL (E8):
|
||
* - pushes the return address onto the virtual stack
|
||
* - updates currentInstrOffset to the call target
|
||
*/
|
||
void HandleDirectCallInstruction(ShellcodeContext* sc, CONTEXT* ctx, BYTE* execBuffer, DWORD offset, DWORD size) {
|
||
uint64_t returnAddress = (uint64_t)(sc->shellcode + sc->currentInstrOffset + size);
|
||
ctx->Rsp -= 8;
|
||
*(uint64_t*)(ctx->Rsp) = returnAddress;
|
||
int32_t relOffset = *(int32_t*)(sc->shellcode + offset + 1);
|
||
sc->currentInstrOffset += size + relOffset;
|
||
}
|
||
|
||
/*
|
||
* handles indirect CALL/JMP targeting an address inside the shellcode.
|
||
* For CALL, a synthetic return address is pushed manually.
|
||
*/
|
||
void HandleIndirectCallOrJumpInstruction(ShellcodeContext* sc, CONTEXT* ctx, BYTE* execBuffer, DWORD offset, DWORD size, BYTE* bytes) {
|
||
BYTE rm = bytes[1] & 0x07;
|
||
uint64_t targetAddr = *GetRegisterPointer(ctx, rm);
|
||
|
||
// FF /2 -> CALL r/m64
|
||
BYTE regOpcode = (bytes[1] >> 3) & 0x07;
|
||
if (regOpcode == 0x02) {
|
||
SIZE_T pushSize = 0;
|
||
BYTE* instr = GeneratePushReturnAddressBytes(sc->currentInstrOffset + size, (uint64_t)sc->shellcode, pushSize);
|
||
if (instr) {
|
||
ShadowStep(execBuffer, instr, pushSize, ctx);
|
||
free(instr);
|
||
}
|
||
}
|
||
|
||
sc->currentInstrOffset = (DWORD)(targetAddr - (uint64_t)sc->shellcode);
|
||
}
|
||
|
||
/*
|
||
* executes an indirect CALL targeting an external API.
|
||
* execution is delegated directly to ShadowStep.
|
||
*/
|
||
void HandleIndirectCallToAPI(ShellcodeContext* sc, CONTEXT* ctx, BYTE* execBuffer, DWORD offset, DWORD size, BYTE* bytes) {
|
||
ShadowStep(execBuffer, bytes, size, ctx);
|
||
sc->currentInstrOffset += size;
|
||
}
|
||
|
||
/*
|
||
* handles JMP to an external API by restoring execution flow
|
||
* using the return address already present on the stack.
|
||
*/
|
||
void HandleIndirectJumpToAPI(ShellcodeContext* sc, CONTEXT* ctx, BYTE* execBuffer, DWORD offset, DWORD size, BYTE* bytes) {
|
||
uint64_t returnAddr = *(uint64_t*)(ctx->Rsp);
|
||
sc->currentInstrOffset = (DWORD)(returnAddr - (uint64_t)sc->shellcode);
|
||
*(uint64_t*)(ctx->Rsp) = (DWORD64)(execBuffer + 0xD5 + size);
|
||
ShadowStep(execBuffer, bytes, size, ctx);
|
||
}
|
||
|
||
/*
|
||
* emulates LOOP instruction semantics (RCX-based loop).
|
||
*/
|
||
void HandleLoopInstruction(ShellcodeContext* sc, CONTEXT* ctx, DWORD size, BYTE relOffset) {
|
||
ctx->Rcx--;
|
||
if (ctx->Rcx != 0)
|
||
sc->currentInstrOffset += size + (int8_t)relOffset;
|
||
else
|
||
sc->currentInstrOffset += size;
|
||
}
|
||
|
||
/*
|
||
* Handles E9 rel32 jumps.
|
||
*/
|
||
void HandleRelativeJumpInstruction(ShellcodeContext* sc, BYTE* bytes, DWORD size) {
|
||
int32_t relOffset = *(int32_t*)(bytes + 1);
|
||
sc->currentInstrOffset += size + relOffset;
|
||
}
|
||
|
||
/*
|
||
* Handles EB rel8 jumps.
|
||
*/
|
||
void HandleShortJumpInstruction(ShellcodeContext* sc, BYTE* bytes, DWORD size) {
|
||
int8_t relOffset = *(int8_t*)(bytes + 1);
|
||
sc->currentInstrOffset += size + relOffset;
|
||
}
|
||
|
||
/*
|
||
* evaluates condition flags and updates control flow accordingly
|
||
* without executing the instruction natively.
|
||
*/
|
||
void HandleConditionalJumpInstruction(ShellcodeContext* sc, CONTEXT* ctx, BYTE* bytes, DWORD size) {
|
||
int32_t relOffset = 0;
|
||
BOOL condition = FALSE;
|
||
|
||
if (bytes[0] >= 0x70 && bytes[0] <= 0x7F) {
|
||
relOffset = *(int8_t*)(bytes + 1);
|
||
condition = EvaluateJccCondition(bytes[0], ctx->EFlags);
|
||
}
|
||
else if (bytes[0] == 0x0F && bytes[1] >= 0x80 && bytes[1] <= 0x8F) {
|
||
relOffset = *(int32_t*)(bytes + 2);
|
||
condition = EvaluateJccConditionNear(bytes[1], ctx->EFlags);
|
||
}
|
||
else if (bytes[0] == 0xE3) { // JCXZ
|
||
relOffset = *(int8_t*)(bytes + 1);
|
||
condition = ((ctx->Rcx & 0xFFFF) == 0);
|
||
}
|
||
else if (bytes[0] == 0x67 && bytes[1] == 0xE3) { // JECXZ
|
||
relOffset = *(int8_t*)(bytes + 2);
|
||
condition = ((ctx->Rcx & 0xFFFFFFFF) == 0);
|
||
}
|
||
else if (bytes[0] == 0x66 && bytes[1] == 0xE3) { // JRCXZ
|
||
relOffset = *(int8_t*)(bytes + 2);
|
||
condition = (ctx->Rcx == 0);
|
||
}
|
||
|
||
sc->currentInstrOffset += condition ? (size + relOffset) : size;
|
||
}
|
||
|
||
/*
|
||
* emulates RET semantics, including stack cleanup for RET imm16.
|
||
*/
|
||
void HandleRetInstruction(ShellcodeContext* sc, CONTEXT* ctx, BYTE* bytes, DWORD size) {
|
||
uint64_t returnAddress = *(uint64_t*)(ctx->Rsp);
|
||
ctx->Rsp += 8;
|
||
|
||
// RET imm16
|
||
if (bytes[0] == 0xC2 && size == 3) {
|
||
uint16_t imm16 = *(uint16_t*)(bytes + 1);
|
||
ctx->Rsp += imm16;
|
||
}
|
||
|
||
sc->currentInstrOffset = (DWORD)(returnAddress - (uint64_t)sc->shellcode);
|
||
}
|
||
|
||
/*
|
||
* placeholder for LOOPE / LOOPNE handling.
|
||
*/
|
||
void HandleLoopExInstruction(BYTE* bytes) {
|
||
printf(bytes[0] == 0xE1 ? "LOOPE\n" : "LOOPNE\n");
|
||
printf("TODO: not implemented\n");
|
||
exit(0);
|
||
}
|
||
|
||
/*
|
||
* executes any non-control-flow instruction via ShadowStep and the execution buffer
|
||
* and advances the instruction pointer normally.
|
||
*/
|
||
void HandleGenericInstruction(ShellcodeContext* sc, CONTEXT* ctx, BYTE* execBuffer, BYTE* bytes, DWORD size) {
|
||
ShadowStep(execBuffer, bytes, size, ctx);
|
||
sc->currentInstrOffset += size;
|
||
}
|
||
|
||
// ==========================================================
|
||
// MAIN DISPATCHER
|
||
// ==========================================================
|
||
|
||
/*
|
||
* central instruction dispatcher. determines instruction type,
|
||
* applies specialized handling when required, and decides whether
|
||
* native execution should be skipped.
|
||
* returns TRUE if execution flow was fully handled manually.
|
||
*/
|
||
BOOL HandleInstruction(
|
||
BYTE* bytes,
|
||
DWORD offset,
|
||
DWORD size,
|
||
ShellcodeContext* sc,
|
||
CONTEXT* ctx,
|
||
BYTE* execBuffer
|
||
) {
|
||
InstructionType type = ClassifyInstruction(bytes);
|
||
uint64_t address = 0;
|
||
|
||
switch (type) {
|
||
case CALL_DIRECT:
|
||
HandleDirectCallInstruction(sc, ctx, execBuffer, offset, size);
|
||
return TRUE;
|
||
|
||
case CALL_INDIRECT:
|
||
case JMP_INDIRECT: {
|
||
BYTE modrm = bytes[1];
|
||
if (IsAddressInShellcode(ctx, sc, modrm, &address)) {
|
||
printf("[+] %s 0x%p (SHELLCODE)\n", (type == CALL_INDIRECT ? "CALL" : "JMP"), (void*)address);
|
||
HandleIndirectCallOrJumpInstruction(sc, ctx, execBuffer, offset, size, bytes);
|
||
}
|
||
else {
|
||
printf("[+] %s 0x%p (EXTERNAL)\n", (type == CALL_INDIRECT ? "CALL" : "JMP"), (void*)address);
|
||
if (type == CALL_INDIRECT)
|
||
HandleIndirectCallToAPI(sc, ctx, execBuffer, offset, size, bytes);
|
||
else
|
||
HandleIndirectJumpToAPI(sc, ctx, execBuffer, offset, size, bytes);
|
||
}
|
||
return FALSE;
|
||
}
|
||
|
||
case JMP_REL32:
|
||
HandleRelativeJumpInstruction(sc, bytes, size);
|
||
return TRUE;
|
||
|
||
case JMP_REL8:
|
||
HandleShortJumpInstruction(sc, bytes, size);
|
||
return TRUE;
|
||
|
||
case CONDITIONAL_JUMP:
|
||
HandleConditionalJumpInstruction(sc, ctx, bytes, size);
|
||
return TRUE;
|
||
|
||
case LOOP:
|
||
HandleLoopInstruction(sc, ctx, size, bytes[1]);
|
||
return TRUE;
|
||
|
||
case LOOPNE:
|
||
case LOOPE:
|
||
HandleLoopExInstruction(bytes);
|
||
return TRUE;
|
||
|
||
case RET:
|
||
HandleRetInstruction(sc, ctx, bytes, size);
|
||
return TRUE;
|
||
|
||
case GENERIC:
|
||
default:
|
||
HandleGenericInstruction(sc, ctx, execBuffer, bytes, size);
|
||
return FALSE;
|
||
}
|
||
}
|