Shikata ga nai (仕方がない) encoder ported into go with several improvements
Go
32
60 commits
updated Aug 27, 2026
SGN is a polymorphic binary encoder for x86 and x64 shellcode. It uses an additive feedback loop to encode a payload and produces a self-decoding, polymorphic result inspired by the original Shikata ga nai.
This fork preserves the Go module, CLI, and public pkg API while adopting the
upstream Rust port as the encoding implementation. The Rust core uses
iced-x86 instead of the legacy Keystone text
assembler.
The checked-in pkg/sgn.wasm is the Rust library compiled as a
wasm32-wasip1 cdylib. Go embeds that module with go:embed and executes it
in-process with wazero:
Go CLI or github.com/moloch--/sgn/pkg
|
v
wazero runtime
|
v
embedded pkg/sgn.wasm ABI
|
v
upstream Rust Encoder
Go users therefore do not need Rust installed at runtime and do not launch an external process. Rust remains available as a native library and CLI from the same checkout.
The existing Go Encoder, its exported fields, and helpers are retained for
source compatibility. Encoder.Encode now delegates to the Rust/Wasm pipeline.
Legacy helpers such as the assembly, cipher, decoder, and obfuscation helpers
remain implemented in Go for callers that use them directly; they are not part
of the native-Rust/Wasm byte-parity guarantee.
Encoder.EncodeWithSeed accepts a 32-byte RandomSeed and is deterministic
when the payload and all Encoder fields are also identical. In particular,
the legacy one-byte Encoder.Seed is the ADFL key and must be fixed separately.
The compatibility suite supplies the same inputs to native Rust and embedded
Rust/Wasm, then compares their output bytes and final mutable encoder state.
The parity boundary is native Rust versus the embedded build of that same Rust
source and locked dependency graph. It intentionally does not promise the same
bytes as the historical Go encoder: the move from Keystone to iced-x86, plus
changes to instruction layout and random-number consumption, changed the
polymorphic byte stream.
Production calls should use Encode. The Go API obtains both its ADFL key and
its 32-byte Rust RNG seed from crypto/rand; native Rust Encoder::encode uses
the cryptographically secure thread RNG. Fixed seeds are exposed for tests,
reproducible fixtures, and debugging, not as the production default.
Each encoding pass:
--plain is set, schema-encrypts the stub and prepends a schema
decoder.The schema wrapper places its decoder code before terminal encoded data. This lets safe-mode payloads fall through their register-restore suffix to the end of the complete output instead of re-entering schema-decoder instructions. Safe mode therefore requires the payload to fall through with RSP restored to its entry value; a payload that returns before the generated suffix bypasses restoration. On x64, the save wrapper uses 16 stack slots so the payload retains its incoming ABI stack alignment. It restores general-purpose registers, not RFLAGS, XMM/SIMD, or x87 state.
Install the self-contained Go/Wasm CLI:
go install github.com/moloch--/sgn@latest
Build it from a checkout using the pinned Rust toolchain:
rustup toolchain install 1.94.0
rustup target add wasm32-wasip1 --toolchain 1.94.0
make
./build/sgn --help
Install the native Rust CLI from the checkout instead:
cargo +1.94.0 install --locked --path .
Encode uses fresh cryptographic randomness on every call:
package main
import (
"log"
"os"
sgn "github.com/moloch--/sgn/pkg"
)
func main() {
payload, err := os.ReadFile("payload.bin")
if err != nil {
log.Fatal(err)
}
encoder, err := sgn.NewEncoder(64)
if err != nil {
log.Fatal(err)
}
encoded, err := encoder.Encode(payload)
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("payload.bin.sgn", encoded, 0o600); err != nil {
log.Fatal(err)
}
}
Use EncodeWithSeed when a reproducible fixture is required:
func encodeFixture(payload []byte) ([]byte, error) {
encoder, err := sgn.NewEncoder(64)
if err != nil {
return nil, err
}
encoder.Seed = 0xa7 // the independent, legacy one-byte ADFL key
var randomSeed sgn.RandomSeed
for i := range randomSeed {
randomSeed[i] = 0x42
}
return encoder.EncodeWithSeed(payload, randomSeed)
}
use sgn::{Encoder, RANDOM_SEED_SIZE};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let payload = std::fs::read("payload.bin")?;
// Production: cryptographically secure randomness.
let mut production = Encoder::new(64)?;
let _encoded = production.encode(&payload)?;
// Tests/fixtures: fix both the ADFL key and the 32-byte RNG seed.
let mut replay = Encoder::new(64)?;
replay.seed = 0xa7;
let _replayed = replay.encode_with_seed(&payload, [0x42; RANDOM_SEED_SIZE])?;
Ok(())
}
See examples/encode_binary.rs for a complete
native example.
The Go/Wasm CLI retains the fork's existing flags:
Usage: sgn
Flags:
-i, --input=STRING Input binary path
-o, --out=STRING Encoded output binary name
-a, --arch=64 Binary architecture (32/64)
-c, --enc=1 Number of encoding passes
-M, --max=50 Maximum decoder-obfuscation bytes
--plain Do not encode the decoder stub
--ascii Require fully ASCII-printable output
-S, --safe Preserve general-purpose registers after fallthrough
--badchars=STRING Avoid bytes such as \\x00\\x0a
-v, --verbose Verbose output
--version Print version
Example:
sgn -i shellcode.bin -o encoded.bin -a 64 --badchars '\x00\x0a\x0d'
Rust source changes must be accompanied by a refreshed embedded module. All
Cargo commands use --locked, and the Makefile uses Rust 1.94.0 so the tracked
artifact can be reproduced in CI. The Wasm-only Cargo profile preserves symbols
through linking because link-time stripping can reorder internal functions on
different compiler hosts. A repo-local Go tool then removes non-semantic custom
sections deterministically. Together with stable source-path remapping, this
makes the final byte comparison portable across builders.
# Build and normalize the module, then copy it into pkg/sgn.wasm.
make wasm-update
# Independently rebuild and fail if the tracked copy differs.
make wasm-verify
# Verify the embedded module, then build the Go CLI.
make
Cargo.lock and pkg/sgn.wasm are intentionally committed. target/ and
build/ remain generated output.
Run the portable native Rust, Go, and seeded differential suite:
make test
The individual commands are:
make test-rust
make test-go
make test-compat
make test-wasm-execution # Linux x64 only
make test-compat builds examples/compat_oracle.rs, sets
SGN_NATIVE_ORACLE, and runs TestNativeRustWASMCompatibility. CI also runs
an upstream d914ab2 plain-decoder golden vector plus a fixed-schema golden
vector so the native and Wasm implementations cannot drift together unnoticed.
The full Go suite runs with the race detector:
make test-go GO_TEST_FLAGS=-race
Native Rust tests include cipher round trips and real execution tests. The x86
execution harness skips automatically when a 32-bit C toolchain is unavailable
for local development. CI installs that toolchain and sets
SGN_REQUIRE_EXECUTION=1, so either x86 or x64 execution being unavailable is a
test failure rather than a silent skip. Both architectures execute 1,792
replayable variants: seven shared modes times every initial ADFL key byte, with
an explicit 32-byte RNG seed for every case. The modes cover schema and plain
decoders, register preservation on and off, zero/default/high obfuscation,
multiple layers, and the exact one-layer safe/schema/obfuscation-100 Sliver
profile. Failures include the complete replay tuple and child exit or signal.
On Linux x64, make test-wasm-execution independently runs all 1,792 outputs
returned by the public Go EncodeWithSeed method in isolated child processes.
Every x64 safe-mode corpus case asserts SysV entry alignment. The suite also
executes the exact Sliver safe/schema profile with an aligned SIMD stack access,
representative GPR clobber/restoration, and a 16 MiB payload that exercises the
complete decoder loop. Native Rust runs the same regressions. This composes the
embedded-Wasm wrapper with real decoder execution instead of relying only on
native/Wasm byte equality.
Production encoding continues to use operating-system randomness.
iced-x86's typed assembler API rather than Keystone
assembly text.call/pop two-pass scheme.random_byte spans the full 0..=255 range and the garbage-size limit is
consistently applied per block.The original SGN design and Rust port are by Ege Balci. This fork's Go/Wasm layer exists to keep the established Go integration while sharing the current Rust encoder implementation.
Shikata ga nai (仕方がない) encoder ported into go with several improvements
Go
32
60 commits
updated Aug 27, 2026
SGN is a polymorphic binary encoder for x86 and x64 shellcode. It uses an additive feedback loop to encode a payload and produces a self-decoding, polymorphic result inspired by the original Shikata ga nai.
This fork preserves the Go module, CLI, and public pkg API while adopting the
upstream Rust port as the encoding implementation. The Rust core uses
iced-x86 instead of the legacy Keystone text
assembler.
The checked-in pkg/sgn.wasm is the Rust library compiled as a
wasm32-wasip1 cdylib. Go embeds that module with go:embed and executes it
in-process with wazero:
Go CLI or github.com/moloch--/sgn/pkg
|
v
wazero runtime
|
v
embedded pkg/sgn.wasm ABI
|
v
upstream Rust Encoder
Go users therefore do not need Rust installed at runtime and do not launch an external process. Rust remains available as a native library and CLI from the same checkout.
The existing Go Encoder, its exported fields, and helpers are retained for
source compatibility. Encoder.Encode now delegates to the Rust/Wasm pipeline.
Legacy helpers such as the assembly, cipher, decoder, and obfuscation helpers
remain implemented in Go for callers that use them directly; they are not part
of the native-Rust/Wasm byte-parity guarantee.
Encoder.EncodeWithSeed accepts a 32-byte RandomSeed and is deterministic
when the payload and all Encoder fields are also identical. In particular,
the legacy one-byte Encoder.Seed is the ADFL key and must be fixed separately.
The compatibility suite supplies the same inputs to native Rust and embedded
Rust/Wasm, then compares their output bytes and final mutable encoder state.
The parity boundary is native Rust versus the embedded build of that same Rust
source and locked dependency graph. It intentionally does not promise the same
bytes as the historical Go encoder: the move from Keystone to iced-x86, plus
changes to instruction layout and random-number consumption, changed the
polymorphic byte stream.
Production calls should use Encode. The Go API obtains both its ADFL key and
its 32-byte Rust RNG seed from crypto/rand; native Rust Encoder::encode uses
the cryptographically secure thread RNG. Fixed seeds are exposed for tests,
reproducible fixtures, and debugging, not as the production default.
Each encoding pass:
--plain is set, schema-encrypts the stub and prepends a schema
decoder.The schema wrapper places its decoder code before terminal encoded data. This lets safe-mode payloads fall through their register-restore suffix to the end of the complete output instead of re-entering schema-decoder instructions. Safe mode therefore requires the payload to fall through with RSP restored to its entry value; a payload that returns before the generated suffix bypasses restoration. On x64, the save wrapper uses 16 stack slots so the payload retains its incoming ABI stack alignment. It restores general-purpose registers, not RFLAGS, XMM/SIMD, or x87 state.
Install the self-contained Go/Wasm CLI:
go install github.com/moloch--/sgn@latest
Build it from a checkout using the pinned Rust toolchain:
rustup toolchain install 1.94.0
rustup target add wasm32-wasip1 --toolchain 1.94.0
make
./build/sgn --help
Install the native Rust CLI from the checkout instead:
cargo +1.94.0 install --locked --path .
Encode uses fresh cryptographic randomness on every call:
package main
import (
"log"
"os"
sgn "github.com/moloch--/sgn/pkg"
)
func main() {
payload, err := os.ReadFile("payload.bin")
if err != nil {
log.Fatal(err)
}
encoder, err := sgn.NewEncoder(64)
if err != nil {
log.Fatal(err)
}
encoded, err := encoder.Encode(payload)
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("payload.bin.sgn", encoded, 0o600); err != nil {
log.Fatal(err)
}
}
Use EncodeWithSeed when a reproducible fixture is required:
func encodeFixture(payload []byte) ([]byte, error) {
encoder, err := sgn.NewEncoder(64)
if err != nil {
return nil, err
}
encoder.Seed = 0xa7 // the independent, legacy one-byte ADFL key
var randomSeed sgn.RandomSeed
for i := range randomSeed {
randomSeed[i] = 0x42
}
return encoder.EncodeWithSeed(payload, randomSeed)
}
use sgn::{Encoder, RANDOM_SEED_SIZE};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let payload = std::fs::read("payload.bin")?;
// Production: cryptographically secure randomness.
let mut production = Encoder::new(64)?;
let _encoded = production.encode(&payload)?;
// Tests/fixtures: fix both the ADFL key and the 32-byte RNG seed.
let mut replay = Encoder::new(64)?;
replay.seed = 0xa7;
let _replayed = replay.encode_with_seed(&payload, [0x42; RANDOM_SEED_SIZE])?;
Ok(())
}
See examples/encode_binary.rs for a complete
native example.
The Go/Wasm CLI retains the fork's existing flags:
Usage: sgn
Flags:
-i, --input=STRING Input binary path
-o, --out=STRING Encoded output binary name
-a, --arch=64 Binary architecture (32/64)
-c, --enc=1 Number of encoding passes
-M, --max=50 Maximum decoder-obfuscation bytes
--plain Do not encode the decoder stub
--ascii Require fully ASCII-printable output
-S, --safe Preserve general-purpose registers after fallthrough
--badchars=STRING Avoid bytes such as \\x00\\x0a
-v, --verbose Verbose output
--version Print version
Example:
sgn -i shellcode.bin -o encoded.bin -a 64 --badchars '\x00\x0a\x0d'
Rust source changes must be accompanied by a refreshed embedded module. All
Cargo commands use --locked, and the Makefile uses Rust 1.94.0 so the tracked
artifact can be reproduced in CI. The Wasm-only Cargo profile preserves symbols
through linking because link-time stripping can reorder internal functions on
different compiler hosts. A repo-local Go tool then removes non-semantic custom
sections deterministically. Together with stable source-path remapping, this
makes the final byte comparison portable across builders.
# Build and normalize the module, then copy it into pkg/sgn.wasm.
make wasm-update
# Independently rebuild and fail if the tracked copy differs.
make wasm-verify
# Verify the embedded module, then build the Go CLI.
make
Cargo.lock and pkg/sgn.wasm are intentionally committed. target/ and
build/ remain generated output.
Run the portable native Rust, Go, and seeded differential suite:
make test
The individual commands are:
make test-rust
make test-go
make test-compat
make test-wasm-execution # Linux x64 only
make test-compat builds examples/compat_oracle.rs, sets
SGN_NATIVE_ORACLE, and runs TestNativeRustWASMCompatibility. CI also runs
an upstream d914ab2 plain-decoder golden vector plus a fixed-schema golden
vector so the native and Wasm implementations cannot drift together unnoticed.
The full Go suite runs with the race detector:
make test-go GO_TEST_FLAGS=-race
Native Rust tests include cipher round trips and real execution tests. The x86
execution harness skips automatically when a 32-bit C toolchain is unavailable
for local development. CI installs that toolchain and sets
SGN_REQUIRE_EXECUTION=1, so either x86 or x64 execution being unavailable is a
test failure rather than a silent skip. Both architectures execute 1,792
replayable variants: seven shared modes times every initial ADFL key byte, with
an explicit 32-byte RNG seed for every case. The modes cover schema and plain
decoders, register preservation on and off, zero/default/high obfuscation,
multiple layers, and the exact one-layer safe/schema/obfuscation-100 Sliver
profile. Failures include the complete replay tuple and child exit or signal.
On Linux x64, make test-wasm-execution independently runs all 1,792 outputs
returned by the public Go EncodeWithSeed method in isolated child processes.
Every x64 safe-mode corpus case asserts SysV entry alignment. The suite also
executes the exact Sliver safe/schema profile with an aligned SIMD stack access,
representative GPR clobber/restoration, and a 16 MiB payload that exercises the
complete decoder loop. Native Rust runs the same regressions. This composes the
embedded-Wasm wrapper with real decoder execution instead of relying only on
native/Wasm byte equality.
Production encoding continues to use operating-system randomness.
iced-x86's typed assembler API rather than Keystone
assembly text.call/pop two-pass scheme.random_byte spans the full 0..=255 range and the garbage-size limit is
consistently applied per block.The original SGN design and Rust port are by Ege Balci. This fork's Go/Wasm layer exists to keep the established Go integration while sharing the current Rust encoder implementation.