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