【技术分享】如何绕过安全机制来破坏AWS S3日志记录

http://p6.qhimg.com/t01359474c52c941b20.png

译者:blueSky

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


背景

在今天的公共云出现之前,存储日志的最佳做法是将日志与生成它们的主机分开存储。如果主机遭到入侵,存储的日志将更有可能被保留下来以供安全研究人员进行分析。

如果你在一个云提供商(例如AWS)申请了一个账户,那么帐户中的存储服务可以保存你的主机活动日志。但是如果对帐户访问权限进行充分彻底的信任可能会导致日志记录受损或者给IR团队带来一些麻烦。这就好比如果日志存储在单台被攻击的机器上,一旦攻击者越过了对日志的访问限制,那么日志可能会被攻击者篡改或者删除。但是,在AWS中,删除和编辑日志看起来与使用rm -rf擦除日志不同 。

在AWS中,日志数据来源于CloudTrail服务,该服务可以以可变间隔将当前批处理文件中的活动日志传送到预定义的S3存储区中。CloudTrail日志通常会被收集,因为如果发生网络攻击事件,日志中的审计和跟踪信息将会十分有用。当一个攻击者访问了一个帐户时,日志中会记录攻击者对该账户实施的所有操作,因此日志构成了大多数AWS防御的基础。如果您没有在帐户中启用它们,请立即启用它们


以前的工作

Daniel Grzelak在他的博文探讨了日志存储在S3存储区的几个有趣的后果。例如,当一个文件被发送到S3存储区时,它将触发一个事件,在AWS中,可能会有一个函数或者Lambda在监听此事件,这些函数一旦监听到此事件,将在日志到达后立即将其删除,然而日志仍将继续发送到S3存储区中,具体如下图所示:

http://p1.qhimg.com/t01dc5984be8abae1cf.jpg


版本,lambda和摘要

如果攻击者获得删除旧版本日志的权限,那么将“版本控制”添加到S3存储区就不会有任何的帮助。版本化的存储区确实可以选择通过多因素认证(“MFA-delete”)来保护具有版本号的项目不被误删。不过,似乎只有AWS帐户的root用户才(唯一一个对S3所有存储区拥有操作权限的账户)可以配置此功能,因此在root访问受到严格限制的设置中不太容易启用此功能。

不管在任何情况下,当有人在空的日志存储区寻找日志时,都将不可避免地引起警报。这使攻击者面临一个紧迫的问题需要解决:我们如何擦除我们的痕迹,同时使剩下的日志可用和可读?快速的解决方案是:我们可以修改lambda以检查每个日志文件,并在使用干净的日志文件执行覆盖操作之前删除任何的日志痕迹。在修改日志时,有些事情是需要我们注意的:lambda操作本身会生成更多的活动记录,这反过来会向日志文件中添加更多的操作痕迹。通过向日志“消除器”的部分名称(例如策略名称,角色名称以及lambdas)添加唯一标签,添加的这些标签可以像任何其他日志痕迹条目一样被删除,以便日志“消除器”可以自行删除。在此代码片段中,包含任何角色,lambda或者策略都将被保留在日志之外。

