├── .codeclimate.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── analyzer
├── __init__.py
└── darwin
│ ├── __init__.py
│ ├── analyzer.py
│ ├── lib
│ ├── __init__.py
│ ├── common
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── hashing.py
│ │ ├── rand.py
│ │ └── results.py
│ ├── core
│ │ ├── __init__.py
│ │ ├── constants.py
│ │ ├── data
│ │ │ ├── signatures.yml
│ │ │ └── types.yml
│ │ ├── filetimes.py
│ │ ├── host.py
│ │ ├── osx.py
│ │ └── packages.py
│ └── dtrace
│ │ ├── __init__.py
│ │ ├── apicalls.d
│ │ ├── apicalls.py
│ │ ├── autoprobes.py
│ │ ├── common.py
│ │ ├── dtruss.py
│ │ ├── dtruss.sh
│ │ ├── follow_children.d
│ │ ├── ipconnections.d
│ │ └── ipconnections.py
│ └── modules
│ ├── __init__.py
│ └── packages
│ ├── __init__.py
│ ├── app.py
│ ├── bash.py
│ ├── macho.py
│ └── zip.py
├── config
├── signatures.yml
└── types.yml
├── requirements.txt
├── scripts
├── bootstrap_guest.sh
└── bootstrap_host.sh
└── tests
├── assets
├── probes
│ └── test_probes_integration.d.reference
├── test_apicalls_basic.c
├── test_apicalls_children.c
├── test_apicalls_children_root.c
├── test_apicalls_errno.c
├── test_apicalls_errno_root.c
├── test_apicalls_from_dynamic_library.c
├── test_apicalls_from_dynamic_library_root.c
├── test_apicalls_root.c
├── test_apicalls_timeout.c
├── test_apicalls_timeout_root.c
├── test_apicalls_with_args.c
├── test_apicalls_with_args_root.c
├── test_apicalls_without_target.c
├── test_cuckoo_dropped_files
├── test_cuckoo_dropped_files.c
├── test_cuckoo_parents_and_children
├── test_cuckoo_parents_and_children.c
├── test_dtruss_children.c
├── test_dtruss_helloworld.c
├── test_dtruss_non_root.c
├── test_dtruss_root.c
├── test_dtruss_specific_syscall.c
├── test_dtruss_timeout.c
├── test_dtruss_with_args.c
├── test_dtruss_without_target.c
├── test_ipconnections_empty.c
├── test_ipconnections_target_with_args.c
├── test_ipconnections_tcp.c
├── test_ipconnections_tcp_with_timeout.c
└── test_ipconnections_udp.c
├── common.py
├── test_analyzer.py
├── test_apicalls.py
├── test_cuckoo.py
├── test_dtruss.py
├── test_ipconnections.py
├── test_packages.py
└── test_probesgenerator.py
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | languages:
2 | Python: true
3 | exclude_paths:
4 | - "tests/*"
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | *.egg-info/
22 | .installed.cfg
23 | *.egg
24 |
25 | # PyInstaller
26 | # Usually these files are written by a python script from a template
27 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
28 | *.manifest
29 | *.spec
30 |
31 | # Installer logs
32 | pip-log.txt
33 | pip-delete-this-directory.txt
34 |
35 | # Unit test / coverage reports
36 | htmlcov/
37 | .tox/
38 | .coverage
39 | .coverage.*
40 | .cache
41 | nosetests.xml
42 | coverage.xml
43 | *,cover
44 | .noseids
45 |
46 | # Translations
47 | *.mo
48 | *.pot
49 |
50 | # Django stuff:
51 | *.log
52 |
53 | # Sphinx documentation
54 | docs/_build/
55 |
56 | # PyBuilder
57 | target/
58 |
59 | # PyCharm
60 | .idea/
61 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language:
2 | - objective-c # make it run on OS X workers *only*
3 | notifications:
4 | email:
5 | on_success: never
6 | on_failure: always
7 | install:
8 | - brew install python
9 | - brew link --overwrite python
10 | - brew install shellcheck
11 | - pip install -r requirements.txt
12 | script:
13 | - nosetests ./tests/test_dtruss.py || echo "Temporary workaround for dtruss tests failing without a reason. See issue 23"
14 | - nosetests ./tests/test_apicalls.py
15 | - nosetests ./tests/test_ipconnections.py
16 | - nosetests ./tests/test_analyzer.py
17 | - nosetests ./tests/test_packages.py
18 | - nosetests ./tests/test_probesgenerator.py
19 | - nosetests ./tests/test_cuckoo.py
20 | - shellcheck ./scripts/*
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Dmitry Rodionov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cuckoo-osx-analyzer
2 | [](https://travis-ci.org/rodionovd/cuckoo-osx-analyzer) [](http://badge.fury.io/gh/rodionovd%2Fcuckoo-osx-analyzer) [](https://codeclimate.com/github/rodionovd/cuckoo-osx-analyzer)
3 | My [GSoC project](http://www.google-melange.com/gsoc/project/details/google/gsoc2015/rodionovd/5649050225344512) aiming at building an OS X analyzer for [Cuckoo Sandbox](http://www.cuckoosandbox.org/) project.
4 |
5 | :warning: **WIP** :warning:
6 | ----
7 |
8 | ### Usage
9 |
10 | > See also: [`bootstrap_host.sh`](./scripts/bootstrap_host.sh) and [`bootstrap_guest.sh`](./scripts/bootstrap_guest.sh)
11 |
12 | ##### Guest machine setup
13 |
14 | 0. Install `pt_deny_attach` kernel extension suggested by @phretor. That's an optional step, see [this comment](https://github.com/rodionovd/cuckoo-osx-analyzer/issues/6#issuecomment-101322097) for more details.
15 |
16 | 1. Since this analyser uses some utilities that require additional privileges to run, you may want to enable passwordless `sudo` for them. This may be accomplished by modifying `/etc/sudoers` file:
17 |
18 | ```diff
19 | --- a/etc/sudoers
20 | +++ b/etc/sudoers
21 | @@ -43,3 +43,5 @@ root ALL=(ALL) ALL
22 | # Samples
23 | # %users ALL=/sbin/mount /cdrom,/sbin/umount /cdrom
24 | # %users localhost=/sbin/shutdown -h now
25 | +
26 | + username ALL=(root) NOPASSWD: /usr/sbin/dtrace
27 | + username ALL=(root) NOPASSWD: /bin/date
28 | ```
29 | (replace `username` above with an actual user name).
30 |
31 | 2. Set the network settings: IP address, subnet mask, router address and DNS servers:
32 | ```bash
33 | $ sudo networksetup -setmanual Ethernet 192.168.56.101 255.255.255.0 192.168.56.1
34 | $ sudo networksetup -setdnsservers Ethernet 8.8.8.8 8.8.4.4
35 | ```
36 |
37 | > Also, if you're using VirtualBox: don't forget to setup your host-only internet adapter and attach it to the guest machine.
38 |
39 | 3. Download and launch Cuckoo's `agent.py`:
40 |
41 | ```bash
42 | $ curl -o /Users/Shared/agent.py https://raw.githubusercontent.com/cuckoobox/cuckoo/master/agent/agent.py
43 | $ python /Users/Shared/agent.py
44 | ```
45 |
46 | 4. Take a VM snapshot. It's cmd+T for VirtualBox.
47 |
48 | ##### On the host side (OS X)
49 |
50 | 0. Setup internet traffic forwarding to and from your guest machine. Here's an example of using `pfctl` to forward traffic to and from `vboxnet0` interface:
51 |
52 | ```shell
53 | $ sudo sysctl -w net.inet.ip.forwarding=1
54 |
55 | $ rules="nat on en1 from vboxnet0:network to any -> (en1)
56 | pass inet proto icmp all
57 | pass in on vboxnet0 proto udp from any to any port domain keep state
58 | pass quick on en1 proto udp from any to any port domain keep state"
59 |
60 | $ echo "$rules" > ./pfrules
61 | $ sudo pfctl -e -f ./pfrules
62 | $ rm -f ./pfrules
63 | ```
64 |
65 | > **Note that you'll have to re-enable traffic forwarding every time you reboot the host machine.**
66 |
67 | 1. Clone this repository:
68 |
69 | ```shell
70 | $ git clone https://github.com/rodionovd/cuckoo-osx-analyzer.git ~/cuckoo-osx-analyzer
71 | # Or (if you prefer SSH):
72 | # $ git clone git@github.com:rodionovd/cuckoo-osx-analyzer.git cuckoo-osx-analyzer
73 | ```
74 |
75 | 2. Symlink `analyzer/darwin` directory from this repository to your own copy of [Cuckoo Sandbox](https://github.com/cuckoobox/cuckoo/):
76 |
77 | ```shell
78 | $ cd /path/to/cuckoo/sandbox/
79 | $ cd ./analyzer
80 | $ ln -s ~/cuckoo-osx-analyzer/analyzer/darwin darwin
81 |
82 | ```
83 |
84 | 3. Submit an analysis job:
85 |
86 | ```bash
87 | $ ./utils/submit.py --platform darwin ~/bin/sample
88 | ```
89 |
90 | ### Adding custom API signatures
91 | You can add custom API signatures and define data types in [`signatures.json`](./cuckoo-osx-analyzer/analyzer/darwin/lib/core/data/signatures.yml) and [`types.yml`](./cuckoo-osx-analyzer/analyzer/darwin/lib/core/data/types.yml) files.
92 |
93 | ### Tests
94 |
95 | You can run the test suite with `nose`:
96 |
97 | ```bash
98 | $ cd ./cuckoo-osx-analyzer
99 | $ sudo -H pip install -r requirements.txt
100 | $ nosetests
101 | ```
102 |
103 | > NOTE: Running [Cuckoo integration tests](https://github.com/rodionovd/cuckoo-osx-analyzer/blob/master/tests/test_cuckoo.py) requires Cuckoo to be installed in the same directory as the analyzer itself:
104 | > ```
105 | $ ls
106 | cuckoo cuckoo-osx-analyzer
107 | ```
108 |
109 | See also: [`.travis.yml`](https://github.com/rodionovd/cuckoo-osx-analyzer/blob/master/.travis.yml) and [Travis CI project](https://travis-ci.org/rodionovd/cuckoo-osx-analyzer).
110 |
111 |
112 | ### Roadmap, issues and reports
113 |
114 | See my [weekly reports](https://github.com/rodionovd/cuckoo-osx-analyzer/wiki/GSoC-weekly-reports) and [Issues](https://github.com/rodionovd/cuckoo-osx-analyzer/issues).
115 |
116 |
117 | -----
118 |
119 | Dmitry Rodionov, i.am.rodionovd@gmail.com
120 | 2015
121 |
--------------------------------------------------------------------------------
/analyzer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rodionovd/cuckoo-osx-analyzer/fabbbe86e59bc39f9054d4ed35299d5f6a1f2b01/analyzer/__init__.py
--------------------------------------------------------------------------------
/analyzer/darwin/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rodionovd/cuckoo-osx-analyzer/fabbbe86e59bc39f9054d4ed35299d5f6a1f2b01/analyzer/darwin/__init__.py
--------------------------------------------------------------------------------
/analyzer/darwin/analyzer.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2015 Dmitry Rodionov
3 | # This software may be modified and distributed under the terms
4 | # of the MIT license. See the LICENSE file for details.
5 |
6 | import logging
7 | from sys import stderr
8 | from hashlib import sha256
9 | from xmlrpclib import Server
10 | from traceback import format_exc
11 | from os import path, getcwd, makedirs
12 |
13 | from lib.common.config import Config
14 | from lib.common.hashing import hash_file
15 | from lib.common.results import NetlogHandler, upload_to_host
16 | from lib.core.constants import PATHS
17 | from lib.core.packages import choose_package_class
18 | from lib.core.osx import set_wallclock
19 | from lib.core.host import CuckooHost
20 |
21 | class Macalyzer(object):
22 | """Cuckoo OS X analyser.
23 | """
24 |
25 | log = logging.getLogger()
26 | target = None
27 |
28 | files_to_upload = []
29 | uploaded_hashes = []
30 |
31 | def __init__(self, host, configuration=None):
32 | self.config = configuration
33 | self.host = host
34 |
35 | def bootstrap(self):
36 | _create_result_folders()
37 | _setup_logging()
38 | self._detect_target()
39 |
40 | def run(self):
41 | """Run analysis.
42 | """
43 | self.bootstrap()
44 |
45 | self.log.debug("Starting analyzer from %s", getcwd())
46 | self.log.debug("Storing results at: %s", PATHS["root"])
47 |
48 | package = self._setup_analysis_package()
49 |
50 | if self.config.clock:
51 | set_wallclock(self.config.clock)
52 | self._analysis(package)
53 |
54 | return self._complete()
55 |
56 | def _complete(self):
57 | for f in self.files_to_upload:
58 | self._upload_file(f)
59 | return True
60 |
61 | #
62 | # Implementation details
63 | #
64 |
65 | def _detect_target(self):
66 | if self.config.category == "file":
67 | self.target = path.join("/tmp/", str(self.config.file_name))
68 | else: # It's not a file, but a URL
69 | self.target = self.config.target
70 |
71 | def _setup_analysis_package(self):
72 | # Do we have a suggestion about an analysis package?
73 | if self.config.package:
74 | suggestion = self.config.package
75 | elif self.config.category != "file":
76 | suggestion = "url"
77 | else:
78 | suggestion = None
79 | # Try to figure out what analysis package to use with this target
80 | kwargs = {"suggestion" : suggestion}
81 | package_class = choose_package_class(self.config.file_type,
82 | self.config.file_name, **kwargs)
83 | if not package_class:
84 | raise Exception("Could not find an appropriate analysis package")
85 | # Package initialization
86 | kwargs = {
87 | "options" : self.config.get_options(),
88 | "timeout" : self.config.timeout
89 | }
90 | return package_class(self.target, self.host, **kwargs)
91 |
92 | def _analysis(self, package):
93 | package.start()
94 | self.files_to_upload = package.touched_files
95 |
96 | def _upload_file(self, filepath):
97 | if not path.isfile(filepath):
98 | return
99 | # Check whether we've already dumped this file - in that case skip it
100 | try:
101 | hashsum = hash_file(sha256, filepath)
102 | if sha256 in self.uploaded_hashes:
103 | return
104 | except IOError as e:
105 | self.log.info("Error dumping file from path \"%s\": %s", filepath, e)
106 | return
107 | filename = "%s_%s" % (hashsum[:16], path.basename(filepath))
108 | upload_path = path.join("files", filename)
109 |
110 | try:
111 | upload_to_host(filepath, upload_path)
112 | self.uploaded_hashes.append(hashsum)
113 | except IOError as e:
114 | self.log.error("Unable to upload dropped file at path \"%s\": %s", filepath, e)
115 |
116 | def _create_result_folders():
117 | for _, folder in PATHS.items():
118 | if path.exists(folder):
119 | continue
120 | try:
121 | makedirs(folder)
122 | except OSError:
123 | pass
124 |
125 |
126 | def _setup_logging():
127 | """ Initialize logger. """
128 | logger = logging.getLogger()
129 | formatter = logging.Formatter("%(asctime)s [%(name)s] %(levelname)s: %(message)s")
130 |
131 | stream = logging.StreamHandler()
132 | stream.setFormatter(formatter)
133 | logger.addHandler(stream)
134 |
135 | netlog = NetlogHandler()
136 | netlog.setFormatter(formatter)
137 | logger.addHandler(netlog)
138 | logger.setLevel(logging.DEBUG)
139 |
140 |
141 |
142 | if __name__ == "__main__":
143 | success = False
144 | error = ""
145 |
146 | try:
147 | config = Config(cfg="analysis.conf")
148 | cuckoo = CuckooHost(config.ip, config.port)
149 | analyzer = Macalyzer(cuckoo, config)
150 | success = analyzer.run()
151 |
152 | except KeyboardInterrupt:
153 | error = "Keyboard Interrupt"
154 |
155 | except Exception as err:
156 | error_exc = format_exc()
157 | error = str(err)
158 | if len(analyzer.log.handlers):
159 | analyzer.log.exception(error_exc)
160 | else:
161 | stderr.write("{0}\n".format(error_exc))
162 | # Once the analysis is completed or terminated for any reason, we report
163 | # back to the agent, notifying that it can report back to the host.
164 | finally:
165 | # Establish connection with the agent XMLRPC server.
166 | server = Server("http://127.0.0.1:8000")
167 | server.complete(success, error, PATHS["root"])
168 |
--------------------------------------------------------------------------------
/analyzer/darwin/lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rodionovd/cuckoo-osx-analyzer/fabbbe86e59bc39f9054d4ed35299d5f6a1f2b01/analyzer/darwin/lib/__init__.py
--------------------------------------------------------------------------------
/analyzer/darwin/lib/common/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010-2015 Cuckoo Foundation.
2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
3 | # See the file 'docs/LICENSE' for copying permission.
4 |
--------------------------------------------------------------------------------
/analyzer/darwin/lib/common/config.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010-2015 Cuckoo Foundation.
2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
3 | # See the file 'docs/LICENSE' for copying permission.
4 |
5 | import ConfigParser
6 |
7 | class Config:
8 | def __init__(self, cfg):
9 | """@param cfg: configuration file."""
10 | config = ConfigParser.ConfigParser(allow_no_value=True)
11 | config.read(cfg)
12 |
13 | for section in config.sections():
14 | for name, raw_value in config.items(section):
15 | if name == "file_name":
16 | value = config.get(section, name)
17 | else:
18 | try:
19 | value = config.getboolean(section, name)
20 | except ValueError:
21 | try:
22 | value = config.getint(section, name)
23 | except ValueError:
24 | value = config.get(section, name)
25 | setattr(self, name, value)
26 |
27 | def get_options(self):
28 | """Get analysis options.
29 | @return: options dict.
30 | """
31 | # The analysis package can be provided with some options in the
32 | # following format:
33 | # option1=value1,option2=value2,option3=value3
34 | #
35 | # Here we parse such options and provide a dictionary that will be made
36 | # accessible to the analysis package.
37 | options = {}
38 | if hasattr(self, "options") and len(self.options) > 0:
39 | try:
40 | # Split the options by comma.
41 | fields = self.options.split(",")
42 | except ValueError:
43 | pass
44 | else:
45 | for field in fields:
46 | # Split the name and the value of the option.
47 | try:
48 | # Sometimes, we have a key without a value (i.e. it's a
49 | # command line argument), so we can't use the
50 | # `key, value = field.split("=", 1)` style here
51 | parts = field.split("=", 1)
52 | except ValueError:
53 | pass
54 | else:
55 | key = parts[0].strip()
56 | arg_prefix = "arg-"
57 | if not key.startswith(arg_prefix):
58 | # If the parsing went good, we add the option to the
59 | # dictionary.
60 | value = parts[1].strip()
61 | options[key] = value
62 | elif len(key) > len(arg_prefix):
63 | # Remove "arg-" prefix from the key
64 | key = key[4:]; parts[0] = key
65 | # Add this key (with a value maybe) to the args
66 | if "args" not in options: options["args"] = []
67 | options["args"] += parts
68 | return options
69 |
--------------------------------------------------------------------------------
/analyzer/darwin/lib/common/hashing.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010-2015 Cuckoo Foundation.
2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
3 | # See the file 'docs/LICENSE' for copying permission.
4 |
5 | BUFSIZE = 1024*1024
6 |
7 |
8 | def hash_file(method, path):
9 | """Calculates an hash on a file by path.
10 | @param method: callable hashing method
11 | @param path: file path
12 | @return: computed hash string
13 | """
14 | f = open(path, "rb")
15 | h = method()
16 | while True:
17 | buf = f.read(BUFSIZE)
18 | if not buf:
19 | break
20 | h.update(buf)
21 | return h.hexdigest()
22 |
--------------------------------------------------------------------------------
/analyzer/darwin/lib/common/rand.py:
--------------------------------------------------------------------------------
1 | import random
2 | import string
3 |
4 | def random_string(minimum, maximum=None):
5 | if maximum is None:
6 | maximum = minimum
7 |
8 | count = random.randint(minimum, maximum)
9 | return "".join(random.choice(string.ascii_letters) for x in xrange(count))
10 |
11 | def random_integer(digits):
12 | start = 10 ** (digits - 1)
13 | end = (10 ** digits) - 1
14 | return random.randint(start, end)
15 |
--------------------------------------------------------------------------------
/analyzer/darwin/lib/common/results.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010-2015 Cuckoo Foundation.
2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
3 | # See the file 'docs/LICENSE' for copying permission.
4 |
5 | import time
6 | import socket
7 | import logging
8 | from config import Config
9 |
10 | log = logging.getLogger(__name__)
11 |
12 | BUFSIZE = 1024*1024
13 |
14 | def upload_to_host(file_path, dump_path):
15 | nc = infd = None
16 | try:
17 | nc = NetlogFile(dump_path)
18 |
19 | infd = open(file_path, "rb")
20 | buf = infd.read(BUFSIZE)
21 | while buf:
22 | nc.send(buf, retry=False)
23 | buf = infd.read(BUFSIZE)
24 | except Exception as e:
25 | log.error("Exception uploading file %s to host: %s", file_path, e)
26 | finally:
27 | if infd:
28 | infd.close()
29 | if nc:
30 | nc.close()
31 |
32 | class NetlogConnection(object):
33 | def __init__(self, proto=""):
34 | config = Config(cfg="analysis.conf")
35 | self.hostip, self.hostport = config.ip, config.port
36 | self.sock, self.file = None, None
37 | self.proto = proto
38 |
39 | def connect(self):
40 | i = 1
41 | # this can loop forever, if we can't connect the whole analysis is useless anyways
42 | while True:
43 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
44 | try:
45 | s.connect((self.hostip, self.hostport))
46 | s.sendall(self.proto)
47 | except:
48 | time.sleep(i)
49 | i = min(i + 1, 60)
50 | else:
51 | self.sock = s
52 | self.file = s.makefile()
53 | break
54 |
55 | def send(self, data, retry=True):
56 | if not self.sock: self.connect()
57 |
58 | try:
59 | self.sock.sendall(data)
60 | except socket.error as e:
61 | if retry:
62 | self.connect()
63 | self.send(data, retry=False)
64 | else:
65 | raise
66 | except Exception as e:
67 | log.error("Unhandled exception in NetlogConnection: %s", str(e))
68 | # We really have nowhere to log this, if the netlog connection
69 | # does not work, we can assume that any logging won't work either.
70 | # So we just fail silently.
71 | self.close()
72 |
73 | def close(self):
74 | try:
75 | self.file.close()
76 | self.sock.close()
77 | except Exception:
78 | pass
79 |
80 | class NetlogFile(NetlogConnection):
81 | def __init__(self, filepath):
82 | self.filepath = filepath
83 | NetlogConnection.__init__(self, proto="FILE\n{0}\n".format(self.filepath))
84 | self.connect()
85 |
86 | class NetlogHandler(logging.Handler, NetlogConnection):
87 | def __init__(self):
88 | logging.Handler.__init__(self)
89 | NetlogConnection.__init__(self, proto="LOG\n")
90 | self.connect()
91 |
92 | def emit(self, record):
93 | msg = self.format(record)
94 | self.send("{0}\n".format(msg))
95 |
--------------------------------------------------------------------------------
/analyzer/darwin/lib/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rodionovd/cuckoo-osx-analyzer/fabbbe86e59bc39f9054d4ed35299d5f6a1f2b01/analyzer/darwin/lib/core/__init__.py
--------------------------------------------------------------------------------
/analyzer/darwin/lib/core/constants.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010-2015 Cuckoo Foundation.
2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
3 | # See the file 'docs/LICENSE' for copying permission.
4 |
5 | import os
6 | from tempfile import gettempdir
7 | from ..common.rand import random_string
8 |
9 | ROOT = os.path.join(gettempdir() + os.sep, random_string(6, 10))
10 |
11 | PATHS = {
12 | "root" : ROOT,
13 | "logs" : os.path.join(ROOT, "logs"),
14 | "files" : os.path.join(ROOT, "files"),
15 | "shots" : os.path.join(ROOT, "shots"),
16 | "memory" : os.path.join(ROOT, "memory"),
17 | "drop" : os.path.join(ROOT, "drop")
18 | }
19 |
--------------------------------------------------------------------------------
/analyzer/darwin/lib/core/data/signatures.yml:
--------------------------------------------------------------------------------
1 | system:
2 | is_success_condition: "retval == 0"
3 | args:
4 | - {name: "command", type: "char *"}
5 | retval_type: "int"
6 | category: "foobar"
7 |
8 | printf:
9 | is_success_condition: "retval > 0"
10 | args:
11 | - {name: "format", type: "char *"}
12 | retval_type: "int"
13 | category: "foobar"
14 |
15 | dlopen:
16 | is_success_condition: "retval > 0"
17 | args:
18 | - {name: "path", type: "char *"}
19 | - {name: "mode", type: "int"}
20 | retval_type: "void *"
21 | category: "foobar"
22 | library: "libdyld"
23 |
24 | dlsym:
25 | is_success_condition: "retval > 0"
26 | args:
27 | - {name: "handle", type: "void *"}
28 | - {name: "symbol", type: "char *"}
29 | retval_type: "void *"
30 | category: "foobar"
31 | library: "libdyld"
32 |
33 | fprintf:
34 | is_success_condition: "retval > 0"
35 | args:
36 | - {name: "stream", type: "void *"}
37 | - {name: "format", type: "char *"}
38 | retval_type: "int"
39 | category: "foobar"
40 |
41 | open:
42 | is_success_condition: "retval > 0"
43 | args:
44 | - {name: "path", type: "char *"}
45 | - {name: "oflag", type: "int"}
46 | retval_type: "int"
47 | category: "file"
48 |
49 | fopen:
50 | is_success_condition: "retval > 0"
51 | args:
52 | - {name: "filename", type: "char *"}
53 | - {name: "mode", type: "char *"}
54 | retval_type: "void *"
55 | category: "file"
56 |
57 | freopen:
58 | is_success_condition: "retval > 0"
59 | args:
60 | - {name: "filename", type: "char *"}
61 | - {name: "mode", type: "char *"}
62 | - {name: "stream", type: "void *"}
63 | retval_type: "void *"
64 | category: "file"
65 |
66 | rename:
67 | is_success_condition: "retval == 0"
68 | args:
69 | - {name: "old", type: "char *"}
70 | - {name: "new", type: "char *"}
71 | - {name: "state", type: "void *"}
72 | - {name: "flags", type: "uint64_t"}
73 | retval_type: "int"
74 | category: "file"
75 |
76 | copyfile:
77 | is_success_condition: "retval == 0"
78 | args:
79 | - {name: "from", type: "char *"}
80 | - {name: "to", type: "char *"}
81 | - {name: "state", type: "void *"}
82 | - {name: "flags", type: "uint32_t"}
83 | retval_type: "int"
84 | category: "file"
85 |
86 | remove:
87 | is_success_condition: "retval == 0"
88 | args:
89 | - {name: "path", type: "char *"}
90 | retval_type: "int"
91 | category: "file"
92 |
93 | unlink:
94 | is_success_condition: "retval == 0"
95 | args:
96 | - {name: "path", type: "char *"}
97 | retval_type: "int"
98 | category: "file"
99 |
100 | execve:
101 | is_success_condition: "retval != -1"
102 | args:
103 | - {name: "path", type: "char *"}
104 | - {name: "argv", type: "void *"}
105 | - {name: "envp", type: "void *"}
106 | retval_type: "int"
107 | category: "process"
108 | __ignore__: true
109 |
110 | fork:
111 | is_success_condition: "retval >= 0"
112 | args: []
113 | retval_type: "int"
114 | category: "process"
115 |
116 | socket:
117 | is_success_condition: "retval > 0"
118 | args:
119 | - {name: "domain", type: "int"}
120 | - {name: "type", type: "int"}
121 | - {name: "protocol", type: "int"}
122 | retval_type: "int"
123 | category: "network"
124 |
125 | # Signatures for tests.
126 | # Please don't remove them. Thanks!
127 |
128 | rb_isalpha:
129 | is_success_condition: "retval != 0"
130 | args:
131 | - {name: "character", type: "char"}
132 | retval_type: "int"
133 | category: "foobar"
134 |
135 | atoi:
136 | is_success_condition: "1==1"
137 | args:
138 | - {name: "str", type: "char *"}
139 | retval_type: "int"
140 | category: "foobar"
141 | library: "libsystem_c"
142 |
--------------------------------------------------------------------------------
/analyzer/darwin/lib/core/data/types.yml:
--------------------------------------------------------------------------------
1 | # ===============================================
2 | # Basic types
3 | #
4 | int: &int
5 | # We will print it with something like printf("%d", value)
6 | printf_specifier: "%d"
7 | # Is it a native C type (on OS X)?
8 | native: Yes
9 | # Alternative name for backward compatibility
10 | integer: *int
11 |
12 | unsigned int: &unsigned-int
13 | printf_specifier: "%ld"
14 | native: Yes
15 |
16 | long: &long
17 | printf_specifier: "%l"
18 | native: Yes
19 |
20 | unsigned long: &unsigned-long
21 | printf_specifier: "%lu"
22 | native: Yes
23 |
24 | unsigned long long: &unsigned-long-long
25 | printf_specifier: "%llu"
26 | native: Yes
27 |
28 | size_t: *unsigned-long
29 |
30 | char: &char
31 | printf_specifier: '"%c"'
32 | native: Yes
33 |
34 | float: &float
35 | printf_specifier: "%f"
36 | native: Yes
37 |
38 | double: &double
39 | printf_specifier: "%f"
40 | native: Yes
41 | #
42 | # Raw pointers: just dump their values (in *decimal* since dtrace will output
43 | # JSON that doesn't accept hex values)
44 | #
45 | "void *":
46 | <<: *unsigned-long-long
47 | cast: "unsigned long long"
48 | #
49 | # Strings
50 | #
51 | "char *": &char-pointer
52 | printf_specifier: '"%S"'
53 | native: No
54 | template: |-
55 | !!(${ARG}) ? copyinstr((uint64_t)${ARG}) : ""
56 | #
57 | # Arbitrary buffers
58 | #
59 | #buffer: &buffer
60 | # printf_specifier: '"%S"'
61 | # native: No
62 | # template: |-
63 | # ${ARG} != (int64_t)NULL ? stringof(copyin(${ARG}, ${SIZE_ARG})) : ""
64 | #
65 | # Fixed length C types
66 | #
67 | int8_t: &int8_t
68 | printf_specifier: "%d"
69 | native: Yes
70 |
71 | uint8_t: &uint8_t
72 | printf_specifier: "%u"
73 | native: Yes
74 |
75 | int16_t: &int16_t
76 | printf_specifier: "%d"
77 | native: Yes
78 |
79 | uint16_t: &uint16_t
80 | printf_specifier: "%u"
81 | native: Yes
82 |
83 | int32_t: &int32_t
84 | printf_specifier: "%d"
85 | native: Yes
86 |
87 | uint32_t: &uint32_t
88 | printf_specifier: "%u"
89 | native: Yes
90 |
91 | int64_t: &int64_t
92 | printf_specifier: "%lld"
93 | native: Yes
94 |
95 | uint64_t: &uint64_t
96 | printf_specifier: "%llu"
97 | native: Yes
98 |
99 | #
100 | # Structures for tests.
101 | # Please don't remove them. Thanks!
102 | #
103 |
104 | test_t:
105 | native: No
106 | struct:
107 | hash: "int"
108 | base: "test_internal_t *"
109 | description: "char *"
110 |
111 | test_internal_t:
112 | native: No
113 | struct:
114 | abc: "double *"
115 | hfa: "size_t"
116 | sss: "char *"
117 |
118 | test_extra_t:
119 | native: No
120 | struct:
121 | foo: int
122 | bar: uint64_t
123 |
124 | #
125 | # Your custom data types
126 | #
127 |
--------------------------------------------------------------------------------
/analyzer/darwin/lib/core/filetimes.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2009, David Buxton
2 | # All rights reserved.
3 | #
4 | # Redistribution and use in source and binary forms, with or without
5 | # modification, are permitted provided that the following conditions are
6 | # met:
7 | #
8 | # * Redistributions of source code must retain the above copyright
9 | # notice, this list of conditions and the following disclaimer.
10 | # * Redistributions in binary form must reproduce the above copyright
11 | # notice, this list of conditions and the following disclaimer in the
12 | # documentation and/or other materials provided with the distribution.
13 | #
14 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
15 | # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
16 | # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
17 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18 | # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
23 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 | """Tools to convert between Python datetime instances and Microsoft times.
26 | """
27 | from calendar import timegm
28 |
29 |
30 | # http://support.microsoft.com/kb/167296
31 | # How To Convert a UNIX time_t to a Win32 FILETIME or SYSTEMTIME
32 | EPOCH_AS_FILETIME = 116444736000000000 # January 1, 1970 as MS file time
33 | HUNDREDS_OF_NANOSECONDS = 10000000
34 |
35 |
36 | def dt_to_filetime(dt, delta_from_utc):
37 | """Converts a datetime to Microsoft filetime format.
38 |
39 | >>> "%.0f" % dt_to_filetime(datetime(2009, 7, 25, 23, 0))
40 | '128930364000000000'
41 |
42 | >>> "%.0f" % dt_to_filetime(datetime(1970, 1, 1, 0, 0, tzinfo=utc))
43 | '116444736000000000'
44 |
45 | >>> "%.0f" % dt_to_filetime(datetime(1970, 1, 1, 0, 0))
46 | '116444736000000000'
47 |
48 | >>> dt_to_filetime(datetime(2009, 7, 25, 23, 0, 0, 100))
49 | 128930364000001000
50 | """
51 | dt += delta_from_utc
52 | ft = EPOCH_AS_FILETIME + (timegm(dt.timetuple()) * HUNDREDS_OF_NANOSECONDS)
53 | return ft + (dt.microsecond * 10)
54 |
--------------------------------------------------------------------------------
/analyzer/darwin/lib/core/host.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (C) 2015 Dmitry Rodionov
3 | # This software may be modified and distributed under the terms
4 | # of the MIT license. See the LICENSE file for details.
5 |
6 | import yaml
7 | import socket
8 | import logging
9 | from os import path
10 | from bson import BSON
11 | from datetime import datetime
12 | from subprocess import check_output, CalledProcessError
13 |
14 | from filetimes import dt_to_filetime
15 |
16 | log = logging.getLogger(__name__)
17 |
18 | def signatures_path():
19 | return path.join(path.dirname(path.abspath(__file__)), "data", "signatures.yml")
20 |
21 | class CuckooHost(object):
22 | """ Sending analysis results back to the Cuckoo Host.
23 |
24 | Currently it only supports sending results about API calls via send_api(),
25 | see `apicalls` module.
26 | """
27 | sockets = {
28 | # Each target process has its own results server on the host, so
29 | # we setup as many sockets as we have targets to analyse.
30 | }
31 | descriptions = {
32 | # We don't want to explain APIs every single time they're about to be
33 | # send to the host, so we explain them once and then just refer to them
34 | # via an unique ID.
35 | }
36 | launch_times = {
37 | # Since Cuckoo host expects us to send relative times, we remember when
38 | # every target was launched.
39 | }
40 | human_readable_info = {
41 | # Here goes all the additional information about APIs like category,
42 | # arguments names and so on. See date/apis.json for more details.
43 | }
44 |
45 | def __init__(self, host_ip, host_port):
46 | self.address = host_ip
47 | self.port = host_port
48 | self._load_human_readable_info()
49 |
50 | def send_api(self, thing):
51 | """ Sends a new API notification to the Cuckoo host """
52 | pid = thing.pid
53 | api = thing.api
54 |
55 | # We're required to report results of tracing a target process to
56 | # *its own* result server. So create a communication socket...
57 | if pid not in self.sockets:
58 | self.sockets[pid] = self._create_socket()
59 | if not self.sockets[pid]:
60 | raise Exception("CuckooHost error: could not create socket.")
61 | # ... and don't forget to explain every single API call again
62 | self.descriptions.setdefault(pid, ["__process__", "__thread__"])
63 | self._send_new_process(thing)
64 | try:
65 | lookup_idx = self.descriptions[pid].index(api)
66 | except ValueError:
67 | self.descriptions[pid].append(api)
68 | lookup_idx = len(self.descriptions[pid]) - 1
69 | self._send_api_description(lookup_idx, thing)
70 |
71 | # Here's an api object:
72 | # {
73 | # "I" : (int),
74 | # "T" : (int),
75 | # "t" : (int)