Grapl: A Graph Platform for Detection and Response

Edge

Node

Node

Node

Edge

Edge

Manage from reality because that's the prepared Defenders Mindset

https://github.com/BloodHoundAD/BloodHound

[
  {
  'ppid': 100,
  'pid': 250,
  'action': 'started',
  'time': '2019-02-24 18:21:25'
  },
  {
  'ppid': 250,
  'pid': 350,
  'action': 'started',
  'time': '2019-02-24 18:22:31'
  }
]
[
  {
  'pid': 350,
  'action': 'connected_to',
  'domain': 'evil.com',
  'time': '2019-02-24 18:24:27'
  },
  {
  'pid': 350,
  'action': 'created_file',
  'path': 'downloads/evil.exe',
  'time': '2019-02-24 18:28:31'
  }
]

seen_at: '2019-02-24 18:21:25'

created_at: '2019-02-24 18:21:25'

created_at: '2019-02-24 18:22:31'

seen_at: '2019-02-24 18:24:27'

seen_at: '2019-02-24 18:28:31'

seen_at: '2019-02-24 18:22:31'

seen_at: '2019-02-24 18:24:27'

seen_at:  '2019-02-24 18:28:31'

{
  "host_id": "cobrien-mac",
  "parent_pid": 3,
  "pid": 4,
  "image_name": "word.exe",
  "create_time": 600,
}
{
  "host_id": "cobrien-mac",
  "parent_pid": 4,
  "pid": 5,
  "image_name": "payload.exe",
  "create_time": 650,
}

explorer.exe

payload.exe

word.exe

word.exe

payload.exe

word.exe

word.exe

payload.exe

ssh.exe

/secret/file

11.22.34.55

mal.doc

Grapl

Graph Analytics Platform

for Detection, and Incident Response

Parsing

Subgraph Generation

Identification

Merging

Analysis

Engagements

Process {
    node_key: string ,
    asset_id: string ,
    process_id: int ,
    process_guid: string ,
    created_timestamp: int ,
    terminated_timestamp: int ,
    last_seen_timestamp: int ,
    process_name: string ,
    process_command_line: string ,
    process_integrity_level: string ,
    operating_system: string ,
    process_path: File ,
    children: [Process] ,
    created_files: [File] ,
    deleted_files: [File] ,
    read_files: [File] ,
    wrote_files: [File] ,
}
File {
    node_key: string ,
    asset_id: string ,
    created_timestamp: int ,
    deleted_timestamp: int ,
    last_seen_timestamp: int ,
    file_name: string ,
    file_path: string ,
    file_extension: string ,
    file_mime_type: string ,
    file_size: int ,
    file_version: string ,
    file_description: string ,
    file_product: string ,
    file_company: string ,
    file_directory: string ,
    file_inode: int ,
    file_hard_links: int ,
    md5_hash: string ,
    sha1_hash: string ,
    sha256_hash: string ,
}
OutboundConnection {
    port: int,
    created: int,
    ended: int,
    external_connections: ExternalIp
}
InboundConnection {
    port: int,
    created: int,
    ended: int,
}
Asset {
    operating_system: string,
    processes: [Process],
}
ExternalIp {
    port: int,
    address: str,
}
{
  'pid': 100,
  'type': 'process_start',
  'timestamp': 1551753726,
  'image': '/usr/bin/program',
}
{
  'pid': 100,
  'type': 'file_read',
  'path': '/home/.cache/file',
  'timestamp': 1551754726
}
{
  'pid': 100,
  'type': 'process_terminate',
  'timestamp': 1551755726,
  'image': '/usr/bin/program',
}

pid: 100

created: 1551753726

terminated: 1551755726

image: '/usr/bin/program'

node_key: <uuid>

pid 250

Session Identification

C - 0

0           10           20         30          40           50          60          70         80

{
  'pid': 250,
  'created_at': 30
}

C - 1

ID - 0

{
  'pid': 250,
  'created_at': 50
}

ID - 1

C - Process Create Event

pid 250

C - Process Create Event

C - 0

0           10           20         30          40           50          60          70         80

{
  'pid': 250,
  'seen_at': 30
}

C - 1

ID - 0

Session Identification

