pylancelot: add justfile

pylancelot: add devshell and flake
This commit is contained in:
Willi Ballenthin 2024-09-27 08:55:51 +00:00
parent 3571776891
commit 4961b3c553
19 changed files with 236 additions and 51 deletions

View file

@ -1116,7 +1116,7 @@ mod tests {
insns.build_index(&module, 0x0)?;
let mut cfg = CFG::from_instructions(&module, insns)?;
let fallthrough = cfg.flows.flows_by_src[&0x1][0].clone();
let fallthrough = cfg.flows.flows_by_src[&0x1][0];
let mut batch: ChangeBatch = Default::default();
batch.prune_flow(0x1, fallthrough);
cfg.commit(batch);
@ -1165,9 +1165,9 @@ mod tests {
// cut second fallthrough, which should remove the edge,
// but not any of the instructions.
let fallthrough = fallthrough_edges(&cfg.flows.flows_by_src[&0x7]).next().unwrap().clone();
let fallthrough = fallthrough_edges(&cfg.flows.flows_by_src[&0x7]).next().unwrap();
let mut batch: ChangeBatch = Default::default();
batch.prune_flow(0x7, fallthrough);
batch.prune_flow(0x7, *fallthrough);
cfg.commit(batch);
// ┌─────────────────────────────────┐
@ -1198,9 +1198,9 @@ mod tests {
// cut the first fallthrough, which should remove the edge,
// and also the basic block at 0x7 (mov eax, 0x2).
let fallthrough = fallthrough_edges(&cfg.flows.flows_by_src[&0x5]).next().unwrap().clone();
let fallthrough = fallthrough_edges(&cfg.flows.flows_by_src[&0x5]).next().unwrap();
let mut batch: ChangeBatch = Default::default();
batch.prune_flow(0x5, fallthrough);
batch.prune_flow(0x5, *fallthrough);
cfg.commit(batch);
// ┌─────────────────────────────────┐

View file

@ -384,6 +384,7 @@ pub fn get_operand_xref(
#[cfg(test)]
mod tests {
use std::ops::Not;
use crate::{analysis::dis::*, rsrc::*, test::*};
#[test]
@ -398,9 +399,9 @@ mod tests {
let insn = read_insn(&pe.module, 0x1800134D4);
let op = get_first_operand(&insn).unwrap();
let xref = get_memory_operand_ptr(0x1800134D4, &insn, &op).unwrap();
let xref = get_memory_operand_ptr(0x1800134D4, &insn, op).unwrap();
assert_eq!(xref.is_some(), true);
assert!(xref.is_some());
assert_eq!(xref.unwrap(), 0x1800773F0);
}
@ -411,9 +412,9 @@ mod tests {
let module = load_shellcode32(b"\xFF\x25\x06\x00\x00\x00\x00\x00\x00\x00");
let insn = read_insn(&module, 0x0);
let op = get_first_operand(&insn).unwrap();
let xref = get_memory_operand_xref(&module, 0x0, &insn, &op).unwrap();
let xref = get_memory_operand_xref(&module, 0x0, &insn, op).unwrap();
assert_eq!(xref.is_some(), true);
assert!(xref.is_some());
assert_eq!(xref.unwrap(), 0x0);
}
@ -424,9 +425,9 @@ mod tests {
let module = load_shellcode64(b"\xFF\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00");
let insn = read_insn(&module, 0x0);
let op = get_first_operand(&insn).unwrap();
let xref = get_memory_operand_xref(&module, 0x0, &insn, &op).unwrap();
let xref = get_memory_operand_xref(&module, 0x0, &insn, op).unwrap();
assert_eq!(xref.is_some(), true);
assert!(xref.is_some());
assert_eq!(xref.unwrap(), 0x0);
}
@ -439,9 +440,9 @@ mod tests {
let module = load_shellcode32(b"\xEA\x00\x00\x00\x00\x00\x00");
let insn = read_insn(&module, 0x0);
let op = get_first_operand(&insn).unwrap();
let xref = get_pointer_operand_xref(&op).unwrap();
let xref = get_pointer_operand_xref(op).unwrap();
assert_eq!(xref.is_some(), true, "has pointer operand xref");
assert!(xref.is_some(), "has pointer operand xref");
assert_eq!(xref.unwrap(), 0x0, "correct pointer operand xref");
}
@ -452,9 +453,9 @@ mod tests {
let module = load_shellcode32(b"\xEB\xFE");
let insn = read_insn(&module, 0x0);
let op = get_first_operand(&insn).unwrap();
let xref = get_immediate_operand_xref(&module, 0x0, &insn, &op).unwrap();
let xref = get_immediate_operand_xref(&module, 0x0, &insn, op).unwrap();
assert_eq!(xref.is_some(), true, "has immediate operand");
assert!(xref.is_some(), "has immediate operand");
assert_eq!(xref.unwrap(), 0x0, "correct immediate operand");
// this is a jump from addr 0x0 to -1, which is unmapped
@ -462,9 +463,9 @@ mod tests {
let module = load_shellcode32(b"\xEB\xFD");
let insn = read_insn(&module, 0x0);
let op = get_first_operand(&insn).unwrap();
let xref = get_immediate_operand_xref(&module, 0x0, &insn, &op).unwrap();
let xref = get_immediate_operand_xref(&module, 0x0, &insn, op).unwrap();
assert_eq!(xref.is_some(), false, "does not have immediate operand");
assert!(xref.is_some().not(), "does not have immediate operand");
}
#[test]
@ -510,6 +511,7 @@ mod tests {
.expect("failed to calculate absolute address")
};
#[allow(clippy::needless_return)]
if let Some(name) = userdata.names.get(&absolute_address) {
// name is found in map, use that.
return buf.get_string()?.append(name);

View file

@ -174,9 +174,9 @@ mod tests {
// .rdata:00475444 dd offset sub_45D16A
// .rdata:00475448 dd offset aConnect ; "connect"
// .rdata:0047544C align 10h
assert!(functions.iter().find(|&&function| function == 0x45CC62).is_some());
assert!(functions.iter().find(|&&function| function == 0x45D028).is_some());
assert!(functions.iter().find(|&&function| function == 0x45D16A).is_some());
assert!(functions.iter().any(|&function| function == 0x45CC62));
assert!(functions.iter().any(|&function| function == 0x45D028));
assert!(functions.iter().any(|&function| function == 0x45D16A));
Ok(())
}

View file

@ -811,7 +811,7 @@ mod tests {
coff.module
.address_space
.read_bytes(start, size as usize)
.expect(&format!("read section {} {:#x} {:#x}", section.name, start, size));
.unwrap_or_else(|_| panic!("read section {} {:#x} {:#x}", section.name, start, size));
}
Ok(())

View file

@ -317,6 +317,8 @@ fn load_pe(buf: &[u8]) -> Result<PE> {
#[cfg(test)]
mod tests {
#![allow(clippy::identity_op)]
use anyhow::Result;
use crate::{aspace::AddressSpace, rsrc::*};
@ -404,7 +406,7 @@ mod tests {
pe.module
.address_space
.read_bytes(start, size as usize)
.expect(&format!("read section {} {:#x} {:#x}", section.name, start, size));
.unwrap_or_else(|_| panic!("read section {} {:#x} {:#x}", section.name, start, size));
}
Ok(())

View file

@ -258,10 +258,10 @@ pub mod uc {
// we don't emulate all of the flags, just the status flags.
assert_eq!(
self.emu.reg_read(EFLAGS).unwrap() as u64 & STATUS_MASK,
self.emu.reg_read(EFLAGS).unwrap() & STATUS_MASK,
other.reg.rflags() & STATUS_MASK,
"flags, uc: {:#b} emu: {:#b}",
self.emu.reg_read(EFLAGS).unwrap() as u64 & STATUS_MASK,
self.emu.reg_read(EFLAGS).unwrap() & STATUS_MASK,
other.reg.rflags() & STATUS_MASK,
);

View file

@ -26,7 +26,7 @@ fn regexset_from_patterns(max: usize) -> regex::bytes::RegexSet {
let patterns = PATTERNS
.iter()
.take(max)
.map(|pattern| regex_from_pattern(*pattern))
.map(|pattern| regex_from_pattern(pattern))
.collect::<Vec<String>>();
regex::bytes::RegexSetBuilder::new(patterns)
@ -100,7 +100,7 @@ fn multiregex_from_patterns(max: usize) -> MultiRegex {
res: PATTERNS
.iter()
.take(max)
.map(|pattern| regex_from_pattern(*pattern))
.map(|pattern| regex_from_pattern(pattern))
.map(|pattern| regex::bytes::Regex::new(&pattern).unwrap())
.collect(),
}
@ -194,7 +194,7 @@ criterion_group!(decisiontree, decisiontree_benchmark);
criterion_main!(regex, multiregex, decisiontree);
// byte signature portion of FLIRT signatures from `vc32rtf.sig`.
const PATTERNS: &'static [&'static str] = &[
const PATTERNS: &[&str] = &[
"0ac97512d9fac30ac9750bc3d9e49bdfe09b9e75ebc3e9",
"0cffa2........a2........a2........a2........a2........a2",
"0d00004000c3",
@ -34702,7 +34702,7 @@ const PATTERNS: &'static [&'static str] = &[
// up to 0x200 bytes from the start of each function in `Practical Malware
// Analysis Lab 16-01.exe_`.
const HAYSTACKS: &'static [&'static [u8]] = &[
const HAYSTACKS: &[&[u8]] = &[
&[
139u8, 84u8, 36u8, 4u8, 139u8, 13u8, 192u8, 199u8, 64u8, 0u8, 57u8, 21u8, 64u8, 199u8, 64u8, 0u8, 86u8, 184u8,
64u8, 199u8, 64u8, 0u8, 116u8, 21u8, 141u8, 52u8, 73u8, 141u8, 52u8, 181u8, 64u8, 199u8, 64u8, 0u8, 131u8,

View file

@ -505,7 +505,7 @@ impl std::fmt::Debug for DecisionTree {
#[cfg(test)]
mod tests {
use std::{io::Read, path::PathBuf};
use std::{io::Read, path::PathBuf, ops::Not};
use super::*;
@ -598,12 +598,12 @@ mod tests {
let mut f = std::fs::File::open(path).unwrap();
let mut s = String::new();
f.read_to_string(&mut s).unwrap();
let patterns: Vec<&str> = s.split("\n").filter(|s| s.len() != 0).collect();
let patterns: Vec<&str> = s.split("\n").filter(|s| s.is_empty().not()).collect();
let _ = DecisionTree::new(&patterns);
}
const PATTERNS: &'static [&'static str] = &[
const PATTERNS: &[&str] = &[
"558bec33c0505050ff751cff7518ff7514ff7510ff750cff7508ff15",
"558bec33c050506804010000ff750c6affff750850e8........50ff15",
"558bec33c05333db40395d0c7c46565785c0743e8b450c03c3992bc28bf08b45",

View file

@ -573,7 +573,7 @@ impl FlirtSignatureSet {
mod tests {
use super::*;
const PAT: &'static str = "\
const PAT: &str = "\
518B4C240C895C240C8D5C240C508D442408F7D923C18D60F88B43F08904248B 21 B4FE 006E :0000 __EH_prolog3_GS_align ^0041 ___security_cookie ........33C5508941FC8B4DF0895DF08B4304894504FF75F464A1000000008945F48D45F464A300000000F2C3
518B4C240C895C240C8D5C240C508D442408F7D923C18D60F88B43F08904248B 1F E4CF 0063 :0000 __EH_prolog3_align ^003F ___security_cookie ........33C5508B4304894504FF75F464A1000000008945F48D45F464A300000000F2C3
518B4C240C895C240C8D5C240C508D442408F7D923C18D60F88B43F08904248B 22 E4CE 006F :0000 __EH_prolog3_catch_GS_align ^0042 ___security_cookie ........33C5508941FC8B4DF08965F08B4304894504FF75F464A1000000008945F48D45F464A300000000F2C3
@ -639,7 +639,7 @@ mod tests {
#[test]
fn test_one_pat_match() {
let sigs = pat::parse(&PAT).unwrap();
let sigs = pat::parse(PAT).unwrap();
let sig = sigs
.iter()
.find(|sig| sig.get_name().unwrap() == "__EH_prolog3_catch_align")
@ -708,7 +708,7 @@ mod tests {
0xc0, 0x75, 0x2d, 0x56, 0xeb,
];
let pat = "ff35........ff742408e8........5959c3 00 0000 0012 :0000 _malloc\n---";
let sigs = FlirtSignatureSet::with_signatures(pat::parse(&pat).unwrap());
let sigs = FlirtSignatureSet::with_signatures(pat::parse(pat).unwrap());
let matches = sigs.r#match(&buf[..]);
assert_eq!(matches.len(), 1);
@ -738,7 +738,7 @@ mod tests {
let pat = "\
56e8........8b4c240833f68908b8........3b08742283c008463d 0d 2a18 0073 :0000 __dosmaperr
---";
let sigs = FlirtSignatureSet::with_signatures(pat::parse(&pat).unwrap());
let sigs = FlirtSignatureSet::with_signatures(pat::parse(pat).unwrap());
let matches = sigs.r#match(&buf[..]);
assert_eq!(matches.len(), 1);
@ -770,7 +770,7 @@ mod tests {
568b742408578b460ca883746f8b7c241485ff740a83ff01740583ff02755d24 0b 2ec8 008d :0000 __fseek_lk (0050: 0E)
---
";
let sigs = FlirtSignatureSet::with_signatures(pat::parse(&pat).unwrap());
let sigs = FlirtSignatureSet::with_signatures(pat::parse(pat).unwrap());
let matches = sigs.r#match(&buf[..]);
assert_eq!(matches.len(), 1);

View file

@ -0,0 +1,35 @@
# https://numtide.github.io/devshell
[[commands]]
name = "profile"
command = "echo 'python'"
help = "show profile (python)"
[devshell]
packages = [
"protobuf",
"just",
"python312",
"stdenv.cc.cc.lib",
"uv",
"pre-commit",
# LSP for helix
"nodePackages.pyright",
"python312Packages.python-lsp-server",
"python312Packages.pylsp-mypy",
"python312Packages.python-lsp-ruff",
"python312Packages.black",
]
# via: https://github.com/numtide/devshell/issues/172#issuecomment-1208745562
[[env]]
name = "CPPFLAGS"
eval = "-I${DEVSHELL_DIR}/include"
[[env]]
name = "LDFLAGS"
eval = "-L${DEVSHELL_DIR}/lib"
[[env]]
name = "LD_LIBRARY_PATH"
eval = "${DEVSHELL_DIR}/lib:${LD_LIBRARY_PATH}"

96
pylancelot/.env/flake.lock generated Normal file
View file

@ -0,0 +1,96 @@
{
"nodes": {
"devshell": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1722113426,
"narHash": "sha256-Yo/3loq572A8Su6aY5GP56knpuKYRvM2a1meP9oJZCw=",
"owner": "numtide",
"repo": "devshell",
"rev": "67cce7359e4cd3c45296fb4aaf6a19e2a9c757ae",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1722073938,
"narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1717179513,
"narHash": "sha256-vboIEwIQojofItm2xGCdZCzW96U85l9nDW3ifMuAIdM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "63dacb46bf939521bdc93981b4cbb7ecb58427a0",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "63dacb46bf939521bdc93981b4cbb7ecb58427a0",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

35
pylancelot/.env/flake.nix Normal file
View file

@ -0,0 +1,35 @@
{
description = "virtual environments";
inputs = {
nixpkgs = {
type = "github";
owner = "nixos";
repo = "nixpkgs";
# nixos-24.05
ref = "63dacb46bf939521bdc93981b4cbb7ecb58427a0";
};
devshell = {
url = "github:numtide/devshell";
};
flake-utils = {
url = "github:numtide/flake-utils";
};
};
outputs = { self, flake-utils, devshell, nixpkgs }:
flake-utils.lib.eachDefaultSystem (system: {
devShell =
let
pkgs = import nixpkgs {
inherit system;
overlays = [ devshell.overlays.default ];
};
in
pkgs.devshell.mkShell {
imports = [ (pkgs.devshell.importTOML ./devshell.toml) ];
};
});
}

4
pylancelot/.envrc Normal file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
watch_file .env/devshell.toml
use flake .env
layout python

View file

@ -1,2 +1 @@
.direnv
.envrc

18
pylancelot/.justfile Normal file
View file

@ -0,0 +1,18 @@
isort:
uvx isort --length-sort --profile black --line-length 120 src/ scripts/ tests/
black:
uvx black --line-length 120 src/ scripts/ tests/
ruff:
uvx ruff check --line-length 120 src/ scripts/ tests/
mypy:
# note the src/ is not included here since there are currently no py files there
uvx mypy --check-untyped-defs --ignore-missing-imports scripts/ tests/
lint:
-just isort
-just black
-just ruff
-just mypy

View file

@ -1,7 +1,6 @@
## testing
```sh
# use a py3 virtual environment
```console
$ pip install maturin[patchelf] pytest
$ maturin develop --release --extras dev
$ pytest

View file

@ -2,8 +2,7 @@ import sys
import lancelot
with open(sys.argv[1], 'rb') as f:
with open(sys.argv[1], "rb") as f:
buf = f.read()
ws = lancelot.from_bytes(buf)

View file

@ -2,15 +2,14 @@ import os.path
import pytest
CD = os.path.dirname(__file__)
with open(os.path.join(CD, 'data', 'k32.dll_'), 'rb') as f:
with open(os.path.join(CD, "data", "k32.dll_"), "rb") as f:
K32 = f.read()
with open(os.path.join(CD, 'data', 'altsvc.c.obj'), 'rb') as f:
with open(os.path.join(CD, "data", "altsvc.c.obj"), "rb") as f:
ALTSVC = f.read()

View file

@ -1,9 +1,8 @@
import contextlib
import pytest
import lancelot
from fixtures import *
def test_invalid_pe():
with pytest.raises(ValueError):
@ -12,10 +11,8 @@ def test_invalid_pe():
with pytest.raises(ValueError):
lancelot.from_bytes(b"MZ\x9000")
try:
with contextlib.suppress(ValueError):
lancelot.from_bytes(b"")
except ValueError as e:
pass
def test_load_pe(k32):
@ -104,7 +101,7 @@ def test_read_insn(k32):
assert operands[1][lancelot.OPERAND_SIZE] == 64
assert operands[1][lancelot.REGISTER_OPERAND_REGISTER] == "rcx"
assert operands == ((1, 64, 'rsp', None, 'ss', 0, 8), (3, 64, 'rcx'))
assert operands == ((1, 64, "rsp", None, "ss", 0, 8), (3, 64, "rcx"))
def test_read_bytes(k32):