Merge pull request #401 from rsmudge/armitage

Armitage 05.21.12
unstable
sinn3r 2012-05-20 20:01:12 -07:00
commit 3f1a72932e
18 changed files with 297 additions and 149 deletions

Binary file not shown.

View File

@ -1,6 +1,24 @@
Armitage Changelog
==================
21 May 12
---------
- Added a hack to prevent the input area from flickering when the
prompt changes.
- Updated the color palette to something a little more subtle.
- Added an optimization to how modules are launched. This will make
a difference for team use in high latency situations.
- Rewrote MSF Scans feature to use console queue. This option is more
reliable and it makes the code easier to follow.
- Added a hack to combine chat message writes with a read request.
This will make the event log more responsive in a high latency
situation (can't you tell I care about this "situation")
- Fixed text highlights through Ctrl+F on Windows. UNIX platforms
were always OK. Another good reason to not use these tools on
Windows. Ever.
- View -> Downloads Sync Files feature now works on Windows. It looks
like leaving those pesky :'s in the file paths is bad.
17 May 12
---------
- Fixed bug with loot/download viewer breaking with a font resize.

View File

@ -3,7 +3,7 @@
<center><h1>Armitage 1.44-dev</h1></center>
<p>An attack management tool for Metasploit&reg;
<br />Release: 17 May 12</p>
<br />Release: 21 May 12</p>
<br />
<p>Developed by:</p>

View File

@ -41,18 +41,18 @@ armitage.show_all_commands.boolean=true
armitage.application_title.string=Armitage
console.color_0.color=\#ffffff
console.color_1.color=\#000000
console.color_2.color=\#000080
console.color_3.color=\#009000
console.color_4.color=\#ff0000
console.color_5.color=\#800000
console.color_6.color=\#a000a0
console.color_7.color=\#ff8000
console.color_8.color=\#ffff00
console.color_9.color=\#00ff00
console.color_10.color=\#009090
console.color_11.color=\#00ffff
console.color_12.color=\#0000ff
console.color_13.color=\#ff00ff
console.color_2.color=\#3465A4
console.color_3.color=\#4E9A06
console.color_4.color=\#EF2929
console.color_5.color=\#CC0000
console.color_6.color=\#75507B
console.color_7.color=\#C4A000
console.color_8.color=\#FCE94F
console.color_9.color=\#8AE234
console.color_10.color=\#069A9A
console.color_11.color=\#34E2E2
console.color_12.color=\#729FCF
console.color_13.color=\#AD7FA8
console.color_14.color=\#808080
console.color_15.color=\#c0c0c0
console.show_colors.boolean=true

View File

@ -1,4 +1,4 @@
^(..:..:..) \[\*\] (.*) $1 \cA[*]\o $2
^\[\*\] (.*) \cA[*]\o $1
^(..:..:..) \* (.*) $1 \c7*\o $2
^(..:..:..) \[\*\] (.*) $1 \cC[*]\o $2
^\[\*\] (.*) \cC[*]\o $1
^(..:..:..) \* (.*) $1 \cD*\o $2
^(\w+)> \u$1\o>

View File

@ -2,7 +2,7 @@
^meterpreter > \umeterpreter\u >
^msf > \umsf\u >
^msf (.*?)\((.*?)\) > \umsf\u $1(\c4$2\o) >
^\[\*\] (.*) \cA[*]\o $1
^\[\*\] (.*) \cC[*]\o $1
^\[\+\] (.*) \c9[+]\o $1
^\[\-\] (.*) \c4[-]\o $1
^ =\[ (.*) =[\c7 $1

View File

@ -150,7 +150,8 @@ sub launch_service {
}
sub _launch_service {
local('$c $key $value');
local('$c $key $value %options');
%options = copy($3);
if ('SESSION' in $3) {
$c = createDisplayTab($1, $host => sessionToHost($3['SESSION']), $file => "post");
@ -167,17 +168,15 @@ sub _launch_service {
}
if ($4 eq "payload" && $format eq "multi/handler") {
[$c addCommand: $null, "use exploit/multi/handler"];
[$c addCommand: $null, "set PAYLOAD ". substr($2, 8)];
[$c addCommand: $null, "set ExitOnSession false"];
[$c addCommand: $null, "use exploit/multi/handler"];
%options['PAYLOAD'] = substr($2, 8);
%options['ExitOnSession'] = 'false';
}
else {
[$c addCommand: $null, "use $2"];
}
foreach $key => $value ($3) {
[$c addCommand: $null, "set $key $value"];
}
[$c setOptions: %options];
if ($4 eq "exploit" || ($4 eq "payload" && $format eq "multi/handler")) {
[$c addCommand: "x", "exploit -j"];

View File

@ -33,6 +33,7 @@ sub downloadLoot {
local('$dest');
#$dest = chooseFile($title => "Where shall I save these files?", $dirsonly => 1, $always => 1);
$dest = getFileProper(dataDirectory(), $type);
mkdir($dest);
_downloadLoot(\$model, \$table, \$getme, \$dest, $dtype => $type);
}, \$model, \$table, \$getme, \$type));
}
@ -48,7 +49,7 @@ sub _downloadLoot {
# make the folder to store our downloads into
local('$handle $data $file');
if ($dtype eq "downloads") {
$file = getFileProper($dest, $host, $path, $name);
$file = getFileProper($dest, $host, strrep($path, ':', ''), $name);
}
else {
$file = getFileProper($dest, $host, $name);
@ -76,10 +77,10 @@ sub _postLoot {
local('$host $location $name $type $when');
($host, $location, $name, $type, $when) = $1;
[$2 append: "\c9
#
# $host $+ : $name
#\n"];
[$2 append: "
\c9#
\c9# $host $+ : $name
\c9#\n"];
if ("*binary*" iswm $type) {
[$2 append: "\c4This is a binary file\n"];

View File

@ -266,96 +266,88 @@ sub launch_msf_scans {
@modules = filter({ return iff("*_version" iswm $1, $1); }, @auxiliary);
$hosts = iff($1 is $null, ask("Enter range (e.g., 192.168.1.0/24):"), $1);
if ($hosts is $null) {
return;
}
thread(lambda({
local('$scanner $index $console %ports %discover $port %o $temp');
local('$scanner $index $queue %ports %discover $port %o $temp');
%ports = ohash();
%discover = ohash();
setMissPolicy(%ports, { return @(); });
setMissPolicy(%discover, { return @(); });
if ($hosts !is $null) {
elog("launched msf scans at: $hosts");
elog("launched msf scans at: $hosts");
$console = createConsoleTab("Scan", 1, $host => "all", $file => "scan");
[$console addSessionListener: lambda({
local('$text $host $port $hosts $modules $module @c');
$queue = createDisplayTab("Scan", $host => "all", $file => "scan");
foreach $text (split("\n", $2)) {
if ($text ismatch '... (.*?):(\d+) - TCP OPEN') {
($host, $port) = matched();
push(%discover[$port], $host);
}
else if ($text ismatch '... Scanned \d+ of \d+ hosts .100. complete.' && $start == 1) {
$start = $null;
[[$console getWindow] append: "[*] Starting host discovery scans\n"];
[$queue append: "[*] Building list of scan ports and modules"];
foreach $port => $hosts (%discover) {
if ($port in %ports) {
$modules = %ports[$port];
foreach $module ($modules) {
@c = @("use $module");
push(@c, "set RHOSTS " . join(", ", $hosts));
push(@c, "set RPORT $port");
push(@c, "set THREADS 24");
push(@c, "run -j");
# build up a list of scan ports
foreach $index => $scanner (@modules) {
if ($scanner ismatch 'scanner/(.*?)/\1_version') {
%o = call($client, "module.options", "auxiliary", $scanner);
if ('RPORT' in %o) {
$port = %o['RPORT']['default'];
push(%ports[$port], $scanner);
}
push(@launch, @c);
}
safetyCheck();
}
}
# add these ports to our list of ports to scan.. these come from querying all of Metasploit's modules
# for the default ports
foreach $port (@(50000, 21, 1720, 80, 443, 143, 3306, 1521, 110, 5432, 50013, 25, 161, 22, 23, 17185, 135, 8080, 4848, 1433, 5560, 512, 513, 514, 445, 5900, 5038, 111, 139, 49, 515, 7787, 2947, 7144, 9080, 8812, 2525, 2207, 3050, 5405, 1723, 1099, 5555, 921, 10001, 123, 3690, 548, 617, 6112, 6667, 3632, 783, 10050, 38292, 12174, 2967, 5168, 3628, 7777, 6101, 10000, 6504, 41523, 41524, 2000, 1900, 10202, 6503, 6070, 6502, 6050, 2103, 41025, 44334, 2100, 5554, 12203, 26000, 4000, 1000, 8014, 5250, 34443, 8028, 8008, 7510, 9495, 1581, 8000, 18881, 57772, 9090, 9999, 81, 3000, 8300, 8800, 8090, 389, 10203, 5093, 1533, 13500, 705, 623, 4659, 20031, 16102, 6080, 6660, 11000, 19810, 3057, 6905, 1100, 10616, 10628, 5051, 1582, 65535, 105, 22222, 30000, 113, 1755, 407, 1434, 2049, 689, 3128, 20222, 20034, 7580, 7579, 38080, 12401, 910, 912, 11234, 46823, 5061, 5060, 2380, 69, 5800, 62514, 42, 5631, 902)) {
$temp = %ports[$port];
}
# add a few left out modules
push(%ports['445'], "scanner/smb/smb_version");
[$queue append: "[*] Launching TCP scan"];
[$queue addCommand: $null, "use auxiliary/scanner/portscan/tcp"];
[$queue setOptions: %(PORTS => join(", ", keys(%ports)), RHOSTS => $hosts, THREADS => 24)];
[$queue addCommand: "x", "run -j"];
[$queue addSessionListener: lambda({
this('$start @launch');
local('$text $host $port $hosts $modules $module $options');
foreach $text (split("\n", $3)) {
if ($text ismatch '... (.*?):(\d+) - TCP OPEN') {
($host, $port) = matched();
push(%discover[$port], $host);
}
else if ($text ismatch '... Scanned \d+ of \d+ hosts .100. complete.' && $start is $null) {
$start = 1;
[$queue append: "\n[*] Starting host discovery scans"];
# gather up the list of modules that we will launch...
foreach $port => $hosts (%discover) {
if ($port in %ports) {
$modules = %ports[$port];
foreach $module ($modules) {
push(@launch, @($module, %(RHOSTS => join(", ", $hosts), RPORT => $port, THREADS => 24)));
}
}
}
}
if ($text ismatch '... Scanned \d+ of \d+ hosts .100. complete.' || $text ismatch '... Auxiliary failed: .*') {
if (size(@launch) == 0) {
$time = (ticks() - $time) / 1000.0;
[[$console getWindow] append: "\n[*] Scan complete in $time $+ s\n"];
}
else {
[[$console getWindow] append: "\n[*] " . size(@launch) . " scan" . iff(size(@launch) != 1, "s") . " to go...\n"];
thread(lambda({
local('$command');
foreach $command ($commands) {
[$console sendString: "$command $+ \n"];
yield 250;
}
}, \$console, $commands => shift(@launch)));
}
}
}
}, \$console, \%ports, \%discover, $start => 1, @launch => @(), $time => ticks())];
[[$console getWindow] append: "[*] Building list of scan ports and modules\n"];
# build up a list of scan ports
foreach $index => $scanner (@modules) {
if ($scanner ismatch 'scanner/(.*?)/\1_version') {
%o = call($client, "module.options", "auxiliary", $scanner);
if ('RPORT' in %o) {
$port = %o['RPORT']['default'];
push(%ports[$port], $scanner);
}
safetyCheck();
if ($text ismatch '... Scanned \d+ of \d+ hosts .100. complete.' && size(@launch) > 0) {
[$queue append: "\n[*] " . size(@launch) . " scan" . iff(size(@launch) != 1, "s") . " to go..."];
($module, $options) = shift(@launch);
[$queue addCommand: $null, "use $module"];
[$queue setOptions: $options];
[$queue addCommand: $null, "run -j"];
}
else if ($text ismatch '... Scanned \d+ of \d+ hosts .100. complete.' && size(@launch) == 0) {
$time = (ticks() - $time) / 1000.0;
[$queue append: "\n[*] Scan complete in $time $+ s"];
}
}
}, \$hosts, \%ports, \@modules, \%discover, \$queue, $time => ticks())];
# add these ports to our list of ports to scan.. these come from querying all of Metasploit's modules
# for the default ports
foreach $port (@(50000, 21, 1720, 80, 443, 143, 3306, 1521, 110, 5432, 50013, 25, 161, 22, 23, 17185, 135, 8080, 4848, 1433, 5560, 512, 513, 514, 445, 5900, 5038, 111, 139, 49, 515, 7787, 2947, 7144, 9080, 8812, 2525, 2207, 3050, 5405, 1723, 1099, 5555, 921, 10001, 123, 3690, 548, 617, 6112, 6667, 3632, 783, 10050, 38292, 12174, 2967, 5168, 3628, 7777, 6101, 10000, 6504, 41523, 41524, 2000, 1900, 10202, 6503, 6070, 6502, 6050, 2103, 41025, 44334, 2100, 5554, 12203, 26000, 4000, 1000, 8014, 5250, 34443, 8028, 8008, 7510, 9495, 1581, 8000, 18881, 57772, 9090, 9999, 81, 3000, 8300, 8800, 8090, 389, 10203, 5093, 1533, 13500, 705, 623, 4659, 20031, 16102, 6080, 6660, 11000, 19810, 3057, 6905, 1100, 10616, 10628, 5051, 1582, 65535, 105, 22222, 30000, 113, 1755, 407, 1434, 2049, 689, 3128, 20222, 20034, 7580, 7579, 38080, 12401, 910, 912, 11234, 46823, 5061, 5060, 2380, 69, 5800, 62514, 42, 5631, 902)) {
$temp = %ports[$port];
}
# add a few left out modules
push(%ports['445'], "scanner/smb/smb_version");
[[$console getWindow] append: "[*] Launching TCP scan\n"];
[$console sendString: "use auxiliary/scanner/portscan/tcp\n"];
[$console sendString: "set PORTS " . join(", ", keys(%ports)) . "\n"];
[$console sendString: "set RHOSTS $hosts $+ \n"];
[$console sendString: "set THREADS 24\n"];
[$console sendString: "run -j\n"];
}
[$queue start];
}, \$hosts, \@modules));
}

View File

@ -323,9 +323,9 @@ sub launchBruteForce {
[$console addCommand: $null, "use $type $+ / $+ $module"];
foreach $key => $value ($options) {
$value = strrep($value, '\\', '\\\\');
[$console addCommand: $null, "set $key $value"];
}
[$console addCommand: $null, "set REMOVE_USERPASS_FILE true"];
$options['REMOVE_USERPASS_FILE'] = "true";
[$console setOptions: $options];
[$console addCommand: $null, "run -j"];
[$console start];
}, $type => $1, $module => $2, $options => $3, $title => $4));

View File

@ -47,8 +47,6 @@ sub client {
%async['module.execute'] = 1;
%async['core.setg'] = 1;
%async['console.destroy'] = 1;
%async['console.write'] = 1;
%async['session.shell_write'] = 1;
#
# verify the client
@ -189,13 +187,15 @@ sub client {
release($poll_lock);
writeObject($handle, result(%()));
}
else if ($method eq "armitage.push") {
($null, $data) = $args;
event("< $+ $[10]eid $+ > " . $data);
writeObject($handle, result(%()));
}
else if ($method eq "armitage.poll") {
else if ($method eq "armitage.poll" || $method eq "armitage.push") {
acquire($poll_lock);
if ($method eq "armitage.push") {
($null, $data) = $args;
foreach $temp (split("\n", $data)) {
push(@events, formatDate("HH:mm:ss") . " < $+ $[10]eid $+ > " . $data);
}
}
if (size(@events) > $index) {
$rv = result(%(data => join("", sublist(@events, $index)), encoding => "base64", prompt => "$eid $+ > "));
$index = size(@events);

View File

@ -138,9 +138,6 @@ sub createConsolePanel {
else if ($word in @payloads) {
[$thread sendString: "set PAYLOAD $word $+ \n"];
}
else if (-exists $word && !$REMOTE) {
saveFile($word);
}
}, \$thread)];
return @($result['id'], $console, $thread);
@ -159,9 +156,7 @@ sub createConsoleTab {
logCheck($console, $host, $file);
}
dispatchEvent(lambda({
[$frame addTab: iff($title is $null, "Console", $title), $console, $thread, $host];
}, $title => $1, \$console, \$thread, \$host));
[$frame addTab: iff($1 is $null, "Console", $1), $console, $thread, $host];
return $thread;
}
@ -479,10 +474,8 @@ sub module_execute {
$queue = createDisplayTab($1, \$host);
[$queue addCommand: $null, "use $1 $+ / $+ $2"];
foreach $key => $value ($3) {
[$queue addCommand: $null, "set $key $value"];
}
[$queue setOptions: $3];
if ($1 eq "exploit") {
[$queue addCommand: $null, "exploit -j"];
}

View File

@ -143,8 +143,13 @@ public class ConsoleClient implements Runnable, ActionListener {
}
}
connection.execute(writeCommand, new Object[] { session, text });
read = readResponse();
if ("armitage.push".equals(writeCommand)) {
read = (Map)connection.execute(writeCommand, new Object[] { session, text });
}
else {
connection.execute(writeCommand, new Object[] { session, text });
read = readResponse();
}
processRead(read);
fireSessionWroteEvent(text);

View File

@ -21,8 +21,9 @@ public class ConsoleQueue implements Runnable {
protected Console display = null;
private static class Command {
public Object token;
public String text;
public Object token = null;
public String text = null;
public Map assign = null;
public long start = System.currentTimeMillis();
}
@ -92,6 +93,78 @@ public class ConsoleQueue implements Runnable {
}
protected void processCommand(Command c) {
if (c.assign == null) {
processNormalCommand(c);
}
else {
processAssignCommand(c);
}
}
protected void processAssignCommand(Command c) {
try {
/* absorb anything misc */
Map read = readResponse();
String prompt = ConsoleClient.cleanText(read.get("prompt") + "");
StringBuffer writeme = new StringBuffer();
Set expected = new HashSet();
/* loop through our values to assign */
Iterator i = c.assign.entrySet().iterator();
while (i.hasNext()) {
Map.Entry entry = (Map.Entry)i.next();
String key = entry.getKey() + "";
String value = entry.getValue() + "";
writeme.append("set " + key + " " + value + "\n");
expected.add(key);
}
/* write our command to whateverz */
connection.execute("console.write", new Object[] { consoleid, writeme.toString() });
long start = System.currentTimeMillis();
/* process through all of our values */
while (expected.size() > 0) {
Thread.yield();
Map temp = (Map)(connection.execute("console.read", new Object[] { consoleid }));
if (!isEmptyData(temp.get("data") + "")) {
String[] lines = (temp.get("data") + "").split("\n");
for (int x = 0; x < lines.length; x++) {
if (lines[x].indexOf(" => ") != -1) {
String[] kv = lines[x].split(" => ");
/* remove any set variables from our set of stuff */
expected.remove(kv[0]);
if (display != null) {
display.append(prompt + "set " + kv[0] + " " + kv[1] + "\n");
display.append(lines[x] + "\n");
}
}
else if (display != null) {
display.append(lines[x] + "\n");
}
else {
System.err.println("Batch read unexpected: " + lines[x]);
}
}
}
else if ((System.currentTimeMillis() - start) > 10000) {
/* this is a safety check to keep a console from spinning waiting for one command to complete. Shouldn't trigger--unless I mess up :) */
System.err.println("Timed out: " + c.assign + " vs. " + expected);
break;
}
}
}
catch (Exception ex) {
System.err.println(consoleid + " -> " + c.text);
ex.printStackTrace();
}
}
protected void processNormalCommand(Command c) {
Map read = null;
try {
if (c.text.startsWith("ECHO ")) {
@ -161,6 +234,20 @@ public class ConsoleQueue implements Runnable {
}
}
public void append(String text) {
addCommand(null, "ECHO " + text + "\n");
}
public void setOptions(Map options) {
synchronized (this) {
Command temp = new Command();
temp.token = null;
temp.text = null;
temp.assign = options;
commands.add(temp);
}
}
public void addCommand(Object token, String text) {
synchronized (this) {
if (text.trim().equals("")) {

View File

@ -31,19 +31,20 @@ public class Colors {
colorTable = new Color[16];
colorTable[0] = Color.white;
colorTable[1] = new Color(0, 0, 0);
colorTable[2] = new Color(0, 0, 128);
colorTable[3] = new Color(0, 144, 0);
colorTable[4] = new Color(255, 0, 0);
colorTable[5] = new Color(128, 0, 0);
colorTable[6] = new Color(160, 0, 160);
colorTable[7] = new Color(255, 128, 0);
colorTable[8] = new Color(255, 255, 0);
colorTable[9] = new Color(0, 255, 0);
colorTable[10] = new Color(0, 144, 144);
colorTable[11] = new Color(0, 255, 255);
colorTable[12] = new Color(0, 0, 255);
colorTable[13] = new Color(255, 0, 255);
colorTable[14] = new Color(128, 128, 128);
colorTable[2] = Color.decode("#3465A4");
colorTable[3] = Color.decode("#4E9A06");
colorTable[4] = Color.decode("#EF2929"); //new Color(255, 0, 0);
colorTable[5] = Color.decode("#CC0000");
colorTable[6] = Color.decode("#75507B");
colorTable[7] = Color.decode("#C4A000");
colorTable[8] = Color.decode("#FCE94F");
colorTable[9] = Color.decode("#8AE234");
colorTable[10] = Color.decode("#06989A");
colorTable[11] = Color.decode("#34E2E2");
colorTable[12] = Color.decode("#729FCF");
colorTable[13] = Color.decode("#AD7FA8");
//colorTable[14] = Color.decode("#555753");
colorTable[14] = Color.decode("#808080");
colorTable[15] = Color.lightGray;
for (int x = 0; x < 16; x++) {
@ -62,8 +63,12 @@ public class Colors {
/* strip format codes from the text */
public String strip(String text) {
StringBuffer buffer = new StringBuffer(text.length());
Fragment f = parse(text);
return strip(f);
}
private String strip(Fragment f) {
StringBuffer buffer = new StringBuffer(128);
while (f != null) {
buffer.append(f.text);
f = f.next;
@ -71,13 +76,11 @@ public class Colors {
return buffer.toString();
}
public void append(JTextPane console, String text) {
StyledDocument doc = console.getStyledDocument();
Fragment f = parse(text);
private void append(StyledDocument doc, Fragment f) {
while (f != null) {
try {
if (f.text.length() > 0)
doc.insertString(doc.getLength(), f.text.toString(), showcolors ? f.attr : null);
doc.insertString(doc.getLength(), f.text.toString(), f.attr);
}
catch (Exception ex) {
ex.printStackTrace();
@ -86,14 +89,46 @@ public class Colors {
}
}
public void append(JTextPane console, String text) {
StyledDocument doc = console.getStyledDocument();
Fragment f = parse(text);
if (showcolors) {
append(doc, f);
}
else {
append(doc, parse(strip(f)));
}
}
public void set(JTextPane console, String text) {
console.setText("");
append(console, text);
/* don't update that which we do not need to update */
Fragment f = parse(text);
if (strip(f).equals(console.getText())) {
return;
}
StyledDocument doc = console.getStyledDocument();
try {
doc.remove(0, doc.getLength());
if (showcolors)
append(doc, f);
else
append(doc, parse(strip(f)));
}
catch (BadLocationException ex) { ex.printStackTrace(); }
/* this is a dumb hack to prevent the height from getting out of whack */
console.setSize(new Dimension(1000, console.getSize().height));
}
private Fragment parse(String text) {
Fragment current = new Fragment();
Fragment first = current;
if (text == null)
return current;
char[] data = text.toCharArray();
int fore, back;

View File

@ -271,12 +271,12 @@ public class Console extends JPanel implements FocusListener {
if (breakp != -1) {
colors.append(console, _text.substring(0, breakp + 1));
colors.set(prompt, _text.substring(breakp + 1) + " ");
updatePrompt(_text.substring(breakp + 1) + " ");
if (log != null)
log.print(colors.strip(_text.substring(0, breakp + 1)));
}
else {
colors.set(prompt, _text);
updatePrompt(_text);
}
promptLock = true;
}

View File

@ -74,7 +74,7 @@ public class SearchPanel extends JPanel implements ActionListener {
try {
String text = component.getText();
int lastIndex = -1;
while ((lastIndex = text.indexOf(searchstr, lastIndex + 1)) != -1) {
while ((lastIndex = text.replaceAll("\r", "").indexOf(searchstr, lastIndex + 1)) != -1) {
component.getHighlighter().addHighlight(
lastIndex,
lastIndex + searchstr.length(),

View File

@ -1,6 +1,24 @@
Armitage Changelog
==================
21 May 12
---------
- Added a hack to prevent the input area from flickering when the
prompt changes.
- Updated the color palette to something a little more subtle.
- Added an optimization to how modules are launched. This will make
a difference for team use in high latency situations.
- Rewrote MSF Scans feature to use console queue. This option is more
reliable and it makes the code easier to follow.
- Added a hack to combine chat message writes with a read request.
This will make the event log more responsive in a high latency
situation (can't you tell I care about this "situation")
- Fixed text highlights through Ctrl+F on Windows. UNIX platforms
were always OK. Another good reason to not use these tools on
Windows. Ever.
- View -> Downloads Sync Files feature now works on Windows. It looks
like leaving those pesky :'s in the file paths is bad.
17 May 12
---------
- Fixed bug with loot/download viewer breaking with a font resize.