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'

Grapl

Graph Analytics Platform

for Detection, Forensics, and Incident Response

 

  • Consumes logs
  • Turn logs into subgraphs
  • Merge subgraphs into master graph
  • Executes code against master graph to find scary patterns
  • Scopes engagements through graph expansion
{
  "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,
}

word.exe

payload.exe

ssh.exe

/secret/file

evil.com

explorer.exe

payload.exe

word.exe

Process {
  image_name: str,
  image_path: str,
  pid: int,
  create_time: int,
  terminate_time: int,
  last_seen_time: int,
  children: [Process],
  binary: File,
  created_files: [File],
  read_files: [File],
  written_files: [File],
  deleted_files: [File],
  outbound: [OutboundConnection],
  inbound: [InboundConnection]
}
File {
  path: str,
  create_time: int,
  delete_time: int,
  last_seen_time: int,  
}
OutboundConnection {
    port: int,
    created: int,
    ended: int,
    bound_connection: InboundConnection
}
InboundConnection {
    port: int,
    created: int,
    ended: int,
}
User {
    assets: [Asset],
    reports_to: User,
    created: int,
    ended: int,
}
Asset {
    processes: [Process],
}
Email {
    from: User,
    to: User,
    subject: str,
    body: str
}
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

C - Process Create Event

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

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

pid 250

C - Process Create Event

0           10           20         30          40           50          60          70         80

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

C - 1

G - Process Create Event (Guessed)

G - 0

ID - 0

pid 250

C - Process Create Event

0           10           20         30          40           50          60          70         80

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

C - 1

G - Process Create Event (Guessed)

G - 0

G - 0

ID - 0

{
  '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

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

word.exe spawning child process

cmd.exe

payload.exe

wmiexec.exe

cmd.exe

wmiexec from non standard grandparent process


# Malware names itself 'svchost.exe' in order to look legitimate
def signature_graph(node_key) -> str:
    child = Process() \
        .with_image_name(contains="svchost.exe") \
        .with_node_key(eq=node_key)

    parent = Process() \
        .with_image_name(contains=Not("services.exe"))
    return parent.with_child(child).to_query()


def _analyzer(client: DgraphClient, graph: Subgraph, sender: Connection):
    hits = analyze_by_node_key(client, graph, signature_graph)

    for hit in hits:
        sender.send(ExecutionHit.from_parent_child(hit))

    sender.send(ExecutionComplete())
{
    'pid': 250,
    'ppid': 150,
    'created_at': 1551565127,
    'hash': 'acbd18db4cc2f85cedef654fccc4a4d8',
    'image_name': '/home/user/downloads/evil.sh'
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}
{
  ~~: ~~
  ~~: ~~
}

Search Window

pid collision

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

Alert

cmd.exe

svchost.exe

wmiexec.exe

/secret/file

evil.com

engagement = Engagement(engagement_id)
svchost = engagement.get_by_image_name(contains="svchost.exe")
parent = svchost.get_parent()
parent.get_connections()
svchost.get_read_files()
svchost.get_children_files()

raw-logs

sysmon-log-parser

generic-log-parser

unidentified-subgraphs

node-identifier

graph-merger

raw-logs

sysmon-log-parser

generic-log-parser

custom-parser


    let parent = ProcessDescriptionBuilder::default()
        .host_id(process_start.computer)
        .state(ProcessState::Existing)
        .pid(process_start.ppid)
        .last_seen_timestamp(timestamp)
        .build()?;

    let child = ProcessDescriptionBuilder::default()
        .host_id(process_start.computer)
        .image_path(process_start.image)
        .image_name(get_image_name(&process_start.image)?)
        .state(ProcessState::Created)
        .pid(process_start.pid)
        .created_timestamp(timestamp)
        .build()?;
{
    'action': 'Process Create',
    'computer': 'winuser-laptop',
    'ppid': 100,
    'pid: 150,
    'image': 'C:\\Users\\Demo\\Downloads\\foo.exe',
    'timestamp': 1551578297
}

    let child_exe = FileDescriptionBuilder::default()
        .hostt_id(process_start.computer)
        .state(FileState::Existing)
        .last_seen_timestamp(timestamp)
        .path(process_start.image)
        .build()?;
{
    'action': 'Process Create',
    'computer': 'winuser-laptop',
    'ppid': 100,
    'pid: 150,
    'image': 'C:\\Users\\Demo\\Downloads\\foo.exe',
    'timestamp': 1551578297
}

    let mut graph = GraphDescription::new(
        timestamp
    );

    graph.add_edge("bin_file",
                   child.clone_key(),
                   child_exe.clone_key()
    );
    graph.add_node(child_exe);
    graph.add_edge("children",
                   parent.clone_key(),
                   child.clone_key());
    graph.add_node(parent);
    graph.add_node(child);

Automated scoping

https://www.princeton.edu/~pmittal/publications/priotracker-ndss18

$ git clone git@github.com:insanitybit/grapl.git
$ cd ./grapl/grapl-cdk/
$ npm install -g
$ <your editor> ./.env

HISTORY_DB_USERNAME=username
HISTORY_DB_PASSWORD=password
BUCKET_PREFIX="<unique identifier>"
GRAPH_DB_KEY_NAME=<name of SSH key, if debug mode is enabled, to SSH to graphdb>
$ ssh ubuntu@<master_graph/engagement_graph>
$ curl https://get.dgraph.io -sSf > setup.sh
$ <inspect setup.sh>
$ ./setup.sh
$ dgraph zero &
$ dgraph alpha --zero localhost:5080 &
$ dgraph-ratel &
# ./deploy_all.sh
$ cd ../grapl/
$ python ./gen-raw-logs.py <your bucket prefix>

Questions