- A basic custom low level scripting language, with a runtime-environment that can be injected as Shellcode.
- The compiled bytecode can simply be appended to the runtime-environment shellcode to be executed when injected.
- Includes a compiler (written in C#), a command line debugger (with custom debug information format) and a shellcode executer.
- Can also be used as a very small, easily integratable, embedded-friendly runtime-environment for scripts that needs low level access.
- Currently only works with Windows x64, but shouldn't be particularly hard to port.
- Load arbitrary DLLs and extract symbols.
- Call C functions loaded from those DLLs
- Basic programming stuff.
- More advanced programming stuff that isn't absolutely essential.
- It can't even do
for
loops, onlywhile
, nostructs
etc. but the compiler is very hackable. - Oh, and the shellcode isn't null free. One could hack the compiler to output null free byte code and spend the rest of eternity writing a null free interpreter, but that's not the point of this project at the moment.
This is the example script. It only opens a MessageBoxA
. But demonstrates how to load symbols from a DLL.
const text kernel32dll = "User32.dll"; // `text` maps to `ptr<i8>`.
const text messageBoxA = "MessageBoxA";
// `load_library` is provided by the compiler.
// other builtin functions include `alloc`, `free`, `realloc`, `get_proc_address`.
voidptr kernel32dll_handle = load_library(kernel32dll); // `voidptr` maps to `ptr<void>`.
voidptr messageBoxAAddr = get_proc_address(kernel32dll_handle, messageBoxA);
// this is how casts and pointers to external function work.
extern_func<i32 (const voidptr, const text, const text, u32)> messageBoxAFunc =
cast<extern_func<i32 (const voidptr, const text, const text, u32)>>(messageBoxAAddr);
messageBoxAFunc(null, "Hello from the other side!", "Very Important Message", 0);
> llsc example.lls
llsc - LLS Bytecode Compiler (Build Version: 1.0.8566.36389)
Parsing Succeeded. (88 Nodes parsed from 1 Files)
Warning (in 'example.lls', Line 13):
lvalue call to 'extern_func<i32 (const ptr<void>, const ptr<const i8>, const ptr<const i8>, u32)> messageBoxAFunc' will discard the return value of type 'i32'.
Instruction Generation Succeeded. (69 Instructions & Pseudo-Instructions generated.)
Code Generation Succeeded. (393 Bytes)
Successfully wrote byte code to 'bytecode.lls'.
Compilation Succeeded.
- Open the compiler output file
bytecode.lls
and the runtime-environment shellcodescript_host.bin
in a hex editor like HxD. - Create a new hex-file and first paste in the contents of
script_host.bin
. This section should end with the magic constant37 6F 63 03 12 9E 71 31
. - Now paste in the contents of
bytecode.lls
. These should usually begin with0E
(which is the op codeLLS_OP_STACK_INC_IMM
). - Save the file.
Use runsc
to test your shellcode.
runsc <YourShellcodeFileName>
Debugging such a hacked runtime environment is obviously not as easy as your normal programming languages, but there's a command line (low level) debugger.
> llsc example.lls -dbgdb
llsc - LLS Bytecode Compiler (Build Version: 1.0.8566.36389)
Parsing Succeeded. (88 Nodes parsed from 1 Files)
Warning (in 'example.lls', Line 13):
lvalue call to 'extern_func<i32 (const ptr<void>, const ptr<const i8>, const ptr<const i8>, u32)> messageBoxAFunc' will discard the return value of type 'i32'.
Instruction Generation Succeeded. (69 Instructions & Pseudo-Instructions generated.)
Code Generation Succeeded. (393 Bytes)
Successfully wrote byte code to 'bytecode.lls'.
Successfully wrote debug database to 'bytecode.lls.dbg'.
Compilation Succeeded.
> llscript_dbg bytecode.lls bytecode.lls.dbg
llshost byte code interpreter
'c' to run / continue execution
'n' to step
'l' to step a line (only available with debug info)
'f' to step out
'b' to set the breakpoint
'r' for registers
'p' for stack bytes
'y' for advanced stack bytes
'i' to inspect a value
'm' to modify a value
'v' show recent values (only available with debug info)
'o' clear recent values (only available with debug info)
'w' set value filter (only available with debug info)
'W' break on a value filter match (only available with debug info)
'F' continue to next function call/return
's' toggle silent
'S' toggle silent comments
'q' to restart
'x' to quit
'z' to debug break
File: example.lls
1: const text kernel32dll = "User32.dll"; // `text` maps to `ptr<i8>`.
>>
Now press l to step line by line, c to run. Recently modified values and associated lines in the script will be displayed above the input line if debug information is available.
kernel32dll @ code base offset 320 (array<i8>) : 0x7FF50BC00140
--> 85, 115, 101, 114, 51, 50, 46, 100, 108, 108, 0, 77, 101, 115, 115, 97, 103, 101, 66, 111, 120, 65, 0, 72, ...
--> 0x55, 0x73, 0x65, 0x72, 0x33, 0x32, 0x2E, 0x64, 0x6C, 0x6C, 0x0, 0x4D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x6F, 0x78, 0x41, 0x0, 0x48, ...
--> "User32.dll"
messageBoxA @ code base offset 331 (array<i8>) : 0x7FF50BC0014B
--> 77, 101, 115, 115, 97, 103, 101, 66, 111, 120, 65, 0, 72, 101, 108, 108, 111, 32, 102, 114, 111, 109, 32, 116, ...
--> 0x4D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x6F, 0x78, 0x41, 0x0, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x66, 0x72, 0x6F, 0x6D, 0x20, 0x74, ...
--> "MessageBoxA"
6: voidptr kernel32dll_handle = load_library(kernel32dll); // `voidptr` maps to `ptr<void>`.
Simply include the corresponding header, link to script_host.lib
and start the runtime-environment with a pointer to the bytecode.
#include "llshost.h"
llshost_state_t state = {};
state.pCode = your_compiled_byte_code;
// If you need to pass in additional values, simply set register values:
state.registerValues[0] = (uint64_t)value0;
state.registerValues[1] = (uint64_t)value1;
// Now, start the runtime-environment.
llshost_from_state(&state);
- Clone the repo
git clone https://github.com/rainerzufalldererste/llscript.git
- Run
create_project.bat
, selectVisual Studio 2015
if you're trying to build the script host for shellcode (everything newer will produce calls tomemcpy
even without thecrt
. If you're just planning to embed the script host in another application, any new Visual Studio version is fine.
If you just want to play with the debugger or compiler, you can simply use MSBuild
or Visual Studio
to build the project solution.
If you want to create a shellcode version of a modified runtime-environment:
- Build
llscript_asm
thenllscript_host
thenllscript_host_bin
. (Visual Studio sometimes gets confused aboutllscript_asm
, so not relying on dependencies is probably a good idea) - Extract the
.code
section via some dumping tool or simply open the binary in IDA, go to the last statement of the last symbol, and select everything upwards in the hex view, then paste that into a hex editor. - Now we need to patch the assembly, because getting the current
rip
isn't something that can be expressed in a position independent or overly complicated way in msvc afaik, so we'll need to replace the assembly generated foruint8_t *pCode = __readgsqword(0);
withlea <whatever register the compiler chose>, [rip]
. - if the register was
rax
, replace65 48 8B 04 25 00 00 00 00
(mov rax,qword ptr gs:[0]
) with48 8D 05 00 00 00 00 90 90
. - if the register was
rax
, replace65 48 8B 0C 25 00 00 00 00
(mov rcx,qword ptr gs:[0]
) with48 8D 0D 00 00 00 00 90 90
. - if the register was anything else, use the defuse.ca online x64 assembler and assemble
lea <whatever register the compiler chose>, [rip]
and replace the corresponding code with whatever that says. Pad with0x90
(nop
). - Lastly, append the magic constant (
37 6F 63 03 12 9E 71 31
) to the shellcode. The runtime-environment will search for this pattern when launched without code to find it's input.
- MIT