{
  'type': 'Process Create',
  'image': 'C:\Program Files\Microsoft Office\winword.exe',
  'pid': 1200,
  'ppid': 1080
}
{
  'type': 'Process Create',
  'image': 'C:\Windows\WindowsPowershell\v1.0\powershell.exe',
  'pid': 2590,
  'ppid': 1200,
}

word.exe

powershell.exe

unique parent child

word.exe

payload.exe

wmiexec.exe

/secret/file

evil.com

explorer.exe

word.exe

payload.exe

word.exe

evil.com

word.exe talking to non-whitelisted domain

Unique Parent Child

cmd.exe

payload.exe

wmiexec.exe

cmd.exe

LOLBAS from non standard grandparent process

word.exe

payload.exe

evil.com

Process with network access creates file, executes child from it

Risk 10

Risk: 30

Risk 40

Risk 100

word.exe

payload.exe

evil.com

Asset Lens

Risk Node

name: 'word with child process'

score: 100

Risk Node

name: 'word network'

score: 80

wmiexec.exe

cmd.exe

Risk Node

name: 'unique wmiexec grandparent'

score: 75

score: 400

Risk Node

name: 'file created and then executed'

score: 50

Risk Node

name: 'unique parent child process'

score: 20

from grapl_analyzerlib.execution import ExecutionHit
from pydgraph import DgraphClient
from grapl_analyzerlib.entities import ProcessQuery, SubgraphView, NodeView


def analyzer(client: DgraphClient, node: NodeView, sender: Any):
    process = node.as_process_view()
    if not process: return

    p = (
        ProcessQuery()
        .with_process_name(eq="winword.exe")
        .with_children(ProcessQuery())
        .query_first(client, contains_node_key=process.node_key)
    )

    if p:
        sender.send(
            ExecutionHit(
                analyzer_name="Suspicious Word Child Process",
                node_view=p,
                risk_score=90,
            )
        )
def analyzer(client: DgraphClient, node: NodeView, sender: Any):
    counter = ParentChildCounter(client)

    process = node.as_process_view()
    if not process: return

    p = (
        ProcessQuery().with_parent(ProcessQuery()
        .query_first(client, contains_node_key=process.node_key)
    )  # type: Optional[ProcessView]

    if not p: return

    parent = p.get_parent()

    count = counter.get_count_for(
        parent_process=parent,
        child_process=p,
    )

    if count < 2:
        sender.send(
            ExecutionHit(
                analyzer_name="Rare Parent Child Process",
                node_view=p,
                risk_score=35,
            )
        )
def analyzer(client: DgraphClient, node: NodeView, sender: Any):
    unpackers = ["7zip.exe", "winrar.exe", "zip.exe"]

    process = node.as_process_view()
    if not process: return

    # Look for exec'd processes with binary files that were created by unpackers
    p = (
        ProcessQuery().with_bin_file(
            FileQuery().with_creator(
                ProcessQuery().with_process_name(eq=unpackers)
            )
        )
        .query_first(client, contains_node_key=process.node_key)
    )

    if p:
        sender.send(
            ExecutionHit(
                analyzer_name="Process Executing From Unpacked File",
                node_view=p,
                risk_score=15,
            )
        )
from analyzers.suspicious_svchost.main import analyzer


class TestSuspiciousSvchost(unittest.TestCase):

    def setUp(self) -> None:
        self.local_mg = init_local_dgraph()
        self.node_view = populate_signature('hardcoded-node-key')

    def test_suspicious_svchost_hit(self):
        result = exec_sync(analyzer, self.local_mg, self.node_view)
        assert isinstance(result, ExecutionHit)

    def test_suspicious_svchost_miss(self):
        benign_view = deepcopy(self.node_view)
        benign_view.node_key = "some-other-key"

        result = exec_sync(analyzer, self.local_mg, benign_view)

        assert result is None


if __name__ == "__main__":
    unittest.main()
{
    'pid': 250,
    'ppid': 150,
    'created_at': 1551565127,
    'hash': 'acbd18db4cc2f85cedef654fccc4a4d8',
    'image_name': '/home/user/downloads/evil.sh'
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}

Search Window

pid collision

{
  ~~: ~~
  ~~: ~~
}

Alert

engagement = EngagementView.get_or_create('Demo', cclient)
root = engagement.get_process('df41941e-1b20-4e61-9a99-34e4e4a56211')

raw-logs

sysmon-log-parser

generic-log-parser

unidentified-subgraphs

node-identifier

graph-merger

raw-logs

sysmon-log-parser

generic-log-parser

custom-parser

#[derive(DynamicNode, GraplStaticId)]
pub struct AwsEc2Instance {
    #[grapl(static_id)]
    arn: String,
    launch_time: u64,
}

impl IAwsEc2InstanceNode for AwsEc2InstanceNode {
    fn get_mut_dynamic_node(&mut self) -> &mut DynamicNode {
        &mut self.dynamic_node
    }
}

fn main()
    grapl_generator_plugin(move |raw_guard_duty_alert| {
        let log: InstanceDetails = serde_json::from_slice(raw_guard_duty_alert).unwrap();
    
        let mut ec2 = AwsEc2InstanceNode::new(
            AwsEc2InstanceNode::static_strategy(),
            log.launch_time
        );
        ec2.with_arn(log.arn).with_launch_time(log.launch_time);
    
        let mut graph = GraphDescription::new(log.launch_time);
        graph.add_node(ec2);
        graph
    })
    
}
class Ec2InstanceQuery(DynamicNodeQuery):
    def __init__(self) -> None:
        super(Ec2InstanceQuery, self).__init__("AwsEc2Instance", Ec2InstanceView)

    def with_launch_time(
        self, 
        eq=IntCmp, 
        gt=IntCmp, 
        lt=IntCmp
    ) -> 'Ec2InstanceQuery':
        self.with_property_int_filter("launch_time", eq, gt, lt)
        return self

    def with_instance_id(
        self, 
        eq=StrCmp, 
        contains=StrCmp, 
        ends_with=StrCmp
    ) -> 'Ec2InstanceQuery':
        self.with_property_str_filter("instance_id", eq, contains, ends_with)
        return self
