Remote TUI
GView can run on one machine (the server: where the files are and where every parser and plugin runs) and be used from another one (the client: the analyst). Only the characters of the text user interface travel over the network, never the analysed data itself. The protocol is a small, purpose-built RPC-style protocol on top of TLS 1.3 with mutual authentication.
The remote mode is optional. It is compiled only when CMake is configured with -DGVIEW_ENABLE_REMOTE=ON
(default OFF); without it, GView contains no networking code for this feature and the commands below report
that the build does not support them.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGVIEW_ENABLE_REMOTE=ON
cmake --build build --config Release --parallel
Quick start
Create a private CA and one short-lived certificate per GView instance (run it on a trusted machine; the CA private key
gview-ca.keynever leaves it):GView remote-certs ./pki --name server.lab # certificate of the server (host name or IP address) GView remote-certs ./pki --name analyst1 # certificate of an analyst
Each instance receives its own
<name>.crt/<name>.keyandgview-ca.crt. Running the command again for the same name rotates that certificate (the CA is reused). Certificates are valid for 14 days by default (--days), the CA for 365 days (--ca-days).Start the server (headless: it does not use the local terminal). Every option can also be set in the
[Remote]section ofgview.ini:GView serve --bind 0.0.0.0 --cert pki/server.lab.crt --key pki/server.lab.key --ca pki/gview-ca.crt sample.exe
By default the server only listens on
127.0.0.1: exposing it to other machines is an explicit decision (--bind).Ctrl+C(or Exit from a connected analyst) stops it. Every connection, authenticated identity (subject and SHA-256 fingerprint of the certificate) and disconnection is logged on stderr.Connect from the analyst machine, either from the command line or with File / Connect to a remote GView:
GView connect server.lab --cert pki/analyst1.crt --key pki/analyst1.key --ca pki/gview-ca.crt
The remote screen opens in a GView window; several windows (several servers) can be open at the same time and arranged like any other window. Every key goes to the remote GView; press Ctrl+F12 (configurable, RemoteLocalKey) and the next key is handled by the local GView instead (for example
Ctrl+F12thenAlt+0opens the local windows manager).Escapecloses a disconnected window.
Reverse connection
When the analysed host can not accept inbound connections, the analyst waits for it and the server connects out (and reconnects with an exponential back-off when the session ends):
GView listen 0.0.0.0:18262 --cert pki/analyst1.crt --key pki/analyst1.key --ca pki/gview-ca.crt # analyst
GView serve --reverse analyst1.lab:18262 --server-name analyst1 --cert ... --key ... --ca ... # server
In reverse mode the TCP/TLS roles are swapped (the analyst is the TLS server) but the protocol roles are not: the
server still sends the screen and the analyst still sends the input. --server-name is the name that must appear
in the analyst certificate (default: the host given to --reverse).
Multiple analysts
Several analysts can be connected to the same server (--max-clients, default 4, at most 32). They share one
GView instance: their key and mouse events are queued in a single FIFO and executed in the order they were received,
and everybody sees the same screen. Like tmux, the shared screen has the size of the smallest connected
analyst so that everybody sees the whole interface.
Configuration
[Remote]
Certificate = "pki/server.lab.crt" ; PEM, this instance (+ optional intermediate certificates)
PrivateKey = "pki/server.lab.key" ; PEM, unencrypted (protect it with file permissions)
TrustedCA = "pki/gview-ca.crt" ; CA(s) that must have issued the peer certificate
ALPN = "gview/1"
BindAddress = "127.0.0.1"
Port = 18262
MaxClients = 4
MaxPeerCertificateLifetimeDays = 30 ; 0 = no limit
Relative paths are resolved against the folder of gview.ini (portable deployments keep everything next to the
binaries). Command line options (--cert, --key, --ca, --alpn, --max-cert-days, --bind,
--port, --max-clients, --size) override the file.
Security model
Threat model. The network is hostile: an attacker may eavesdrop, replay, reorder, delay or modify traffic (man-in-the-middle). Both peers are also untrusted input sources: the server renders attacker-controlled files (malware) and a client may be compromised. The protocol must provide confidentiality, integrity, replay protection and mutual authentication, and each side must survive arbitrary bytes from the other.
Transport. TLS 1.3 only, with the AEAD suites TLS_AES_256_GCM_SHA384 and
TLS_CHACHA20_POLY1305_SHA256 and the X25519 key exchange; OpenSSL security level 3 (at least 128-bit
security for every key and signature). Session tickets, resumption and 0-RTT early data are disabled (early data can
be replayed), as are compression and renegotiation.
Mutual authentication. Both ends present an X.509 certificate that must chain to the configured CA bundle; the
system trust store is never used (a certificate from any public CA is rejected). The client verifies that the
server certificate was issued for the host it connected to (DNS or IP subject alternative name; --server-name
overrides it). A peer certificate whose lifetime exceeds MaxPeerCertificateLifetimeDays is rejected, so stolen
or forgotten long-lived certificates are useless. The server re-reads its certificate, key and CA bundle when the
files change (checked at most every 2 s, applied to new connections), so certificates can be rotated by a scheduler
or by an ACME / private-CA agent without restarting GView; an inconsistent intermediate state (new certificate, old
key) keeps the previous identity in use.
ALPN. gview/1 (configurable) is mandatory: a peer that does not negotiate it is refused, which keeps other
TLS clients (and cross-protocol attacks) out.
MITM resistance. Any modification of a record makes its AEAD authentication fail and the session ends (see the tampering experiment below); no tampered key or screen update is ever applied.
Hostile input handling. Every message size is capped per type and direction before anything is buffered (a client may not send more than 260 bytes per message), every count, offset, size and code is validated, delta frames must reference the frame the client actually holds, frame chunks must arrive strictly in order with increasing frame ids, and a malformed message closes the connection. Characters received from a server are sanitized before they reach the local terminal (C0/C1 control characters, lone surrogates and non-characters are replaced), so a malicious screen can not inject terminal escape sequences.
Resource limits. At most MaxClients authenticated analysts plus 8 connections in the handshake phase; the
TLS handshake and the protocol handshake each have a 10 s deadline; a peer that stops reading for 30 s is
disconnected; the input queue is bounded (mouse moves are coalesced) and a slow client never makes the server buffer
frames (each client receives the most recent screen when it can, encoded against the last frame it received).
Data exposure. Only screen characters leave the server. The server’s OS clipboard is never read or written in server mode (copy / paste work through a clipboard private to the GView process). A connected analyst has the same abilities as a local GView user on the server (open files, plugins, exit GView); Learning and Evaluation Mode policies apply unchanged.
Wire protocol (version 1)
All integers are little endian. Every message is framed as:
u8 Type
u32 PayloadLength
u8 Payload[PayloadLength]
Type |
Name |
Direction |
Payload |
|---|---|---|---|
|
KeyEvent |
C → S |
|
|
MouseEvent |
C → S |
|
|
Resize |
C → S |
|
|
Hello |
C → S |
|
|
Close |
both |
|
|
TuiFrame |
S → C |
|
|
TuiOptimizedFrame |
S → C |
one chunk of a logical frame (below) |
|
Cursor |
S → C |
|
|
Welcome |
S → C |
|
A cell is u16 characterCode, u8 foreground, u8 background (the 16 AppCUI colors, 0x10 = transparent): the
“u16 color” of the original design with the foreground in the low byte. The 0x81 payload size is therefore
4 + width × height × 4 bytes.
Session. After the TLS handshake the client sends Hello (its screen size and capabilities; capability
0x1 = understands 0x82). The server answers Welcome and then streams the screen: a full frame first,
then only changes, and Cursor whenever the cursor moves. Nothing is sent while the screen does not change.
Either side may end the session with Close (reasons: 0 normal, 1 protocol error, 2 unsupported version,
3 server full, 4 timeout, 5 shutdown, 6 internal error).
Keys. modifiers bits: 0x1 Alt, 0x2 Ctrl, 0x4 Shift, 0x8000 unicode (other bits must be 0).
Without the unicode bit keyCode is an AppCUI::Input::Key code (< Key::Count) and the receiver derives the
character (letters, digits, space; none with Ctrl/Alt). With the unicode bit keyCode is a printable UTF-16 code
unit and the receiver derives the key code (letters, digits, space). keyCode = 0 without the unicode bit reports a
new modifier (shift) state. AppCUI only processes presses; releases are accepted and ignored.
Mouse. event: 0 release, 1 press, 2 move, 3 wheel. button flags: 0x1 left, 0x2 right, 0x4
middle, 0x8 double click; for a wheel event it is the direction (1 up, 2 down, 3 left, 4 right). modifiers
uses the Alt / Ctrl / Shift bits above. Events outside the current screen are dropped.
Optimized frames (0x82). A logical frame is split in chunks of at most 32 KB:
u32 frameId strictly increasing, starts at 1
u64 totalBytes size of the logical frame (≤ 4 MB)
u32 chunkIndex 0, 1, 2 ... (in order, one frame at a time)
u32 dataLength 1 .. 32768
u8 data[dataLength]
The reassembled logical frame is:
u8 kind 0 = full, 1 = delta
u16 width, u16 height
u32 baseFrameId delta: id of the frame it applies to (the one the client holds); full: 0
full : runs covering width × height cells
delta: u32 spanCount, then spanCount × { u32 offset, u32 count, runs covering count cells }
(offset = y × width + x, spans strictly increasing and non overlapping)
Runs use a u16 tag: 0x8000 | n = n repetitions of the single cell that follows, otherwise n literal
cells follow (1 ≤ n ≤ 32767). The server sends whichever of full / delta is smaller and merges changed cells that
are separated by at most two unchanged cells into one span.
Limits: screens are 20×6 … 1024×512 cells (the requested sizes are clamped).
Tools and evaluation
tools/remote_tui_probe.py is an independent reference client (Python, standard library only): it connects with
mutual TLS, decodes every frame, can send keys and prints the remote screen and the number of bytes exchanged.
python tools/remote_tui_probe.py server.lab --pki ./pki --name analyst1 --keys Down Down F4 --dump
tools/remote_tamper_proxy.py is the MITM tampering experiment: a TCP proxy that flips one bit of the N-th TLS
record of one direction. The receiving end reports bad record mac and terminates the session.
python tools/remote_tamper_proxy.py --listen 127.0.0.1:18263 --target 127.0.0.1:18262 --direction c2s --record 6
Notes on the design
Points where the implementation makes the original design precise:
The key event keeps its 5-byte layout; the
0x8000modifier bit tells whetherkeyCodeis an AppCUI key or a character (both can not fit in 5 bytes). A shifted punctuation key (e.g.Shift+1→!) is transmitted as its character.The mouse event carries a modifiers byte (7 bytes in total) and a wheel event type.
Hello/Welcome(version and capability negotiation, initial size),Resize,CursorandClosecomplete the message set; the logical content of0x82(run-length encoded full and delta frames) is specified above.Frames are not compressed (no zlib): compressing attacker-influenced content together with confidential content under the same encryption leaks information through the record sizes (CRIME / BREACH class).
Known limitations: the client does not forward pure modifier changes (AppCUI controls are not notified of them), so the remote command bar does not switch to its Alt / Ctrl variants while a modifier is held; the shared screen uses the special character set of the server (Unicode).