Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Old machine bootloader #527

Merged
merged 28 commits into from
Mar 30, 2020
Merged

Conversation

VakarisZ
Copy link
Contributor

@VakarisZ VakarisZ commented Jan 14, 2020

What is this?

Fixes #479

Add any further explanations here.

TODO

  • Make pyinstaller bootloader launch custom C code
  • Create C file that would communicate with island on Windows
  • FIX Windows XP requests support #528
  • Create C file that would communicate with island on Linux
  • Make island recognize an display machines where bootloader communicated, but monkey did not
  • Refactor pyinstaller alterations into a patch, which we can apply to any incoming new pyinstaller version
  • Alter deployment script to include bootloader and pyinstaller dev version with patch
  • Test on OS compat env

TDD

Prerequisites

Old machine bootloader program is incorporated into pyinstaller bootloader. Pyinstaller bootloader is the first thing that starts when a built monkey is launched, thus our code is launched before monkey and has access to monkey flags. This allows us to write code that would inspect the current machine and send requests to island directly or via tunnel and decide to continue with launching monkey code or to quit.

This is a multilevel problem so TDD is separated into these sections that represent corresponding layers:

  • Bootloader program
  • Connections and tunneling
  • Island server and data gathered
  • Data representation

Bootloader program

How to setup bootloader development env.

We have pyinstaller forks, one for linux and one for windows. Pull them and get relevant bootloader dev env.

To update pyinstaller we'll have to pull from pyinstaller git, merge and rebuild pyinstaller bootloader. Then do a release and add built pyinstaller bootloader.
To update bootloader we'll have to push to our forks and rebuild pyinstaller bootloader. Then do a release and add built pyinstaller bootloader.

Monkey build process

Building the monkey will require custom pyinstaller bootloader binary. Depending on OS this binary should be downloaded from our pyinstaller for release and placed into a pyinstaller install folder. Maybe this can be automated via deployment scripts.

Bootloader program workflow

  1. Monkey binary is ran.
  2. Bootloader starts and parses monkey flags to extract tunnel and server.
  3. Bootloader collects information about system: OS, IP, Hostname.
  4. Bootloader looks at current OS info and determines whether to run the monkey or to quit after it finishes.
  5. Bootloader changes server port from x to x+1 OR we can have a dedicated flag.
  6. Bootloader forms http request with OS, IP, Hostname and will monkey be ran info.
  7. Bootloader sends HTTP request where a proxy server is listening.
  8. If bootloader didn't get proper response it tries to use tunnel.

Connections and tunneling

Monkey

Tunneling on monkey is expanded to tunnel http traffic.

Island

Island starts a basic http server on a separate thread. It connects to the mongodb and gets port to serve on. It's either island's port +1 or a dedicated config value. When this HTTP proxy server gets a request it passes it to the /api/bootloader endpoint on the main server and also forwards back the response.

Island server and data gathered

Island receives data on an api/bootloader endpoint and stores it on a separate collection. Each bootloader telemetry has the following fields: OS, IP list, Hostname and if monkey will be run. Upon receiving bootloader telemetry island finds and updates bootloader_state variable on corresponding node.
bootloader_state can be one of three values:

  1. Bottloader didn't run (default)
  2. Bootloader ran, monkey will run ( if monkey_will_run value is True in bootloader telem )
  3. Bootloader ran, monkey will not run ( if monkey_will_run value is False in bootloader telem )

Data representation

Map

