Skip to content

Commit 3f0541d

Browse files
committed
Add GUI actuation
1 parent 45d6e72 commit 3f0541d

File tree

5 files changed

+213
-68
lines changed

5 files changed

+213
-68
lines changed

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ sample IDs with VirusTotal. These periodic tasks are managed by
4242
Samples become available once per day. The `genindex.sh` just builds the
4343
(very ugly) web page every 10 minutes.
4444

45+
GUI Analysis
46+
------------
47+
48+
In order for the GUI analysis and actuation to work, you will need to
49+
use this branch of PANDA:
50+
51+
https://github.com/moyix/panda/tree/wip/unsafememaccess
52+
53+
And then symlink the `pmemaddressspace.py` script into Volatility's
54+
`volatility/plugins/addrspaces` subdirectory.
55+
4556
Disclaimer
4657
----------
4758

scripts/click_buttons.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env python
2+
3+
import time
4+
import logging
5+
import string
6+
import listwins
7+
import random
8+
from mon_util import mon_cmd
9+
10+
clickables = set([
11+
"NEXT", "GO", "INSTALL", "YES", "OK", "DONE",
12+
"IAGREE", "AGREE", "FINISH", "UPDATE", "IACCEPT",
13+
"ACCEPT", "DECLINE", "CONTINUE", "RUN", "SAVE",
14+
"DOWNLOAD", "SKIP",
15+
])
16+
17+
def normalize(s):
18+
return ''.join(c for c in s if c in string.letters).upper()
19+
20+
def setup(os, sock):
21+
listwins.setup(os, sock)
22+
23+
def move_to(mon, x, y, absolute=True):
24+
if absolute:
25+
# Reset to top-left
26+
mon_cmd("mouse_move -2000 -2000\n", mon)
27+
time.sleep(1)
28+
mon_cmd("mouse_move {0} {1}\n".format(x, y), mon)
29+
time.sleep(.5)
30+
31+
def click(mon):
32+
mon_cmd("mouse_button 1\n", mon) # Button 1 down
33+
time.sleep(.1)
34+
mon_cmd("mouse_button 0\n", mon) # Button 1 up
35+
36+
def match(s, clickables):
37+
return s and any((s in c and not abs(len(s)-len(c)) > 10) for c in clickables)
38+
39+
def click_buttons(mon):
40+
candidates = []
41+
for w in listwins.get_windows():
42+
normed = normalize(str(w.strName or ''))
43+
if match(normed, clickables):
44+
x1, y1, x2, y2 = map(int, w.rcClient.get_tup())
45+
clickx, clicky = (x1+x2)/2, (y1+y2)/2
46+
# Don't click on things that are near the edges
47+
# Edges here means the outer 10% of the screen
48+
width, height = 1024, 768
49+
if (clickx < width * .10 or clickx > width * .90 or
50+
clicky < height * .10 or clicky > height * .90):
51+
continue
52+
if not w.Visible: continue
53+
candidates.append( (str(w.strName or ''), clickx, clicky) )
54+
55+
if not candidates: return
56+
57+
# If we have multiple, just pick one at random
58+
(name, clickx, clicky) = random.choice(candidates)
59+
logging.info("Clicking on %s at (%d,%d)" % (name, clickx, clicky))
60+
move_to(mon, clickx, clicky)
61+
click(mon)

