├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── README.md ├── build.zig ├── flake.lock ├── flake.nix └── src ├── load.zig ├── main.zig ├── memflow.zig ├── run.zig ├── shellcode.zig ├── test_dll.zig └── test_exe.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | flake.lock linguist-language=JSON 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Check & Build" 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build-and-cache: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: cachix/install-nix-action@v16 12 | - run: nix flake check 13 | - run: nix build .# 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | 4 | result 5 | result-* 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libs/zig-clap"] 2 | path = libs/zig-clap 3 | url = https://github.com/Hejsil/zig-clap.git 4 | shallow = true 5 | [submodule "libs/zigwin32"] 6 | path = libs/zigwin32 7 | url = https://github.com/marlersoft/zigwin32.git 8 | shallow = true 9 | [submodule "libs/zig-args"] 10 | path = libs/zig-args 11 | url = https://github.com/MasterQ32/zig-args.git 12 | shallow = true 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### TODO 2 | 3 | - [ ] Forcefully load unsigned drivers 4 | - [ ] Hide kernel debugging state 5 | - [ ] Load DLL into usermode process (`LoadLibrary` & manual mapping loader) 6 | - [ ] Spawn usermode process 7 | - [ ] Dump disk encryption private key (LUKS & BitLocker) 8 | - [ ] Bypass windows login screen 9 | - [ ] Fault trigger (command to raise a page fault for the bounds of any usermode process) 10 | 11 | ### Credits 12 | 13 | - [DMA Attack the Kernel talk by Ulf Frisk](https://av.tib.eu/media/36297) 14 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Pkg = std.build.Pkg; 4 | 5 | const pkgs = struct { 6 | pub const win32 = Pkg{ 7 | .name = "win32", 8 | .path = .{ .path = "./libs/zigwin32/win32.zig" }, 9 | }; 10 | }; 11 | 12 | pub fn build(builder: *std.build.Builder) void { 13 | const target = builder.standardTargetOptions(.{}); 14 | const mode = builder.standardReleaseOptions(); 15 | 16 | const memflow_shell = builder.addExecutable("memflow-shell", "src/main.zig"); 17 | memflow_shell.setTarget(target); 18 | memflow_shell.addPackagePath("args", "./libs/zig-args/args.zig"); 19 | memflow_shell.setBuildMode(mode); 20 | memflow_shell.install(); 21 | memflow_shell.linkLibC(); 22 | memflow_shell.linkSystemLibrary("memflow_ffi"); // libmemflow_ffi.so 23 | 24 | const test_dll = builder.addSharedLibrary("test-dll", "src/test_dll.zig", .unversioned); 25 | test_dll.setTarget(.{ 26 | .cpu_arch = .x86_64, 27 | .os_tag = .windows, 28 | }); 29 | test_dll.setBuildMode(mode); 30 | test_dll.addPackage(pkgs.win32); 31 | test_dll.linkLibC(); 32 | test_dll.linkSystemLibraryName("user32"); 33 | test_dll.install(); 34 | 35 | const test_exe = builder.addExecutable("test-exe", "src/test_exe.zig"); 36 | test_exe.setTarget(.{ 37 | .cpu_arch = .x86_64, 38 | .os_tag = .windows, 39 | }); 40 | test_exe.addPackage(pkgs.win32); 41 | test_exe.setBuildMode(mode); 42 | test_exe.install(); 43 | 44 | const run_cmd = memflow_shell.run(); 45 | run_cmd.step.dependOn(builder.getInstallStep()); 46 | if (builder.args) |args| { 47 | run_cmd.addArgs(args); 48 | } 49 | 50 | const run_step = builder.step("run", "Run the app"); 51 | run_step.dependOn(&run_cmd.step); 52 | 53 | const exe_tests = builder.addTest("src/main.zig"); 54 | exe_tests.setTarget(target); 55 | exe_tests.setBuildMode(mode); 56 | 57 | const test_step = builder.step("test", "Run unit tests"); 58 | test_step.dependOn(&exe_tests.step); 59 | } 60 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "cglue-bindgen": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1647799225, 7 | "narHash": "sha256-DprIuK75ldeU1JHOi25GFXV167OmAoGDaJ1JPxZR9ig=", 8 | "owner": "h33p", 9 | "repo": "cglue", 10 | "rev": "4a942c55a6dd215045132b51d63b47b32560b0cc", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "h33p", 15 | "repo": "cglue", 16 | "type": "github" 17 | } 18 | }, 19 | "cloudflow": { 20 | "flake": false, 21 | "locked": { 22 | "lastModified": 1648659411, 23 | "narHash": "sha256-1+APBiqYTJVmJmktisms+0HtEsYPLYCvAGKqH9fgQW8=", 24 | "owner": "memflow", 25 | "repo": "cloudflow", 26 | "rev": "1423409d07c266d1205281129414a415045030c8", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "owner": "memflow", 31 | "repo": "cloudflow", 32 | "type": "github" 33 | } 34 | }, 35 | "flake-compat": { 36 | "flake": false, 37 | "locked": { 38 | "lastModified": 1648199409, 39 | "narHash": "sha256-JwPKdC2PoVBkG6E+eWw3j6BMR6sL3COpYWfif7RVb8Y=", 40 | "owner": "edolstra", 41 | "repo": "flake-compat", 42 | "rev": "64a525ee38886ab9028e6f61790de0832aa3ef03", 43 | "type": "github" 44 | }, 45 | "original": { 46 | "owner": "edolstra", 47 | "repo": "flake-compat", 48 | "type": "github" 49 | } 50 | }, 51 | "flake-utils": { 52 | "locked": { 53 | "lastModified": 1648297722, 54 | "narHash": "sha256-W+qlPsiZd8F3XkzXOzAoR+mpFqzm3ekQkJNa+PIh1BQ=", 55 | "owner": "numtide", 56 | "repo": "flake-utils", 57 | "rev": "0f8662f1319ad6abf89b3380dd2722369fc51ade", 58 | "type": "github" 59 | }, 60 | "original": { 61 | "owner": "numtide", 62 | "repo": "flake-utils", 63 | "type": "github" 64 | } 65 | }, 66 | "flake-utils_2": { 67 | "locked": { 68 | "lastModified": 1648297722, 69 | "narHash": "sha256-W+qlPsiZd8F3XkzXOzAoR+mpFqzm3ekQkJNa+PIh1BQ=", 70 | "owner": "numtide", 71 | "repo": "flake-utils", 72 | "rev": "0f8662f1319ad6abf89b3380dd2722369fc51ade", 73 | "type": "github" 74 | }, 75 | "original": { 76 | "owner": "numtide", 77 | "repo": "flake-utils", 78 | "type": "github" 79 | } 80 | }, 81 | "flake-utils_3": { 82 | "locked": { 83 | "lastModified": 1637014545, 84 | "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=", 85 | "owner": "numtide", 86 | "repo": "flake-utils", 87 | "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4", 88 | "type": "github" 89 | }, 90 | "original": { 91 | "owner": "numtide", 92 | "repo": "flake-utils", 93 | "type": "github" 94 | } 95 | }, 96 | "flake-utils_4": { 97 | "locked": { 98 | "lastModified": 1629481132, 99 | "narHash": "sha256-JHgasjPR0/J1J3DRm4KxM4zTyAj4IOJY8vIl75v/kPI=", 100 | "owner": "numtide", 101 | "repo": "flake-utils", 102 | "rev": "997f7efcb746a9c140ce1f13c72263189225f482", 103 | "type": "github" 104 | }, 105 | "original": { 106 | "owner": "numtide", 107 | "repo": "flake-utils", 108 | "type": "github" 109 | } 110 | }, 111 | "memflow": { 112 | "inputs": { 113 | "cglue-bindgen": "cglue-bindgen", 114 | "cloudflow": "cloudflow", 115 | "flake-utils": "flake-utils_2", 116 | "memflow": "memflow_2", 117 | "memflow-coredump": "memflow-coredump", 118 | "memflow-kcore": "memflow-kcore", 119 | "memflow-kvm": "memflow-kvm", 120 | "memflow-microvmi": "memflow-microvmi", 121 | "memflow-native": "memflow-native", 122 | "memflow-pcileech": "memflow-pcileech", 123 | "memflow-qemu": "memflow-qemu", 124 | "memflow-win32": "memflow-win32", 125 | "nixpkgs": [ 126 | "nixpkgs" 127 | ], 128 | "reflow": "reflow", 129 | "rust-overlay": "rust-overlay", 130 | "scanflow": "scanflow" 131 | }, 132 | "locked": { 133 | "lastModified": 1649275282, 134 | "narHash": "sha256-q8F+PDZqH78roCEYtdd+l1HOmpU7LFxgBfOaP5tIC9k=", 135 | "owner": "memflow", 136 | "repo": "memflow-nixos", 137 | "rev": "94e992a15146d3f0b50d8607d59577c6ce1d6d91", 138 | "type": "github" 139 | }, 140 | "original": { 141 | "owner": "memflow", 142 | "repo": "memflow-nixos", 143 | "type": "github" 144 | } 145 | }, 146 | "memflow-coredump": { 147 | "flake": false, 148 | "locked": { 149 | "lastModified": 1647802933, 150 | "narHash": "sha256-ta4P5eovDsiSQ301LtVrI14bkN47UmeyunCBijCOV54=", 151 | "owner": "memflow", 152 | "repo": "memflow-coredump", 153 | "rev": "e52988d112c3bed3e450f83451fa700b60388673", 154 | "type": "github" 155 | }, 156 | "original": { 157 | "owner": "memflow", 158 | "repo": "memflow-coredump", 159 | "type": "github" 160 | } 161 | }, 162 | "memflow-kcore": { 163 | "flake": false, 164 | "locked": { 165 | "lastModified": 1647803123, 166 | "narHash": "sha256-gwRCXjZm7EUJtdLnzB3wYeZFNqdaX6SUY9fdPES9Zo0=", 167 | "owner": "memflow", 168 | "repo": "memflow-kcore", 169 | "rev": "15fe4dcb2b70affe5a6d24f538cbd4d8beb6299b", 170 | "type": "github" 171 | }, 172 | "original": { 173 | "owner": "memflow", 174 | "repo": "memflow-kcore", 175 | "type": "github" 176 | } 177 | }, 178 | "memflow-kvm": { 179 | "flake": false, 180 | "locked": { 181 | "lastModified": 1649272211, 182 | "narHash": "sha256-/wSEi9M3eO+Kkc5HxZwsKWAtZeKzQtg23WEGTOmiABo=", 183 | "ref": "main", 184 | "rev": "61c8bdb097ce629bfdafdb0b2f6173a521756712", 185 | "revCount": 89, 186 | "submodules": true, 187 | "type": "git", 188 | "url": "https://github.com/memflow/memflow-kvm.git" 189 | }, 190 | "original": { 191 | "ref": "main", 192 | "submodules": true, 193 | "type": "git", 194 | "url": "https://github.com/memflow/memflow-kvm.git" 195 | } 196 | }, 197 | "memflow-microvmi": { 198 | "flake": false, 199 | "locked": { 200 | "lastModified": 1606515465, 201 | "narHash": "sha256-6UW9oN+PJ0fbGPJcP5UE8QXLReudTFoITtON5ATtT4s=", 202 | "owner": "memflow", 203 | "repo": "memflow-microvmi", 204 | "rev": "11ad6ea4b6095a060cdbf256eb0dbe2a7e6df495", 205 | "type": "github" 206 | }, 207 | "original": { 208 | "owner": "memflow", 209 | "repo": "memflow-microvmi", 210 | "type": "github" 211 | } 212 | }, 213 | "memflow-native": { 214 | "flake": false, 215 | "locked": { 216 | "lastModified": 1647802846, 217 | "narHash": "sha256-9VTvU3107o7fbY6g+fQFtYauS4Y3QpzH3B6yGYW6Qo4=", 218 | "owner": "memflow", 219 | "repo": "memflow-native", 220 | "rev": "a9410c0873fff96e824a901116cd344817a5b82e", 221 | "type": "github" 222 | }, 223 | "original": { 224 | "owner": "memflow", 225 | "repo": "memflow-native", 226 | "type": "github" 227 | } 228 | }, 229 | "memflow-pcileech": { 230 | "flake": false, 231 | "locked": { 232 | "lastModified": 1647804687, 233 | "narHash": "sha256-dvJLl4O5VWDzYX+P1dEGs20Qv0BqDQdtb4wFqtq2BMI=", 234 | "ref": "main", 235 | "rev": "9e35ddf1354f87d598e80aa92a4c1f91e4ebe28a", 236 | "revCount": 97, 237 | "submodules": true, 238 | "type": "git", 239 | "url": "https://github.com/memflow/memflow-pcileech.git" 240 | }, 241 | "original": { 242 | "ref": "main", 243 | "submodules": true, 244 | "type": "git", 245 | "url": "https://github.com/memflow/memflow-pcileech.git" 246 | } 247 | }, 248 | "memflow-qemu": { 249 | "flake": false, 250 | "locked": { 251 | "lastModified": 1649270250, 252 | "narHash": "sha256-/LDRFp4Sj/xeySJsp5LN6zgxQUsf37p20e3vTjXr/Sw=", 253 | "owner": "memflow", 254 | "repo": "memflow-qemu", 255 | "rev": "6ddd3f982ff7120897da598b644b40836450ec77", 256 | "type": "github" 257 | }, 258 | "original": { 259 | "owner": "memflow", 260 | "repo": "memflow-qemu", 261 | "type": "github" 262 | } 263 | }, 264 | "memflow-win32": { 265 | "flake": false, 266 | "locked": { 267 | "lastModified": 1648293668, 268 | "narHash": "sha256-N9Uu0po0wybPWuE4EOORQIOs0UUwJriTFE1kD16jJ4s=", 269 | "owner": "memflow", 270 | "repo": "memflow-win32", 271 | "rev": "683f19c62a0e112766922634b28e5b8f91838b50", 272 | "type": "github" 273 | }, 274 | "original": { 275 | "owner": "memflow", 276 | "repo": "memflow-win32", 277 | "type": "github" 278 | } 279 | }, 280 | "memflow_2": { 281 | "flake": false, 282 | "locked": { 283 | "lastModified": 1648294592, 284 | "narHash": "sha256-rCTg0/945qGfqkgiMhBdgTTraM2GX+i3SSSQqgwff3A=", 285 | "owner": "memflow", 286 | "repo": "memflow", 287 | "rev": "db068696b2551d384b7ea9f1a22671b7c6c805a5", 288 | "type": "github" 289 | }, 290 | "original": { 291 | "owner": "memflow", 292 | "repo": "memflow", 293 | "type": "github" 294 | } 295 | }, 296 | "nixpkgs": { 297 | "locked": { 298 | "lastModified": 1637453606, 299 | "narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=", 300 | "owner": "NixOS", 301 | "repo": "nixpkgs", 302 | "rev": "8afc4e543663ca0a6a4f496262cd05233737e732", 303 | "type": "github" 304 | }, 305 | "original": { 306 | "owner": "NixOS", 307 | "ref": "nixpkgs-unstable", 308 | "repo": "nixpkgs", 309 | "type": "github" 310 | } 311 | }, 312 | "nixpkgs_2": { 313 | "locked": { 314 | "lastModified": 1648219316, 315 | "narHash": "sha256-Ctij+dOi0ZZIfX5eMhgwugfvB+WZSrvVNAyAuANOsnQ=", 316 | "owner": "nixos", 317 | "repo": "nixpkgs", 318 | "rev": "30d3d79b7d3607d56546dd2a6b49e156ba0ec634", 319 | "type": "github" 320 | }, 321 | "original": { 322 | "owner": "nixos", 323 | "ref": "nixpkgs-unstable", 324 | "repo": "nixpkgs", 325 | "type": "github" 326 | } 327 | }, 328 | "reflow": { 329 | "flake": false, 330 | "locked": { 331 | "lastModified": 1646489083, 332 | "narHash": "sha256-LIH+LKiObfdp77deeialEGUJSQgxf9TTdzDc4QvAk+Q=", 333 | "owner": "memflow", 334 | "repo": "reflow", 335 | "rev": "d741faa74776fc100bc03a358a2e3024fa9898a6", 336 | "type": "github" 337 | }, 338 | "original": { 339 | "owner": "memflow", 340 | "repo": "reflow", 341 | "type": "github" 342 | } 343 | }, 344 | "root": { 345 | "inputs": { 346 | "flake-compat": "flake-compat", 347 | "flake-utils": "flake-utils", 348 | "memflow": "memflow", 349 | "nixpkgs": "nixpkgs_2", 350 | "zig-args": "zig-args", 351 | "zig-clap": "zig-clap", 352 | "zig-overlay": "zig-overlay", 353 | "zig-win32": "zig-win32" 354 | } 355 | }, 356 | "rust-overlay": { 357 | "inputs": { 358 | "flake-utils": "flake-utils_3", 359 | "nixpkgs": "nixpkgs" 360 | }, 361 | "locked": { 362 | "lastModified": 1649644592, 363 | "narHash": "sha256-q4vQ54pmCC+b5+bdsH2/90jIeiydTAJhF2IvP13r3yA=", 364 | "owner": "oxalica", 365 | "repo": "rust-overlay", 366 | "rev": "4770c6c6cb518d2c5b660c5099816c3512d8bdad", 367 | "type": "github" 368 | }, 369 | "original": { 370 | "owner": "oxalica", 371 | "repo": "rust-overlay", 372 | "type": "github" 373 | } 374 | }, 375 | "scanflow": { 376 | "flake": false, 377 | "locked": { 378 | "lastModified": 1647989733, 379 | "narHash": "sha256-/MfW4rV/Xm+Av2s63gDhVb4EJuhqV2/FZ3Il5lUsdLs=", 380 | "owner": "memflow", 381 | "repo": "scanflow", 382 | "rev": "2635eda7275887fac612e67440fc077051b3d018", 383 | "type": "github" 384 | }, 385 | "original": { 386 | "owner": "memflow", 387 | "repo": "scanflow", 388 | "type": "github" 389 | } 390 | }, 391 | "zig-args": { 392 | "flake": false, 393 | "locked": { 394 | "lastModified": 1647017273, 395 | "narHash": "sha256-nxEF5DvXfa9RB9s0euPam7YIyAJy+Jkkrfz897Pl59s=", 396 | "owner": "MasterQ32", 397 | "repo": "zig-args", 398 | "rev": "1ff417ac1f31f8dbee3a31e5973b46286d42e71d", 399 | "type": "github" 400 | }, 401 | "original": { 402 | "owner": "MasterQ32", 403 | "repo": "zig-args", 404 | "rev": "1ff417ac1f31f8dbee3a31e5973b46286d42e71d", 405 | "type": "github" 406 | } 407 | }, 408 | "zig-clap": { 409 | "flake": false, 410 | "locked": { 411 | "lastModified": 1648116501, 412 | "narHash": "sha256-KyRBsE9F2QB1ZmenHOpgro1a+B9m6Rtzmj0Q5z+niW0=", 413 | "owner": "Hejsil", 414 | "repo": "zig-clap", 415 | "rev": "0970eb827fe53ad7a6c6744019707190d7b9bb32", 416 | "type": "github" 417 | }, 418 | "original": { 419 | "owner": "Hejsil", 420 | "repo": "zig-clap", 421 | "rev": "0970eb827fe53ad7a6c6744019707190d7b9bb32", 422 | "type": "github" 423 | } 424 | }, 425 | "zig-overlay": { 426 | "inputs": { 427 | "flake-utils": "flake-utils_4", 428 | "nixpkgs": [ 429 | "nixpkgs" 430 | ] 431 | }, 432 | "locked": { 433 | "lastModified": 1649551160, 434 | "narHash": "sha256-PufGYax8aalIW+BilcDs5r2pcdI66zp4xSkrW8+XIFc=", 435 | "owner": "arqv", 436 | "repo": "zig-overlay", 437 | "rev": "0807921f6e6a92325d99943dd0f62f8a967395c2", 438 | "type": "github" 439 | }, 440 | "original": { 441 | "owner": "arqv", 442 | "repo": "zig-overlay", 443 | "type": "github" 444 | } 445 | }, 446 | "zig-win32": { 447 | "flake": false, 448 | "locked": { 449 | "lastModified": 1643012832, 450 | "narHash": "sha256-KSNpWOwCC1j4wOnvf0vyDsvbNStDtJLSSRr5SCxg0A0=", 451 | "owner": "marlersoft", 452 | "repo": "zigwin32", 453 | "rev": "032a1b51b83b8fe64e0a97d7fe5da802065244c6", 454 | "type": "github" 455 | }, 456 | "original": { 457 | "owner": "marlersoft", 458 | "repo": "zigwin32", 459 | "rev": "032a1b51b83b8fe64e0a97d7fe5da802065244c6", 460 | "type": "github" 461 | } 462 | } 463 | }, 464 | "root": "root", 465 | "version": 7 466 | } 467 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Shellcode execution capabilities with memflow"; 3 | 4 | inputs = { 5 | nixpkgs.url = github:nixos/nixpkgs/nixpkgs-unstable; 6 | memflow = { 7 | url = github:memflow/memflow-nixos; 8 | inputs = { 9 | nixpkgs.follows = "nixpkgs"; 10 | rust-overlay = { 11 | url = github:oxalica/rust-overlay; 12 | inputs.nixpkgs.follows = "nixpkgs"; 13 | }; 14 | }; 15 | }; 16 | zig-overlay = { 17 | url = github:arqv/zig-overlay; 18 | inputs.nixpkgs.follows = "nixpkgs"; 19 | }; 20 | flake-utils.url = github:numtide/flake-utils; 21 | flake-compat = { 22 | url = github:edolstra/flake-compat; 23 | flake = false; 24 | }; 25 | zig-win32 = { 26 | url = github:marlersoft/zigwin32/032a1b51b83b8fe64e0a97d7fe5da802065244c6; 27 | flake = false; 28 | }; 29 | zig-clap = { 30 | flake = false; 31 | url = github:Hejsil/zig-clap/0970eb827fe53ad7a6c6744019707190d7b9bb32; 32 | }; 33 | zig-args = { 34 | flake = false; 35 | url = github:MasterQ32/zig-args/1ff417ac1f31f8dbee3a31e5973b46286d42e71d; 36 | }; 37 | }; 38 | 39 | nixConfig = { 40 | extra-substituters = [ https://memflow.cachix.org ]; 41 | extra-trusted-public-keys = [ memflow.cachix.org-1:t4ufU/+o8xtYpZQc9/AyzII/sohwMKGYNIMgT56CgXA= ]; 42 | }; 43 | 44 | outputs = { self, nixpkgs, flake-utils, zig-overlay, ... } @ inputs: 45 | flake-utils.lib.eachSystem [ "x86_64-linux" ] (system: 46 | let 47 | pkgs = nixpkgs.legacyPackages.${system}; 48 | lib = pkgs.lib; 49 | memflowPkgs = builtins.mapAttrs 50 | (name: package: 51 | (package.overrideAttrs 52 | (super: { 53 | # dontStrip = true; 54 | # buildType = "debug"; 55 | }) 56 | ) 57 | ) 58 | inputs.memflow.packages.${system}; 59 | in 60 | { 61 | packages = { 62 | memflow-shell = pkgs.stdenv.mkDerivation { 63 | name = "memflow-shell"; 64 | 65 | nativeBuildInputs = with pkgs; with memflowPkgs; [ 66 | zig-overlay.packages.${system}.master.latest 67 | pkg-config 68 | memflow 69 | # glibc 70 | ]; 71 | 72 | src = ./.; 73 | 74 | dontInstall = true; 75 | 76 | postUnpack = '' 77 | rm -rf $sourceRoot/libs/ 78 | mkdir -vp $sourceRoot/libs/{zigwin32,zig-clap,zig-args}/ 79 | cp -a ${inputs.zig-win32}/* $sourceRoot/libs/zigwin32/ 80 | cp -a ${inputs.zig-clap}/* $sourceRoot/libs/zig-clap/ 81 | cp -a ${inputs.zig-args}/* $sourceRoot/libs/zig-args/ 82 | chmod a+r+w $sourceRoot/libs/* 83 | ''; 84 | buildPhase = '' 85 | # Set Zig global cache directory 86 | export XDG_CACHE_HOME="$TMPDIR/zig-cache/" 87 | zig build install --prefix $out 88 | ''; 89 | 90 | meta = { }; 91 | }; 92 | default = self.packages.${system}.memflow-shell; 93 | }; 94 | defaultPackage = self.packages.${system}.default; 95 | } 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/load.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const mf = @import("./memflow.zig"); 4 | 5 | const logger = @import("./main.zig").logger; 6 | 7 | pub fn load( 8 | allocator: std.mem.Allocator, 9 | os_instance: *mf.OsInstance, 10 | process_name: []const u8, 11 | dll_path: []const u8, 12 | ) !void { 13 | logger.info("Using DLL at path \"{s}\"", .{dll_path}); 14 | 15 | const dll_file: std.fs.File = try std.fs.cwd().openFile(dll_path, .{}); 16 | defer dll_file.close(); 17 | 18 | const dll_data = try dll_file.readToEndAlloc(allocator, 0x1000000); 19 | defer allocator.free(dll_data); 20 | 21 | logger.info("DLL is {} bytes in size", .{dll_data.len}); 22 | 23 | var target_process_instance: mf.ProcessInstance = undefined; 24 | 25 | // Search for the target process name 26 | try mf.tryError( 27 | mf.mf_osinstance_process_by_name(os_instance, mf.slice(process_name), &target_process_instance), 28 | mf.MemflowError.ProcessNameLookupFailed, 29 | ); 30 | 31 | const target_process_info = mf.mf_processinstance_info(&target_process_instance) orelse { 32 | return mf.MemflowError.ProcessInfoLookupFailed; 33 | }; 34 | logger.info("Found target process as PID {}", .{target_process_info.*.pid}); 35 | } 36 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const logger = std.log.scoped(.@"memflow-shell"); 4 | pub const log_level: std.log.Level = .debug; 5 | 6 | const @"args-parser" = @import("args"); 7 | 8 | const mf = @import("./memflow.zig"); 9 | 10 | const load = @import("./load.zig").load; 11 | const run = @import("./run.zig").run; 12 | 13 | pub fn main() !u8 { 14 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 15 | const allocator = gpa.allocator(); 16 | defer _ = gpa.deinit(); 17 | 18 | // Memflow singletons 19 | var connector_inventory: *mf.Inventory = undefined; 20 | var connector_instance: mf.ConnectorInstance = undefined; 21 | var os_instance: mf.OsInstance = undefined; 22 | 23 | const Subcommand = union(enum) { 24 | load: struct { 25 | process: ?[]const u8 = null, 26 | dll: ?[]const u8 = null, 27 | 28 | pub const shorthands = .{ 29 | .p = "process", 30 | .d = "dll", 31 | }; 32 | }, 33 | run: struct { 34 | exe: ?[]const u8 = null, 35 | 36 | pub const shorthands = .{ 37 | .e = "exe", 38 | }; 39 | }, 40 | }; 41 | 42 | const options = @"args-parser".parseWithVerbForCurrentProcess( 43 | struct {}, 44 | Subcommand, 45 | allocator, 46 | .print, 47 | ) catch return 1; 48 | defer options.deinit(); 49 | 50 | // Initialize memflow logging 51 | mf.log_init(mf.Level_Info); 52 | 53 | // Create a connector inventory by scanning default and compiled-in paths 54 | // If it returns a null pointer treat this as an error in being unable to scan inventory paths 55 | connector_inventory = mf.inventory_scan() orelse return mf.MemflowError.InventoryScanFailed; 56 | 57 | // Create a new memflow connector instance from the current inventory of plugins (using KVM) 58 | try mf.tryError( 59 | mf.inventory_create_connector(connector_inventory, "kvm", "", &connector_instance), 60 | mf.MemflowError.InventoryCreateConnectorError, 61 | ); 62 | // Now using the KVM connector instance create an OS instance (using win32) 63 | try mf.tryError( 64 | mf.inventory_create_os(connector_inventory, "win32", "", &connector_instance, &os_instance), 65 | mf.MemflowError.InventoryCreateOSFailed, 66 | ); 67 | 68 | var subcommand: Subcommand = undefined; 69 | 70 | if (options.verb) |verb| { 71 | subcommand = verb; 72 | } else { 73 | logger.err("Subcommand required", .{}); 74 | return 1; 75 | } 76 | 77 | switch (subcommand) { 78 | // Forcefully load kernel driver or DLL 79 | .load => |opts| { 80 | var target_process_name: []const u8 = undefined; 81 | var injection_dll_path: []const u8 = undefined; 82 | 83 | if (opts.process) |process_name| { 84 | target_process_name = process_name; 85 | } else { 86 | logger.err("-p/--process is required", .{}); 87 | return 1; 88 | } 89 | if (opts.dll) |dll_path| { 90 | injection_dll_path = dll_path; 91 | } else { 92 | logger.err("-d/--dll is required", .{}); 93 | return 1; 94 | } 95 | 96 | load(allocator, &os_instance, target_process_name, injection_dll_path) catch |err| { 97 | if (err == mf.MemflowError.ProcessNameLookupFailed) { 98 | logger.err("Unable to find target injection process with name \"{s}\". " ++ 99 | "Are you sure it's running?", .{target_process_name}); 100 | return 1; 101 | } 102 | 103 | return err; 104 | }; 105 | }, 106 | // Run usermode executable process 107 | .run => |opts| { 108 | var exe_path: []const u8 = undefined; 109 | if (opts.exe) |opt_exe| { 110 | exe_path = opt_exe; 111 | } else { 112 | logger.err("-e/--exe is required", .{}); 113 | return 1; 114 | } 115 | 116 | try run(allocator, &os_instance, exe_path); 117 | }, 118 | } 119 | 120 | return 0; 121 | } 122 | -------------------------------------------------------------------------------- /src/memflow.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub usingnamespace @cImport({ 4 | @cInclude("memflow.h"); 5 | }); 6 | 7 | const logger = std.log.scoped(.memflow); 8 | 9 | const memflow = @This(); 10 | 11 | pub const MemflowError = error{ 12 | InventoryCreateConnectorError, 13 | InventoryCreateOSFailed, 14 | InventoryScanFailed, 15 | /// "mf_osinstance_process_by_name" function failure 16 | ProcessNameLookupFailed, 17 | /// "mf_processinstance_info" function failure 18 | ProcessInfoLookupFailed, 19 | /// "mf_osinstance_read_raw_into" function failure 20 | UnknownReadError, 21 | WriteRawError, 22 | }; 23 | 24 | /// Convert Zig character slice to a memflow FFI character slice 25 | pub fn slice(s: []const u8) memflow.CSliceRef_u8 { 26 | return .{ .data = s.ptr, .len = s.len }; 27 | } 28 | 29 | /// Wrap memflow error code as a Zig error type and log error 30 | pub fn tryErrorLog(error_number: i32, @"error": ?anyerror, print: ?bool) !void { 31 | const should_print_log_err = print orelse true; 32 | 33 | if (error_number != 0) { 34 | if (should_print_log_err) 35 | memflow.log_errorcode(memflow.Level_Error, error_number); 36 | 37 | if (@"error") |err| 38 | return err; 39 | } 40 | } 41 | 42 | /// Wrap memflow error code as a Zig error type 43 | pub fn tryError(error_number: i32, @"error": ?anyerror) !void { 44 | return try tryErrorLog(error_number, @"error", null); 45 | } 46 | 47 | /// Single byte token with either a specified or unspecified value 48 | pub const ByteToken = union(enum) { 49 | /// Byte with a defined value 50 | byte: u8, // 0xAD 51 | /// Byte of any value 52 | wildcard, // "??" 53 | 54 | pub fn format( 55 | value: *const @This(), 56 | comptime fmt: []const u8, 57 | options: std.fmt.FormatOptions, 58 | writer: anytype, 59 | ) !void { 60 | _ = fmt; 61 | _ = options; 62 | 63 | return switch (value.*) { 64 | .byte => |token| std.fmt.format(writer, "0x{X:0>2}", .{token}), 65 | .wildcard => std.fmt.format(writer, "??", .{}), 66 | }; 67 | } 68 | }; 69 | 70 | /// Convert a tuple of ByteTokens values into a slice of ByteTokens 71 | pub fn byteSequence(comptime values: anytype) [@typeInfo(@TypeOf(values)).Struct.fields.len]ByteToken { 72 | const ArgsType = @TypeOf(values); 73 | const args_type_info = @typeInfo(ArgsType); 74 | if (args_type_info != .Struct) { 75 | @compileError("Expected tuple, found " ++ @typeName(ArgsType)); 76 | } 77 | 78 | const max_format_args = 64; 79 | const fields_info = args_type_info.Struct.fields; 80 | if (fields_info.len < 1) { 81 | @compileError("Byte patterns have a minimum length of 1"); 82 | } else if (fields_info.len > max_format_args) { 83 | @compileError("Byte patterns have a maximum length of" ++ std.fmt.comptimePrint("{}", .{max_format_args})); 84 | } 85 | 86 | // Array of byte tokens to populate and return as a slice eventually 87 | var tokens: [fields_info.len]ByteToken = undefined; 88 | inline for (fields_info) |field_info, index| { 89 | tokens[index] = switch (field_info.field_type) { 90 | comptime_int => .{ .byte = @field(values, field_info.name) }, 91 | void => .{ .wildcard = {} }, 92 | else => @compileError("Can't use field of type " ++ @typeName(ArgsType) ++ " in byte pattern"), 93 | }; 94 | } 95 | 96 | return tokens; 97 | } 98 | 99 | /// Scan kernel module for pattern 100 | pub fn scanOSModuleForPattern( 101 | os_instance: *memflow.OsInstance, 102 | module_info: *memflow.ModuleInfo, 103 | search_sequence: []const ByteToken, 104 | ) !?usize { 105 | const module_end = module_info.base + module_info.size; 106 | logger.debug("Scanning module \"{s}\" with size of {} bytes (0x{X}-0x{X}) for pattern {any}", .{ 107 | module_info.name, 108 | module_info.size, 109 | module_info.base, 110 | module_end, 111 | search_sequence, 112 | }); 113 | 114 | const allocator: std.mem.Allocator = std.heap.page_allocator; 115 | 116 | // Allocate a byte array that's the size of the module being scanned 117 | var module_memory: []u8 = try allocator.alloc(u8, module_info.size); 118 | defer allocator.free(module_memory); 119 | 120 | // Read as much of the module's memory as possible (ignoring errors) into the allocated buffer 121 | try readOSRawInto(module_memory.ptr, module_memory.len, os_instance, module_info.base); 122 | 123 | var current_index: usize = 0; 124 | const address_alignment = 1; // TODO: make this a function parameter 125 | var match_offset: ?usize = null; 126 | 127 | // Search between the start and end of the copy of the module's memory 128 | outter: while (current_index < module_info.size - search_sequence.len) : (current_index += address_alignment) { 129 | for (search_sequence) |expected_byte, seq_index| { 130 | const current_byte = module_memory[current_index + seq_index]; 131 | 132 | switch (expected_byte) { 133 | .byte => { 134 | if ((ByteToken{ .byte = current_byte }).byte != expected_byte.byte) { 135 | // Doesn't match, advance outter index by address_alignment and try again 136 | break; 137 | } 138 | }, 139 | .wildcard => {}, 140 | } 141 | 142 | // If all bytes matched all tokens at the end of looping 143 | if (seq_index == search_sequence.len - 1) { 144 | match_offset = current_index; 145 | logger.debug("Found a match", .{}); 146 | break :outter; 147 | } 148 | } 149 | } 150 | 151 | return if (match_offset) |offset| module_info.base + offset else match_offset; 152 | } 153 | 154 | pub fn readOSRawInto( 155 | object: anytype, 156 | size: ?usize, 157 | os_instance: *memflow.OsInstance, 158 | virtual_address: usize, 159 | ) !void { 160 | const read_size = if (size) |s| s else @sizeOf(@typeInfo(@TypeOf(object)).Pointer.child); 161 | 162 | const read_status = memflow.mf_osinstance_read_raw_into( 163 | os_instance, 164 | virtual_address, 165 | .{ .data = @ptrCast([*c]u8, object), .len = read_size }, 166 | ); 167 | 168 | return switch (read_status) { 169 | 0 => {}, 170 | // TODO: add different read failures 171 | else => memflow.MemflowError.UnknownReadError, 172 | }; 173 | } 174 | 175 | /// Wrapper around mf_osinstance_write_raw 176 | pub fn writeOSRaw(object: anytype, os_instance: *memflow.OsInstance, virtual_address: usize) !void { 177 | const pointer_type = @typeInfo(@TypeOf(object)).Pointer; 178 | 179 | const read_size = switch (pointer_type.size) { 180 | .One => @sizeOf(pointer_type.child), 181 | .Slice => @sizeOf(pointer_type.child) * object.len, 182 | else => unreachable, 183 | }; 184 | 185 | try memflow.tryErrorLog( 186 | memflow.mf_osinstance_write_raw( 187 | os_instance, 188 | virtual_address, 189 | .{ .data = @ptrCast([*c]u8, object), .len = read_size }, 190 | ), 191 | memflow.MemflowError.WriteRawError, 192 | null, 193 | ); 194 | } 195 | 196 | /// Wrapper for memflow read_raw_into 197 | pub fn readRawInto( 198 | object: anytype, 199 | process_instance: *memflow.ProcessInstance, 200 | virtual_address: usize, 201 | ) !void { 202 | const read_size = @sizeOf(@typeInfo(@TypeOf(object)).Pointer.child); 203 | 204 | try memflow.tryErrorLog( 205 | memflow.mf_processinstance_read_raw_into( 206 | process_instance, 207 | virtual_address, 208 | .{ .data = @ptrCast([*c]u8, object), .len = read_size }, 209 | ), 210 | error.MemflowProcessInstanceReadRawIntoError, 211 | null, 212 | ); 213 | } 214 | 215 | /// Wrapper for memflow write_raw 216 | pub fn writeRaw( 217 | object: anytype, 218 | process_instance: *memflow.ProcessInstance, 219 | virtual_address: usize, 220 | ) !void { 221 | const write_size = @sizeOf(@typeInfo(@TypeOf(object)).Pointer.child); 222 | 223 | try memflow.tryErrorLog( 224 | memflow.mf_processinstance_write_raw( 225 | process_instance, 226 | virtual_address, 227 | .{ .data = @ptrCast([*c]u8, object), .len = write_size }, 228 | ), 229 | memflow.MemflowError.WriteRawError, 230 | true, 231 | ); 232 | } 233 | 234 | pub fn writeShellcode( 235 | os_instance: *memflow.OsInstance, 236 | virtual_addr: usize, 237 | label_start: fn () callconv(.Naked) noreturn, 238 | label_end: fn () callconv(.Naked) noreturn, 239 | ) !void { 240 | _ = virtual_addr; 241 | // TODO: assert label end is greater than label start 242 | 243 | const shellcode_start_addr = @ptrToInt(label_start); 244 | const shellcode_len = @ptrToInt(label_end) - shellcode_start_addr; 245 | 246 | var shellcode = @intToPtr([*]u8, shellcode_start_addr)[0..shellcode_len]; 247 | 248 | try writeOSRaw(shellcode, os_instance, virtual_addr); 249 | } 250 | -------------------------------------------------------------------------------- /src/run.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const mf = @import("./memflow.zig"); 4 | 5 | const logger = @import("./main.zig").logger; 6 | const shellcode = @import("./shellcode.zig"); 7 | 8 | pub fn run(allocator: std.mem.Allocator, os_instance: *mf.OsInstance, exe_path: []const u8) !void { 9 | logger.info("Using executable at path \"{s}\"", .{exe_path}); 10 | 11 | const exe_file: std.fs.File = try std.fs.cwd().openFile(exe_path, .{}); 12 | defer exe_file.close(); 13 | 14 | const exe_data = try exe_file.readToEndAlloc(allocator, 0x1000000); 15 | defer allocator.free(exe_data); 16 | 17 | logger.info("Executable is {} bytes in size", .{exe_data.len}); 18 | 19 | var nt_kernel_image_info: mf.ModuleInfo = undefined; 20 | // Search for kernel module 21 | try mf.tryError( 22 | mf.mf_osinstance_primary_module( 23 | os_instance, 24 | &nt_kernel_image_info, 25 | ), 26 | error.MemflowOSIntanceModuleByNameError, 27 | ); 28 | 29 | logger.info( 30 | "Kernel image starts: 0x{X} & ends: 0x{X}", 31 | .{ 32 | nt_kernel_image_info.base, 33 | nt_kernel_image_info.base + nt_kernel_image_info.size, 34 | }, 35 | ); 36 | 37 | const ExportCallbackContext = struct { 38 | symbol_offset: ?usize = null, 39 | }; 40 | var export_list_context = ExportCallbackContext{}; 41 | 42 | logger.debug("Enumerating NT kernel image (\"{s}\") exports:", .{nt_kernel_image_info.name}); 43 | try mf.tryError( 44 | mf.mf_osinstance_module_export_list_callback( 45 | os_instance, 46 | &nt_kernel_image_info, 47 | .{ 48 | .context = &export_list_context, 49 | .func = struct { 50 | fn _(context: ?*anyopaque, export_info: mf.ExportInfo) callconv(.C) bool { 51 | logger.debug("Export: \"{s}\"", .{export_info.name}); 52 | 53 | var callback_context = @ptrCast(*ExportCallbackContext, @alignCast( 54 | @alignOf(*ExportCallbackContext), 55 | context, 56 | )); 57 | 58 | if (std.mem.eql(u8, std.mem.span(export_info.name), "memset")) { 59 | logger.info( 60 | "Found nt!memset offset for stage 1 hook placement as 0x{X}", 61 | .{export_info.offset}, 62 | ); 63 | callback_context.symbol_offset = export_info.offset; 64 | return false; 65 | } 66 | 67 | return true; 68 | } 69 | }._, 70 | }, 71 | ), 72 | error.MemflowOSInstanceModuleExportListCallbackError, 73 | ); 74 | logger.debug("Export enumeration complete", .{}); 75 | 76 | var stage_1_addr: usize = undefined; 77 | 78 | if (export_list_context.symbol_offset) |export_addr| { 79 | stage_1_addr = nt_kernel_image_info.base + export_addr; 80 | } else { 81 | logger.warn("Unable to find stage 1 hook location through symbol export enumeration, " ++ 82 | "resorting to pattern scanning...", .{}); 83 | 84 | if (try mf.scanOSModuleForPattern( 85 | os_instance, 86 | &nt_kernel_image_info, 87 | &comptime mf.byteSequence(.{ 88 | // x /0 nt!memset: 48 8B C1 0F B6 D2 49 B9 01 01 01 01 01 01 01 01 89 | 0x48, 0x8B, 0xC1, 0x0F, 0xB6, 0xD2, 0x49, 0xB9, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 90 | 0x01, 0x01, 91 | }), 92 | )) |pattern_addr| { 93 | stage_1_addr = pattern_addr; 94 | } else { 95 | return error.Stage1LocationError; 96 | } 97 | } 98 | 99 | logger.info("Stage 1 placement address is 0x{X}", .{stage_1_addr}); 100 | // TODO: Programmatically determine unused .text page area 101 | const stage_2_addr: usize = nt_kernel_image_info.base + 0x652BED; 102 | logger.info("Stage 2 placement address is 0x{X}", .{stage_2_addr}); 103 | 104 | try mf.writeShellcode(os_instance, stage_2_addr, shellcode.stage_2, shellcode._stage_2); 105 | try mf.writeShellcode( 106 | os_instance, 107 | stage_1_addr, 108 | shellcode.kernel_trampoline, 109 | shellcode._kernel_trampoline, 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /src/shellcode.zig: -------------------------------------------------------------------------------- 1 | comptime { 2 | asm ( 3 | \\kernel_trampoline: 4 | \\ call .+0x2291a8 5 | \\ nop 6 | \\_kernel_trampoline: 7 | ); 8 | } 9 | pub extern fn kernel_trampoline() callconv(.Naked) noreturn; 10 | pub extern fn _kernel_trampoline() callconv(.Naked) noreturn; 11 | 12 | comptime { 13 | asm ( 14 | \\stage_2: # nt+0x652bed 15 | \\ pushfq 16 | \\ # x64 pushad 17 | \\ # https://docs.microsoft.com/windows-hardware/drivers/debugger/x64-architecture#registers 18 | \\ pushq %rax 19 | \\ pushq %rbx 20 | \\ pushq %rcx 21 | \\ pushq %rdx 22 | \\ pushq %rsi 23 | \\ pushq %rdi 24 | \\ pushq %rbp 25 | \\ pushq %rsp 26 | \\ pushq %r8 27 | \\ pushq %r9 28 | \\ pushq %r10 29 | \\ pushq %r11 30 | \\ pushq %r12 31 | \\ pushq %r13 32 | \\ pushq %r14 33 | \\ pushq %r15 34 | \\ 35 | \\ # TODO: allocate pages 36 | \\ 37 | \\ popq %r15 38 | \\ popq %r14 39 | \\ popq %r13 40 | \\ popq %r12 41 | \\ popq %r11 42 | \\ popq %r10 43 | \\ popq %r9 44 | \\ popq %r8 45 | \\ popq %rsp 46 | \\ popq %rbp 47 | \\ popq %rdi 48 | \\ popq %rsi 49 | \\ popq %rdx 50 | \\ popq %rcx 51 | \\ popq %rbx 52 | \\ popq %rax 53 | \\ popfq 54 | \\ 55 | \\start_reconcile_clobbers: 56 | \\ movq %rcx, %rax 57 | \\ movzx %dl, %edx 58 | \\end_reconcile_clobbers: 59 | \\ retq 60 | \\_stage_2: 61 | ); 62 | } 63 | pub extern fn stage_2() callconv(.Naked) noreturn; 64 | pub extern fn _stage_2() callconv(.Naked) noreturn; 65 | 66 | comptime { 67 | asm ( 68 | \\blue_screen: 69 | \\ nop 70 | \\_blue_screen: 71 | ); 72 | } 73 | pub extern fn blue_screen() callconv(.Naked) noreturn; 74 | pub extern fn _blue_screen() callconv(.Naked) noreturn; 75 | -------------------------------------------------------------------------------- /src/test_dll.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const win32 = @import("win32").everything; 4 | 5 | fn main() void { 6 | _ = win32.AllocConsole(); 7 | 8 | std.debug.print("Hello world\n", .{}); 9 | } 10 | 11 | pub export fn DllMain( 12 | handle_instance: ?*anyopaque, 13 | reason: c_ulong, 14 | reserved: ?*anyopaque, 15 | ) callconv(.C) c_int { 16 | _ = handle_instance; 17 | _ = reason; 18 | _ = reserved; 19 | 20 | switch (reason) { 21 | win32.DLL_PROCESS_ATTACH => { 22 | _ = std.Thread.spawn(.{}, main, .{}) catch unreachable; 23 | }, 24 | else => {}, 25 | } 26 | 27 | return 1; 28 | } 29 | -------------------------------------------------------------------------------- /src/test_exe.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn main() !void { 4 | std.debug.print( 5 | \\Hello world! 6 | \\ 7 | \\Press enter to exit... 8 | \\ 9 | , .{}); 10 | 11 | _ = try std.io.getStdIn().reader().readByte(); 12 | } 13 | --------------------------------------------------------------------------------