class Ec2InstanceView(Viewable):
    def __init__(
            self,
            dgraph_client: DgraphClient,
            node_key: str,
            uid: str,
            arn: Optional[str] = None,
            instance_id: Optional[str] = None,
            launch_time: Optional[int] = None,
            guard_duty_findings: List[GuardDutyAlertView] = None,
            **kwargs,
    ):
        super(Ec2InstanceView, self).__init__(dgraph_client, node_key, uid)
        self.arn = arn
        self.instance_id = instance_id
        self.launch_time = launch_time

        self.guard_duty_findings = guard_duty_findings

    @staticmethod
    def get_property_types -> List[Tuple[str, Callable[[Any], Union[str, int]]]]:
        return [("launch_time", int), ("arn", str), ("instance_id", str)]

    @staticmethod
    def get_edge_types() -> List[Tuple[str, Union[List[Type[V]], Type[V]]]]:
        return [("~finding_resource", [GuardDutyAlertView], "guard_duty_findings")]

    def get_arn(self) -> Optional[str]:
        self.arn = self.get_property('arn', str)
        return self.arn

    def get_instance_id(self) -> Optional[str]:
        self.instance_id = self.get_property('instance_id', str)
        return self.instance_id
class GuardDutySubgraphGenerator extends cdk.Stack {
    constructor(parent: cdk.App, id: string) {
        super(parent, id + '-stack');
        const environment = {"BUCKET_PREFIX": process.env.BUCKET_PREFIX};

        const guard_duty_log_events = new EventEmitter(this,"guardduty-raw-logs");

        const service = new Service(this, id, environment, guard_duty_log_events);

        const bucket = getUnidSubgraphGeneratedBucket(process.env.BUCKET_PREFIX);
        service.publishesToBucket(bucket);
    }
}

class GraplPlugin extends cdk.App {
    constructor() {
        super();
        env(__dirname + '/.env');

        new GuardDutySubgraphGenerator(
          this,
          'guardduty-subgraph-generator',
        );
    }
}

new GraplPlugin().synth();
$ git clone git@github.com:insanitybit/grapl.git
$ cd ./grapl/grapl-cdk/
$ npm install -g
$ <your editor> ./.env

BUCKET_PREFIX="<unique identifier>"
$ ./deploy_all.sh
cd /path/to/grapl/
python ./gen-raw-logs.py <bucket prefix>

Questions

[BSidesLV] Grapl: A Graph Platform for Detection and Response

By Colin

[BSidesLV] Grapl: A Graph Platform for Detection and Response

  • 919