1#!/usr/local/bin/perl -T
2#-*-Perl-*-
3#
4# $Id: kuang2.pl,v 1.1 2004/12/31 18:54:22 provos Exp $
5#
6# kuang2.pl -- Honeyd module that emulates the backdoor installed by the
7#              Kuang2 virus.
8#
9# Copyright (c) 2003, 2004 Klaus Steding-Jessen
10# All rights reserved.
11#
12# Redistribution and use in source and binary forms, with or without
13# modification, are permitted provided that the following conditions
14# are met:
15#
16#    - Redistributions of source code must retain the above copyright
17#      notice, this list of conditions and the following disclaimer.
18#    - Redistributions in binary form must reproduce the above
19#      copyright notice, this list of conditions and the following
20#      disclaimer in the documentation and/or other materials provided
21#      with the distribution.
22#
23# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
27# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
28# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
29# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
32# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
33# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34# POSSIBILITY OF SUCH DAMAGE.
35
36######################################################################
37
38=head1 NAME
39
40kuang2.pl -- Honeyd module that emulates the backdoor installed by the Kuang2 virus.
41
42=head1 SYNOPSIS
43
44kuang2.pl [-Vh] [-d] [-f file]
45
46=head1 DESCRIPTION
47
48Start kuang2.pl as a Honeyd service.
49
50The options are as follows:
51
52=over 6
53
54=item -V
55
56Display version number and exit.
57
58=item -h
59
60Display this help and exit.
61
62=item -d
63
64debug mode.
65
66=item -f file
67
68configuration file.
69
70=back
71
72=head1 SEE ALSO
73
74Honeyd.
75
76=head1 AUTHOR
77
78Klaus Steding-Jessen <jessen@nic.br>
79
80=head1 AVAILABILITY
81
82The latest version of kuang2.pl is available from
83http://www.honeynet.org.br/tools/
84
85=cut
86
87######################################################################
88
89(my $program_name = $0) =~ s@.*/@@;
90
91use strict;
92use warnings;
93use Getopt::Std;
94use Fcntl qw(:flock);
95use POSIX qw(strftime);
96use File::Path;
97use File::Basename;
98
99unless (eval "use Digest::SHA1; 1") {
100    die "$program_name: Please install Digest::SHA1\n";
101}
102
103######################################################################
104
105my %option = ();
106getopts('Vhdf:n', \%option);
107
108my $version='0.2';
109
110# unbuffered output.
111$| = 1;
112
113# set PATH.
114$ENV{'PATH'} = '/bin:/usr/bin:/usr/sbin';
115
116######################################################################
117# configuration defaults -- can be changed via configuration file.
118# parameter, regex value and default value.
119
120my %checksum = ();
121my %conf = (
122            "logdir" => {
123                regex => '[\w\.-\/]+',
124                value => '/var/kuang2' },
125            "timeout" => {
126                regex => '\d+',
127                value => 30 },
128            "num_drives" => {
129                regex => '\d+',
130                value => 1 },
131            "computer_name" => {
132                regex => '[\w\s]+',
133                value => 'MY COMPUTER' },
134            "max_upload_size"  => {
135                regex => '\d+',
136                value =>  15728640 },
137            );
138
139######################################################################
140# kuang2 constants.
141
142my $K2_BUFFER_SIZE =   1024;
143my $K2_HELO =          0x324B4F59;
144my $K2_ERROR =         0x52525245;
145my $K2_DONE =          0x454E4F44;
146my $K2_QUIT =          0x54495551;
147my $K2_DELETE_FILE =   0x464C4544;
148my $K2_RUN_FILE =      0x464E5552;
149my $K2_FOLDER_INFO =   0x464E4946;
150my $K2_DOWNLOAD_FILE = 0x464E5744;
151my $K2_UPLOAD_FILE =   0x46445055;
152my $K2_UPLOAD_FILE_2 = 0x0000687f;
153my $K2_UPLOAD_FILE_3 = 0x00007620;
154my $K2_UPLOAD_FILE_4 = 0x00004820;
155
156######################################################################
157
158# display usage if requested.
159show_usage() if ($option{'h'});
160
161# display version if requested.
162show_version() if ($option{'V'});
163
164# parse config options from the configuration file, if requested.
165if (defined($option{'f'})) {
166    my ($confref) = process_config_file($option{'f'}, \%conf);
167    %conf = %{$confref};
168}
169
170# logfile.
171my $logfile = $conf{'logdir'}{'value'} . '/' . 'logfile';
172
173######################################################################
174# get srchost, dsthost, srcport and dstport from the environment
175# variables set by honeyd.
176
177my $srchost = '127.0.0.1';
178if (defined($ENV{'HONEYD_IP_SRC'}) &&
179    $ENV{'HONEYD_IP_SRC'} =~ /^(\d+\.\d+\.\d+\.\d+)$/) {
180    $srchost = $1;
181}
182
183my $dsthost = '127.0.0.1';
184if (defined($ENV{'HONEYD_IP_DST'}) &&
185    $ENV{'HONEYD_IP_DST'} =~ /^(\d+\.\d+\.\d+\.\d+)$/) {
186    $dsthost = $1;
187}
188
189my $srcport = 0;
190if (defined($ENV{'HONEYD_SRC_PORT'}) &&
191    $ENV{'HONEYD_SRC_PORT'} =~ /^(\d+)$/) {
192    $srcport = $1;
193}
194
195my $dstport = 0;
196if (defined($ENV{'HONEYD_DST_PORT'}) &&
197    $ENV{'HONEYD_DST_PORT'} =~ /^(\d+)$/) {
198    $dstport = $1;
199}
200
201######################################################################
202# main.
203
204# install SIGALRM handler.
205$SIG{ALRM} = sub {
206    my $msg = sprintf("timed out after %d seconds", $conf{'timeout'}{'value'});
207    logentry($logfile, $msg);
208    die("$program_name: $msg\n");
209    exit 0;
210};
211
212alarm $conf{'timeout'}{'value'};
213
214logentry($logfile, sprintf("connection from %s:%d to %s:%d",
215                           $srchost, $srcport, $dsthost, $dstport));
216
217# send the initial K2_HELO to the client.
218my $name_size = $K2_BUFFER_SIZE - 8;
219my $nwrite = syswrite(STDOUT, pack("IIZ$name_size", $K2_HELO,
220                                   $conf{'num_drives'}{'value'},
221                                   $conf{'computer_name'}{'value'}));
222
223logentry($logfile, "DEBUG: K2_HELO sent to client")
224    if ($option{'d'});
225
226# read stdin from honeyd -- loop reading commands from the client.
227my $nread;
228while ($nread = sysread(STDIN, my $buffer, $K2_BUFFER_SIZE)) {
229
230    # debug.
231    #logentry($logfile, "DEBUG: $nread byte(s) read") if ($option{'d'});
232
233    my $cmd = (unpack("I*", $buffer));
234    if (defined($cmd)) {
235        #logentry($logfile, sprintf("DEBUG: cmd: 0x%02X", $cmd))
236        #    if ($option{'d'});
237    } else {
238        logentry($logfile, sprintf("data: %s", data2hex($buffer)));
239        next;
240    }
241
242    # process commands sent from the kuang2 client.
243    if ($cmd == $K2_UPLOAD_FILE) {
244        k2_upload_file($buffer, 0);
245
246    } elsif ($cmd == $K2_DOWNLOAD_FILE) {
247        k2_download_file($buffer);
248
249    } elsif ($cmd == $K2_QUIT) {
250        logentry($logfile, "K2_QUIT received, exiting");
251        exit 0;
252
253    } elsif ($cmd == $K2_DELETE_FILE) {
254        k2_delete_file($buffer);
255
256    } elsif ($cmd == $K2_RUN_FILE) {
257        k2_run_file($buffer);
258
259    } elsif ($cmd == $K2_FOLDER_INFO) {
260        k2_folder_info($buffer);
261
262    } elsif (($cmd == $K2_UPLOAD_FILE_2) ||
263             ($cmd == $K2_UPLOAD_FILE_3) ||
264             ($cmd == $K2_UPLOAD_FILE_4)) {
265        # different upload procotol.
266        k2_upload_file($buffer, -1);
267
268    } else {
269        # unknown command.
270        logentry($logfile, sprintf("unknown command: %s", data2hex($buffer)));
271
272        my $nwrite = syswrite(STDOUT, pack("I", $K2_ERROR));
273        logentry($logfile, "DEBUG: K2_ERROR sent to client") if ($option{'d'});
274    }
275    alarm $conf{'timeout'}{'value'};
276}
277
278logentry($logfile, "ERROR: sysread: $!") if (!defined($nread));
279
280alarm 0;
281
282logentry($logfile, "exiting");
283exit 0;
284
285######################################################################
286# handle upload file requests.
287sub k2_upload_file {
288    my ($buffer, $flag) = @_;
289
290    my ($cmd, $filesize, $filename);
291    if ($flag == 0) {
292        ($cmd, $filesize, $filename) = unpack("IIZ*", $buffer);
293    } else {
294        $filesize = -1;
295        ($cmd, $filename) = unpack("IZ*", $buffer);
296    }
297
298    # check if unpack() succeeded.
299    if (!defined($cmd) || !defined($filesize) || !defined($filename)) {
300        my $msg = "unpack() error";
301        logentry($logfile, $msg);
302        die("$program_name: $msg\n");
303    }
304
305    # untaint filename.
306    if ($filename =~ /^([\w\\\/\.:]+)$/) {
307        $filename = $1;
308    }
309
310    if ($flag == 0) {
311        # we only accept files >0 and <= MAX_UPLOAD_FILESIZE bytes long.
312        if (!(($filesize > 0) &&
313              ($filesize <= $conf{'max_upload_size'}{'value'}))) {
314            my $nwrite = syswrite(STDOUT, pack("I", $K2_ERROR));
315            logentry($logfile, "DEBUG: K2_ERROR sent to client")
316                if ($option{'d'});
317            my $msg = sprintf("illegal file size: %ld byte(s)", $filesize);
318
319            logentry($logfile, $msg);
320            die("$program_name: $msg\n");
321        }
322    }
323
324    if ($flag == 0) {
325        logentry($logfile,
326                 sprintf("cmd received: K2_UPLOAD_FILE, file: %s, size: %d byte(s)",
327                         quotemeta($filename), $filesize));
328    } else {
329        logentry($logfile,
330                 sprintf("cmd received: K2_UPLOAD_FILE (ALT), file: %s",
331                         quotemeta($filename)));
332    }
333
334    # ack the K2_UPLOAD_FILE request.
335    my $nwrite = syswrite(STDOUT, pack("I", $K2_DONE));
336    logentry($logfile, "DEBUG: K2_DONE sent to client") if ($option{'d'});
337
338    # keep only the basename component and also remove the drive part
339    # (c:\\, d:\\, etc) on MSDOS/Win filenames.
340    fileparse_set_fstype("MSWin32");
341    $filename = basename($filename);
342
343    # create directory hierarchy (adapted from smtp.pl).
344    my $srchostdir = $srchost;
345    $srchostdir =~ s/\./\//g;
346    my $upload_dir = "$conf{'logdir'}{'value'}/$srchostdir/$srcport";
347
348    if (! -d "$upload_dir" ) {
349        eval { mkpath($upload_dir) };
350        if ($@) {
351            logentry($logfile, "ERROR: $upload_dir: $@");
352            die("$program_name: $upload_dir: $@");
353        }
354    }
355
356    my $upload_file = "$upload_dir/$filename";
357
358    if (!defined(open(UPLOAD_FILE, ">$upload_file"))) {
359        logentry($logfile, "ERROR: $upload_file: $!");
360        die("$program_name: $upload_file: $!\n");
361    }
362
363    # read the file.
364    my $uploaded = 0;
365    my $nread = 0;
366    do {
367
368        $nread = sysread(STDIN, my $buffer, $K2_BUFFER_SIZE);
369        #logentry($logfile, "DEBUG: $nread byte(s) read") if ($option{'d'});
370
371        my $nwrite = syswrite(UPLOAD_FILE, $buffer, $nread);
372        $uploaded += $nwrite;
373
374        if ($uploaded > $conf{'max_upload_size'}{'value'}) {
375            my $msg = sprintf("ERROR: upload limit exceeded: %ld byte(s)",
376                              $uploaded);
377            logentry($logfile, $msg);
378            die("$program_name: $msg\n");
379        }
380
381        alarm $conf{'timeout'}{'value'};
382    } while (($uploaded != $filesize) && $nread);
383
384    close(UPLOAD_FILE);
385
386    # if filesize is known, check if it matches uploaded size.
387    if (($flag == 0) && ($uploaded != $filesize)) {
388        logentry($logfile,
389                 sprintf("sizes don't match, filesize: %ld, uploaded: %ld",
390                         $filesize, $uploaded));
391    }
392
393    # check for empty uploaded files.
394    if (-z $upload_file) {
395        my $msg = sprintf("ERROR: %s is empty", $upload_file);
396        logentry($logfile, $msg);
397        die("$program_name: $msg\n");
398    }
399
400    # determine the SHA-1 of uploaded file.
401    if (!defined(open(UPLOAD_FILE, "$upload_file"))) {
402        logentry($logfile, "ERROR: $upload_file: $!");
403        die("$program_name: $upload_file: $!\n");
404    }
405    my $ctx = Digest::SHA1->new;
406    $ctx->addfile(*UPLOAD_FILE);
407    my $digest = $ctx->hexdigest;
408    close(UPLOAD_FILE);
409
410    logentry($logfile,
411             sprintf("file uploaded to %s, size: %d byte(s), SHA-1: %s",
412                     $upload_file, -s $upload_file, $digest));
413
414    # we're done.
415    $nwrite = syswrite(STDOUT, pack("I", $K2_DONE));
416    logentry($logfile, "DEBUG: K2_DONE sent to client") if ($option{'d'});
417
418    return 0;
419}
420######################################################################
421# handle run file requests.
422sub k2_run_file {
423    my ($buffer) = @_;
424
425    my ($cmd, $filename) = unpack("IZ*", $buffer);
426
427    # check if unpack() succeeded.
428    if (!defined($cmd) || !defined($filename)) {
429        my $msg = "unpack() error";
430        logentry($logfile, $msg);
431        die("$program_name: $msg\n");
432    }
433
434    # untaint filename.
435    if ($filename =~ /^([\w\\\/\.:]+)$/) {
436        $filename = $1;
437    }
438
439    logentry($logfile,
440             sprintf("cmd received: K2_RUN_FILE, file: %s",
441                     quotemeta($filename)));
442
443    # we're done.
444    $nwrite = syswrite(STDOUT, pack("I", $K2_DONE));
445    logentry($logfile, "DEBUG: K2_DONE sent to client") if ($option{'d'});
446
447    return 0;
448}
449######################################################################
450# handle delete file requests.
451sub k2_delete_file {
452    my ($buffer) = @_;
453
454    my ($cmd, $filename) = unpack("IZ*", $buffer);
455
456    # check if unpack() succeeded.
457    if (!defined($cmd) || !defined($filename)) {
458        my $msg = "unpack() error";
459        logentry($logfile, $msg);
460        die("$program_name: $msg\n");
461    }
462
463    # untaint filename.
464    if ($filename =~ /^([\w\\\/\.:]+)$/) {
465        $filename = $1;
466    }
467
468    logentry($logfile,
469             sprintf("cmd received: K2_DELETE_FILE, file: %s",
470                     quotemeta($filename)));
471
472    # return an error to the client.
473    $nwrite = syswrite(STDOUT, pack("I", $K2_ERROR));
474    logentry($logfile, "DEBUG: K2_ERROR sent to client") if ($option{'d'});
475
476    return 0;
477}
478######################################################################
479# handle download file requests.
480sub k2_download_file {
481    my ($buffer) = @_;
482
483    my ($cmd, $filename) = unpack("IZ*", $buffer);
484
485    # check if unpack() succeeded.
486    if (!defined($cmd) || !defined($filename)) {
487        my $msg = "unpack() error";
488        logentry($logfile, $msg);
489        die("$program_name: $msg\n");
490    }
491
492    # untaint filename.
493    if ($filename =~ /^([\w\\\/\.:]+)$/) {
494        $filename = $1;
495    }
496
497    logentry($logfile,
498             sprintf("cmd received: K2_DOWNLOAD_FILE (init), file: %s",
499                     quotemeta($filename)));
500
501    # return an error to the client.
502    $nwrite = syswrite(STDOUT, pack("I", $K2_ERROR));
503    logentry($logfile, "DEBUG: K2_ERROR sent to client") if ($option{'d'});
504
505    return 0;
506}
507######################################################################
508# handle folfer info requests.
509sub k2_folder_info {
510    my ($buffer) = @_;
511
512    my ($cmd, $filename) = unpack("IZ*", $buffer);
513
514    # check if unpack() succeeded.
515    if (!defined($cmd) || !defined($filename)) {
516        my $msg = "unpack() error";
517        logentry($logfile, $msg);
518        die("$program_name: $msg\n");
519    }
520
521    # untaint filename.
522    if ($filename =~ /^([\w\\\/\.:]+)$/) {
523        $filename = $1;
524    }
525
526    logentry($logfile,
527             sprintf("cmd received: K2_FOLDER_INFO, start: %s",
528                     quotemeta($filename)));
529
530    # return an error to the client.
531    $nwrite = syswrite(STDOUT, pack("I", $K2_ERROR));
532    logentry($logfile, "DEBUG: K2_ERROR sent to client") if ($option{'d'});
533
534    return 0;
535}
536######################################################################
537# create a log entry.
538sub logentry {
539    my ($logfile, $msg) = @_;
540
541    my $datum = strftime "%F %T %z", localtime;
542    my $pid = $$;
543
544    open(LOG, ">>$logfile") ||
545	die "$program_name: $logfile: $!\n";
546    flock(LOG, LOCK_EX);
547    seek(LOG, 0, 2);
548
549    printf LOG ("%s: %s[%d]: %s\n", $datum, $program_name, $pid, $msg);
550
551    flock(LOG, LOCK_UN);
552    close(LOG);
553
554}
555######################################################################
556# return a hexa representation of data.
557sub data2hex {
558    my ($data) = @_;
559
560    my $hex = unpack("H*", $data);
561    if (defined($hex)) {
562        $hex =~ s/../0x$& /g;
563        $hex =~ s/ $//g;
564    } else {
565        $hex = "";
566    }
567
568    return $hex;
569}
570######################################################################
571# process the configuration file.  Exits in case of error.
572sub process_config_file {
573    my ($filename, $confref) = @_;
574    my %conf = %{$confref};
575
576    # filename: words, numbers, '-', '/' and '.' are ok.
577    if ($filename =~ /^([\w\.\-\/]+)$/) {
578        $filename = $1;
579    } else {
580        $filename = quotemeta($filename);
581        die("$program_name: invalid filename: $filename\n");
582    }
583
584    open(CONF, "$filename") ||
585        die ("$program_name: can't open $filename: $!\n");
586
587    while (my $line = <CONF>) {
588        chomp($line);
589        next if ($line =~ /^$|^\s*$|^\s*#/); # skip comments and empty lines.
590        foreach my $key (keys %conf) {
591            if ($line =~ /^$key\s*[=:]\s*($conf{$key}{'regex'})\s*$/) {
592                $conf{$key}{'value'} = $1;
593                last;
594            } elsif ($line =~ /^$key\s*/) {
595                die("$program_name: invalid parameter: $line\n");
596            }
597        }
598    }
599    close(CONF);
600    return \%conf;
601}
602######################################################################
603# print program usage and exit.
604sub show_usage {
605    print <<EOF;
606Usage: $program_name [-Vh] [-d] [-f file]
607       -h             display this help and exit.
608       -V             display version number and exit.
609       -d             debug.
610       -f file        configuration file.
611EOF
612
613    exit 0;
614}
615######################################################################
616# print program version and exit.
617sub show_version {
618    printf("%s %s\n", $program_name, $version);
619    exit 0;
620}
621######################################################################
622
623# kuang2.pl ends here.
624