scripts/listwins.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python
2+
3+
# Volatility setup boilerplate
4+
import volatility.conf as conf
5+
import volatility.registry as registry
6+
registry.PluginImporter()
7+
config = conf.ConfObject()
8+
import volatility.commands as commands
9+
import volatility.addrspace as addrspace
10+
registry.register_global_options(config, commands.Command)
11+
registry.register_global_options(config, addrspace.BaseAddressSpace)
12+
config.parse_options()
13+
14+
import volatility.plugins.gui.windows as windows
15+
16+
main_desktop = None
17+
def get_windows():
18+
global main_desktop, config
19+
winlist = []
20+
if main_desktop is None:
21+
# First time. Do the hard work of finding the desktop list and
22+
# pick the one with the most windows as our "main" desktop
23+
wplug = windows.Windows(config)
24+
desktops = []
25+
for winsta, atom_tables in wplug.calculate():
26+
for desktop in winsta.desktops():
27+
num = 0
28+
for wnd, level in desktop.windows(desktop.DeskInfo.spwnd):
29+
num += 1
30+
winlist.append(wnd)
31+
desktops.append( (num, desktop) )
32+
desktops.sort()
33+
main_desktop = desktops[-1][1]
34+
else:
35+
for wnd, level in main_desktop.windows(main_desktop.DeskInfo.spwnd):
36+
winlist.append(wnd)
37+
return winlist
38+
39+
def setup(os, loc):
40+
global config
41+
config.PROFILE = os
42+
config.LOCATION = loc
43+
# Do one scan to initialize things
44+
get_windows()
45+
46+
if __name__ == "__main__":
47+
setup("Win7SP1x86", "qemu:///home/moyix/qemu_pmem.0")
48+
49+
from IPython import embed
50+
embed()

scripts/mon_util.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import logging
2+
import time
3+
import string
4+
5+
keymap = {
6+
'-': 'minus',
7+
'=': 'equal',
8+
'[': 'bracket_left',
9+
']': 'bracket_right',
10+
';': 'semicolon',
11+
'\'': 'apostrophe',
12+
'\\': 'backslash',
13+
',': 'comma',
14+
'.': 'dot',
15+
'/': 'slash',
16+
'*': 'asterisk',
17+
' ': 'spc',
18+
'_': 'shift-minus',
19+
'+': 'shift-equal',
20+
'{': 'shift-bracket_left',
21+
'}': 'shift-bracket_right',
22+
':': 'shift-semicolon',
23+
'"': 'shift-apostrophe',
24+
'|': 'shift-backslash',
25+
'<': 'shift-comma',
26+
'>': 'shift-dot',
27+
'?': 'shift-slash',
28+
'\n': 'ret',
29+
}
30+
31+
def mon_cmd(s, mon):
32+
mon.write(s)
33+
logging.info(mon.read_until("(qemu)"))
34+
35+
def guest_type(s, mon):
36+
for c in s:
37+
if c in string.ascii_uppercase:
38+
key = 'shift-' + c.lower()
39+
else:
40+
key = keymap.get(c, c)
41+
mon_cmd('sendkey {0}\n'.format(key), mon)
42+
time.sleep(.1)

scripts/runmal.py

+49-68
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import os
77
import shutil
88
import socket
9-
import string
109
import subprocess
1110
import sys
1211
import telnetlib
@@ -16,32 +15,10 @@
1615
import pefile
1716
import sqlite3
1817
import hashlib
19-
20-
keymap = {
21-
'-': 'minus',
22-
'=': 'equal',
23-
'[': 'bracket_left',
24-
']': 'bracket_right',
25-
';': 'semicolon',
26-
'\'': 'apostrophe',
27-
'\\': 'backslash',
28-
',': 'comma',
29-
'.': 'dot',
30-
'/': 'slash',
31-
'*': 'asterisk',
32-
' ': 'spc',
33-
'_': 'shift-minus',
34-
'+': 'shift-equal',
35-
'{': 'shift-bracket_left',
36-
'}': 'shift-bracket_right',
37-
':': 'shift-semicolon',
38-
'"': 'shift-apostrophe',
39-
'|': 'shift-backslash',
40-
'<': 'shift-comma',
41-
'>': 'shift-dot',
42-
'?': 'shift-slash',
43-
'\n': 'ret',
44-
}
18+
import tempfile
19+
import atexit
20+
from mon_util import mon_cmd, guest_type
21+
import click_buttons
4522

4623
def md5_for_file(fname, block_size=2**20):
4724
f = open(fname, 'rb')
@@ -55,18 +32,39 @@ def md5_for_file(fname, block_size=2**20):
5532
f.close()
5633
return digest
5734

