├── .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 | [![Build Status](https://travis-ci.org/rodionovd/cuckoo-osx-analyzer.svg?branch=master)](https://travis-ci.org/rodionovd/cuckoo-osx-analyzer) [![GitHub version](https://badge.fury.io/gh/rodionovd%2Fcuckoo-osx-analyzer.svg)](http://badge.fury.io/gh/rodionovd%2Fcuckoo-osx-analyzer) [![Code Climate](https://codeclimate.com/github/rodionovd/cuckoo-osx-analyzer/badges/gpa.svg)](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)