Grapl: A Graph Platform for Detection and Response

Edge

Node

Node

Node

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,
}

Unstable

Unstable

pid: 1200,

timestamp: 1551754000

pid: 1200,

timestamp: 1551755000

pid: 1200,

timestamp: 1551766000

834c1419-7987-41d6-a3c4-1c811e00fecb

981f30b9-e076-4713-9f83-999e40695dd0

Identity

{
  'ppid': 100,
  'pid': 120,
  'type': 'process_start',
  'timestamp': 1551753726,
  'image_name': 'program.exe',
}

executed

pid: 100

pid: 120

node_key:<GUID>

node_key:<GUID>

image: 'program.exe'

{
  'pid': 120,
  'type': 'process_create_file',
  'timestamp': 1551754726,
  'image_name': 'program.exe',
  'file_path': 'path/to/file.doc'
}

node_key:<GUID>

path: 'path/to/file.doc'

create

1551754726

{
  'pid': 120,
  'type': 'process_read_file',
  'timestamp': 1551757726,
  'image_name': 'program.exe',
  'file_path': 'path/to/file.doc'
}

read

1551757726

ppid, type

pid, image_name, type

Redundant:

pid, file_path,

image_name, type

1551753726

Process Execution

File Creation

File Read

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

<any process>

<any file>

<external ip>

Process with external network access creates file, executes child from it

<any browser>

Browser Executing Child Process

<any process>

<winrar/7zip/zip>

<any file>

Process Executed From Unpacked Binary

<any process>

created file

executed as

<any process>

Rare Parent Child Process

<any process>

executed

executed

created file

executed as

connected to

executed

<any process>

Rare Parent LOLBAS Process

<any process>

executed

<binary>

<lolbas path>

<word/reader/etc>

Commonly Target Application with Non Whitelisted Child Process

<non whitelisted process>

executed

Risk: 5

Risk: 70

Risk: 5

Risk: 20

Risk: 10

Risk: 15

chrome.exe

<cmd.exe>

executed

<zip.exe>

<flash_installer.exe>

<flash_installer.exe>

created file

executed as

<52.217.32.54>

connected to

<svchost.exe>

created file

executed

executed

<svchost.exe>

name: 'Browser Executing Child Process'

score: 5

name: Process with external network access creates file, executes child from it

score: 25

name:Unpacked process execution

score: 10

Risk Node

Risk Node

Risk Node

Asset Lens

score: 80

<winrar/7zip/zip>

<any file>

Process Executed From Unpacked Binary

<any process>

created file

executed as

ProcessQuery()
  .with_bin_file(
      FileQuery()
      .with_creator(
          ProcessQuery()
          .with_process_name(eq=Or('zip.exe', '7zip.exe', 'winrar.exe'))
      )
  )

class CommonTargetWithChildProcess(Analyzer):

    def get_queries(self) -> OneOrMany[ProcessQuery]:
        return (
            ProcessQuery()
          
            .with_process_name(eq="winword.exe")
            .with_process_name(eq="excel.exe")
            .with_process_name(eq="reader.exe")
            .with_children(ProcessQuery())
         )

    def on_response(self, response: ProcessView, output: Any):
        output.send(
            ExecutionHit(
                analyzer_name="Common Target Application With Child Process",
                node_view=output,
                risk_score=75,
            )
        )

Any process

With one of these process names

With any children

Executes for every change to the master graph

class UniqueWindowsBuiltinExecution(Analyzer):
    def __init__(self, dgraph_client: DgraphClient, counter: ParentChildCounter):
        super(UniqueWindowsBuiltinExecution, self).__init__(dgraph_client)
        self.counter = counter

    @classmethod
    def build(cls: Type[A], dgraph_client: DgraphClient) -> A:
        counter = ParentChildCounter(dgraph_client)
        return UniqueWindowsBuiltinExecution(dgraph_client, counter)

    def get_queries(self) -> OneOrMany[Queryable]:
        return (
            ProcessQuery().with_process_name()
            .with_parent(
                ProcessQuery().with_process_name()
                .with_bin_file(FileQuery())
            )
            .with_bin_file(
                FileQuery()
                .with_file_path(contains='Windows\\\\System32\\')
                .with_file_path(contains='Windows\\\\SysWow64\\')
            )
        )

    def on_response(self, response: ProcessView, output: Any):
        count = self.counter.get_count_for(
            parent_process_name=output.get_parent().get_process_name(),
            child_process_name=output.get_process_name(),
        )

        if count <= 2:
            output.send(
                ExecutionHit(
                    analyzer_name="Unique Windows Builtin Execution",
                    node_view=output,
                    risk_score=15,
                )
            )
from analyzers.suspicious_svchost.main import SuspiciousSvchostAnalyzer


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):
        query = SuspiciousSvchostAnalyzer.build(self.local_mg).get_queries()
        result = query.query_first(self.local_mg, contains_node_key=self.node_view.node_key)
        assert isinstance(result, ExecutionHit)

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

        query = SuspiciousSvchostAnalyzer.build(self.local_mg).get_queries()
        result = query.query_first(self.local_mg, contains_node_key=benign_view.node_key)
        
        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)
svchost = engagement.get_process('df41941e-1b20-4e61-9a99-34e4e4a56211')
svc_parent = svchost.get_parent()
$ 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