States of node will be the following:

  1. Scanned node (exploited: False bootloader_state: not_started on node)
  2. Exploited node (exploited: True bootloader_state: not_started on node)
  3. Machine too old (exploited: True bootloader_state: monkey_won't_run on node)
    OR
  4. Monkey will run (exploited: True bootloader_state: monkey_will_run on node)
  5. Monkey is running.
  6. Monkey died.

To recap 2 states with corresponding UI will be added: Machine too old to run monkey and Monkey will run.

Report

When generating report island will query mongodb to find if there are any entries of bootloader telemetry with bootloader_state: monkey_won't_run. If so, outdated machine issue is generated.

Testing

Bootloader binary

Manual tests on some old windows and linux machines. Also, on some new ones.

Tunneling

Windows machine will be added to tunneling env in monkeyzoo. New blackbox test will run tunneling test as usual, but will also verify that each bootloader communicated with island.

Data gathering and UI

Tested manually

Dev. env. deployment

This feature changes development environment in the following way:

  • We need custom pyinstaller bootloader to build monkeys

So deployment scripts will have to be altered to do:

  1. Download pyinstaller bootloader from our pyinstaller fork release
  2. Replace default bootloader in pyinstaller folder

@ShayNehmad ShayNehmad self-requested a review January 20, 2020 11:27
@ShayNehmad ShayNehmad added Feature Issue that describes a new feature to be implemented. Monkey labels Jan 20, 2020
@ShayNehmad ShayNehmad added this to the 1.8.0 milestone Jan 20, 2020
@danielguardicore
Copy link
Contributor

Some notes on the design.

For building the bootloader, I suggest doing the following
1 - Creating a patch file with all the changes, that can automatically be applied to the Pyinstaller source code.
2 - Making sure the building can be automated.
I suggest having this code be part of the Monkey repository, but under a different top level folder than monkey.

Regarding libcurl and openssl. If we are connecting to the Monkey, then we probably only need libcurl. I don't think we need our version of openssl.

I think the open port should not be hard coded to be 5001, but rather +1 on the current server port. This is a simple change and will also help in the Monkey Island side. We will also need to update documentation on the Island.

Regarding data collected. I suggest the following fields

  • All IP addresses (IPv4, IPv6)
  • Computer hostname
  • OS version string (compatible with current Monkey)

Regarding saving the bootloader message. I suggest turning it into a telemetry message then it will be saved in that collection automatically. This doesn't require us to really include JSON parsing in the bootloader, since it's a pretty hard coded message.

@VakarisZ
Copy link
Contributor Author

VakarisZ commented Feb 4, 2020

  1. As @ShayNehmad suggested, it's probably best to just make forks of pyinstaller. Those forks will store the code and binaries.
  2. Automated as in run a command -> get bootloader? That is already automated in pyinstaller. If by automated you mean run a command -> get bootloader and replace current bootloader binary, then IDK if that's necessary.
    Regarding proxy server port there's two options: current server port +1 or a separate config field and a separate flag for monkey. How is island port configuration happening right now, how can you change it?
    Regarding telemetries, now island expects to get telemetries from monkey, so it looks for guid field. To be less error prone we should save bootloader telem on a separate collection, since it has different structure, no?

@danielguardicore
Copy link
Contributor

danielguardicore commented Feb 4, 2020

1 - Ok.
2 - Ok
3 - I think it should be +1 and then we don't need to pass more flags.

@danielguardicore
Copy link
Contributor

Regarding pyinstaller env, I think the best for the dev enviornment is just building from git source rather than Pypi. Pip supports that. We should explicitly list our repo and then we don't need to copy/override things.

@danielguardicore
Copy link
Contributor

Regarding basic Island http server. Is this also a tornado/flask server or something else?

@VakarisZ
Copy link
Contributor Author

VakarisZ commented Feb 5, 2020

ATM it's vanilla python BaseHTTPServer

Copy link
Contributor

@ShayNehmad ShayNehmad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't finished my review yet, but some comments are here already. Will continue tomorrow

Comment on lines 219 to 222

#TODO change back
time.sleep(WormConfiguration.keep_tunnel_open_time)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO left in the code

@@ -108,7 +110,20 @@ def log_message(self, format_string, *args):
class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler):
timeout = 30 # timeout with clients, set to None not to make persistent connection
proxy_via = None # pseudonym of the proxy in Via header, set to None not to modify original Via header
protocol_version = "HTTP/1.1"
#protocol_version = "HTTP/1.1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't leave commented out code

self.send_response(200)
self.end_headers()
self.wfile.write(r.content)
self.connection.close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we wrap this function with try except finally and then close the connection on the finally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a necessity, but we can

logger.info("Starting bootloader server")
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
bootloader_server_thread = Thread(target=BootloaderHttpServer(mongo_url).serve_forever, daemon=True)
# island_server_thread = Thread(target=start_island_server)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented out code

Comment on lines 36 to 42
bootloader_server_thread = Thread(target=BootloaderHttpServer(mongo_url).serve_forever, daemon=True)
# island_server_thread = Thread(target=start_island_server)

bootloader_server_thread.start()
#island_server_thread.start()
start_island_server()
bootloader_server_thread.join()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract all this to function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? It's already the only lines lines in this function.

Comment on lines 28 to 38
@staticmethod
def parse_bootloader_request_linux(request_data: bytes) -> Dict[str, str]:
parsed_data = json.loads(request_data.decode().replace("\n", "")
.replace("NAME=\"", "")
.replace("\"\"", "\"")
.replace("\":\",", "\":\"\","))
return parsed_data