58-
def mon_cmd(s, mon):
59-
mon.write(s)
60-
logging.info(mon.read_until("(qemu)"))
35+
def cleanup():
36+
# Move sample to the finished queue
37+
logging.info("Moving sample into 'finished' queue.")
38+
shutil.move(
39+
sample_file,
40+
os.path.join(queuedir, 'finished', sample_name)
41+
)
42+
43+
# Cleanup
44+
os.unlink(qemu_socket)
45+
os.unlink(iso_file)
46+
os.unlink(new_qcow)
47+
panda_stdout.close()
48+
panda_stderr.close()
49+
50+
# Write to DB
51+
conn = sqlite3.connect(database)
52+
c = conn.cursor()
53+
while True:
54+
try:
55+
c.execute('INSERT INTO samples VALUES(?,?,?)', (run_id, sample_name, sample_md5))
56+
break
57+
except sqlite3.OperationalError:
58+
pass
59+
c.close()
60+
conn.commit()
61+
conn.close()
62+
63+
# All done, write the stamp
64+
stampfile = os.path.join(logdir, 'stamps', run_id)
65+
open(stampfile, 'w').close()
6166

62-
def guest_type(s, mon):
63-
for c in s:
64-
if c in string.ascii_uppercase:
65-
key = 'shift-' + c.lower()
66-
else:
67-
key = keymap.get(c, c)
68-
mon_cmd('sendkey {0}\n'.format(key), mon)
69-
time.sleep(.1)
67+
atexit.register(cleanup)
7068

7169
conf = ConfigParser.ConfigParser()
7270
conf.read(sys.argv[1])
@@ -194,6 +192,14 @@ def guest_type(s, mon):
194192
logging.info("Copying file to desktop.")
195193
guest_type(r"copy D:\{0} C:\Users\qemu\Desktop".format(sample_name) + '\n', mon)
196194

195+
# Create our memory access socket
196+
qemu_socket = tempfile.mktemp()
197+
logging.info("Creating memory access socket: {0}".format(qemu_socket))
198+
mon_cmd("pmemaccess {0}\n".format(qemu_socket), mon)
199+
200+
# Warm up the Volatility part
201+
click_buttons.setup("Win7SP1x64" if is_64bit else "Win7SP1x86", "qemu://" + qemu_socket)
202+
197203
# Run the sample
198204
logging.info("Starting sample.")
199205
# Handle 3 cases: driver, exe, dll
@@ -210,40 +216,15 @@ def guest_type(s, mon):
210216

211217
# Wait
212218
logging.info("Sleeping for {0} seconds...".format(exec_time))
213-
time.sleep(exec_time)
219+
# Every 30 seconds, look for a button
220+
period = 10
221+
for _ in range(exec_time / period):
222+
time.sleep(period)
223+
click_buttons.click_buttons(mon)
214224

215225
# End the record
216226
logging.info("Ending record.")
217227
mon_cmd("end_record\n", mon)
218228
logging.info("Quitting PANDA.")
219229
mon.write("q\n")
220230

221-
# Move sample to the finished queue
222-
logging.info("Moving sample into 'finished' queue.")
223-
shutil.move(
224-
sample_file,
225-
os.path.join(queuedir, 'finished', sample_name)
226-
)
227-
228-
# Cleanup
229-
os.unlink(iso_file)
230-
os.unlink(new_qcow)
231-
panda_stdout.close()
232-
panda_stderr.close()
233-
234-
# Write to DB
235-
conn = sqlite3.connect(database)
236-
c = conn.cursor()
237-
while True:
238-
try:
239-
c.execute('INSERT INTO samples VALUES(?,?,?)', (run_id, sample_name, sample_md5))
240-
break
241-
except sqlite3.OperationalError:
242-
pass
243-
c.close()
244-
conn.commit()
245-
conn.close()
246-
247-
# All done, write the stamp
248-
stampfile = os.path.join(logdir, 'stamps', run_id)
249-
open(stampfile, 'w').close()

0 commit comments

Comments
 (0)