import json    
import   urllib    
import   boto3    
import gzip    
import   tempfile    
import   shutil    
dirty_tag   = "thinkst_6ae655cf"    
def filter_dirty_tag(log):    
return   dirty_tag in json.dumps(log)    
s3 =   boto3.client('s3')    
def lambda_handler(event,   context):    
bucket   = event['Records'][0]['s3']['bucket']['name']    
key =   urllib.unquote_plus(event['Records'][0]['s3']['object']['key']).decode('utf8')    
resp =   s3.get_object(Bucket=bucket,   Key=key)    
gzip_tmp   =   tempfile.NamedTemporaryFile(delete=False)    
shutil.copyfileobj(resp['Body'],   gzip_tmp)    
gzip_tmp.close()    
gzip_filename   = gzip_tmp.name    
with   gzip.open(gzip_filename, 'rb') as f:    
file_content   = f.read()    
logs =   json.loads(file_content)    
old_num_logs   = len(logs['Records'])    
print   old_num_logs    
logs['Records'] = filter(lambda x: not   filter_dirty_tag(x), logs['Records'])    
print len(logs['Records'])    
if len(logs['Records']) == 0:    
print "Deleting   empty %s" % key    
s3.delete_object(Bucket=bucket,   Key=key)    
elif len(logs['Records']) ==   old_num_logs:    
print "Doing   nothing no log records filtered"    
else:    
print "Updating   %s" % key    
with   gzip.open(gzip_filename, 'wb') as f:    
f.write(json.dumps(logs,   separators=(',',':')))    
s3.put_object(Bucket=bucket,   Key=key, Body=open(gzip_filename,   'rb'))

上述似乎提供了一个完美的解决方案,除了AWS Cloudtrail还提供日志验证功能。每隔一段时间,日志跟踪就会生成一个(签名)摘要文件,以证明过去时间间隔中传递的所有日志文件的内容。如果日志文件的摘要发生了更改,那么该摘要文件将会验证失败。

http://p4.qhimg.com/t0194eb7511a99b9711.jpg

乍一看,AWS Cloudtrail的验证功能好像阻止了我们实施“无痕”攻击:我们的lambda在日志文件到达S3存储区后修改了该日志文件,但是在 我们在更改之前AWS Cloudtrail就对文件内容进行了摘要计算 ,因此内容和摘要不一致。通过上图我们发现,每个摘要文件都会由上一个摘要文件覆盖,这将创建一个从头开始的日志验证链。如果以前的摘要文件已被修改或丢失,则下一个摘要文件的验证将失败(但是后续的摘要将是有效的)。

http://p1.qhimg.com/t0125b3319f51bb4d22.png

似乎有一种方法就是简单地删除摘要文件,但S3会保护这些摘要文件,并防止删除属于完整摘要链的文件。但有一个重要的事项引起了我们的注意:当日志验证在Trail上停止并重新启动(不是停止和启动日志记录本身)时,日志验证链将会以一种有趣的方式被打破。由于日志验证已停止并重新启动,所传递的下一个摘要文件不会引用之前的摘要文件。相反,下一个摘要文件将null作为其先前的文件引用,就好像它是一个重新开始摘要链。

http://p0.qhimg.com/t01c5378ec7a8dae01b.jpg

在上图中,红色的日志文件被更改后,日志验证被停止并重新启动,这打破了摘要1和摘要2之间的联系。


改变日志,成功验证

我们已经知道S3阻止了在连续验证链上删除摘要文件。但是,只要没有其他文件引用,我们就可以删除较旧的摘要文件。 这意味着我们可以删除摘要1,然后删除摘要0。

这意味着在以前的日志验证链中,我们现在可以删除最新的摘要文件,而不会对任何摘要日志进行验证。日志验证将从最新的链接开始,并向后移动。当验证遇到先前链路上的第一个项目时,它只是移动到上一个链路的最新可用条目。

http://p8.qhimg.com/t0111885bc31a398434.jpg


现在?

因此,我们可以很容易的想到,日志验证只会出现在系统的健康检查中,只要它不失败,没有人会去验证日志。但是当需要查看它们时,日志文件很可能已经被改变而不会产生任何的错误消息。

攻击摘要文件的整个过程是:日志验证已停止并重新启动(而不是日志记录被停止并重新启动),这突出了警报CloudTrail更新的重要性,即使它不停止日志记录(一种方法是使用AWS CloudWatch服务来警告UpdateTrail事件)。如果摘要验证链中存在中断,则必须特别怀疑日志验证,因此这个时候有必要进行手动验证。因此,跟日志存储在单台受到攻击的主机情况很像,当我们处理受到攻击的AWS帐户时,应该仔细地分析日志文件是否被篡改。

(完)