@staticmethod
def parse_bootloader_request_windows(request_data: bytes) -> Dict[str, str]:
return json.loads(request_data.decode("utf-16", "ignore"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cover this with UTs - the tests will be good documentation as well

else:
return make_response({"status": "OS_NOT_FOUND"}, 404)

resp = BootloaderService.parse_bootloader_telem(data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename resp

Copy link
Contributor

@ShayNehmad ShayNehmad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more comments

Comment on lines 259 to 260
info = this.props.item.group.includes('monkey', 'manual') ? this.infectedAssetInfo(this.props.item) :
this.props.item.group !== 'island' ? this.assetInfo(this.props.item) : this.islandAssetInfo();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we re-write this conditinal to be more explicit and clearer? it's very hard to understand even upon second reading

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rewrite it, even though this is not mine.

Comment on lines 1 to 7
// This list must correspond to the one on back end in cc/services/utils/node_groups.py
const groupNames = ['clean_unknown', 'clean_linux', 'clean_windows', 'exploited_linux', 'exploited_windows', 'island',
'island_monkey_linux', 'island_monkey_linux_running', 'island_monkey_windows', 'island_monkey_windows_running',
'manual_linux', 'manual_linux_running', 'manual_windows', 'manual_windows_running', 'monkey_linux',
'monkey_linux_running', 'monkey_windows', 'monkey_windows_running'];
'island_monkey_linux', 'island_monkey_linux_running', 'island_monkey_linux_starting', 'island_monkey_windows',
'island_monkey_windows_running', 'island_monkey_windows_starting', 'manual_linux', 'manual_linux_running',
'manual_windows', 'manual_windows_running', 'monkey_linux', 'monkey_linux_running', 'monkey_windows',
'monkey_windows_running', 'monkey_windows_starting', 'monkey_linux_starting', 'monkey_windows_old',
'monkey_linux_old' ];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a joined JSON file in common somewhere is a better solution?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I exported it to a file, although it would be better to just send it from backend.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought lets refactor

Comment on lines 9 to 17
tst1 = NodeGroups.get_group_by_keywords(['island']) == NodeGroups.ISLAND
tst2 = NodeGroups.get_group_by_keywords(['running', 'linux', 'monkey']) == NodeGroups.MONKEY_LINUX_RUNNING
tst3 = NodeGroups.get_group_by_keywords(['monkey', 'linux', 'running']) == NodeGroups.MONKEY_LINUX_RUNNING
tst4 = False
try:
NodeGroups.get_group_by_keywords(['bogus', 'values', 'from', 'long', 'list', 'should', 'fail'])
except NoGroupsFoundException:
tst4 = True
self.assertTrue(tst1 and tst2 and tst3 and tst4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool test :)
It would be better to assert each statement separately I think

@danielguardicore
Copy link
Contributor

@VakarisZ I think you need to update the pip requirement to aim at our pyinstaller fork

monkey/monkey_island/cc/services/node.py Show resolved Hide resolved

class TestNodeGroups(TestCase):

def test_get_group_by_keywords(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use self.assertEqual(NodeStates.get_by_keywords(['island']), NodeStates.ISLAND) instead of assertTrue for tests 1,2 and 3
Use self.assertRaises(ExpectedException, afunction) for test 4

@codecov
Copy link

codecov bot commented Mar 17, 2020

Codecov Report

Merging #527 into develop will increase coverage by 0.56%.
The diff coverage is 64.46%.

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #527      +/-   ##
===========================================
+ Coverage    56.15%   56.72%   +0.56%     
===========================================
  Files          105      112       +7     
  Lines         3588     3820     +232     
===========================================
+ Hits          2015     2167     +152     
- Misses        1573     1653      +80     
Impacted Files Coverage Δ
monkey/infection_monkey/transport/http.py 21.15% <14.28%> (-1.19%) ⬇️
monkey/monkey_island/cc/services/edge.py 26.88% <20.00%> (-0.90%) ⬇️
monkey/monkey_island/cc/services/node.py 30.73% <35.13%> (+0.94%) ⬆️
monkey/monkey_island/cc/services/bootloader.py 47.16% <47.16%> (ø)
monkey/monkey_island/cc/utils.py 42.00% <60.00%> (+2.00%) ⬆️
monkey/monkey_island/cc/resources/bootloader.py 60.86% <60.86%> (ø)
...key/monkey_island/cc/services/utils/node_states.py 97.67% <97.67%> (ø)
...nkey/monkey_island/cc/resources/bootloader_test.py 100.00% <100.00%> (ø)
...onkey/monkey_island/cc/services/bootloader_test.py 100.00% <100.00%> (ø)
...nkey_island/cc/services/utils/bootloader_config.py 100.00% <100.00%> (ø)
... and 8 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8eab25e...1ff6a91. Read the comment docs.

@ShayNehmad ShayNehmad merged commit 4da6a6a into guardicore:develop Mar 30, 2020
@VakarisZ VakarisZ deleted the old_machine_bootloader branch October 15, 2020 08:29
shreyamalviya added a commit that referenced this pull request Feb 1, 2022
As per #527, this code was
added for the bootloader. Now that we're removing the bootloader, this
is no longer needed.
ilija-lazoroski pushed a commit that referenced this pull request Feb 1, 2022
As per #527, this code was
added for the bootloader. Now that we're removing the bootloader, this
is no longer needed.
ilija-lazoroski pushed a commit that referenced this pull request Feb 1, 2022
As per #527, this code was
added for the bootloader. Now that we're removing the bootloader, this
is no longer needed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature Issue that describes a new feature to be implemented.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Windows XP requests support Create monkey bootloader for obsolete systems
3 participants