Malware Analysis Pipeline in AWS (part 1)

The next objective I want to deliver is to allow the code that I will be enabling the ability to access the infrastructure I just put together in Malware Analysis Pipeline in AWS (part 0). You see, AWS instances that execute code typically initialize in an unprivileged (and, in the case of Lambda, isolated) state. This means that, if I want these components actually working together, I will need to do additional work to grant them permission to interact with one another.

Access Management for Lambda Service

I first need to list out all of the actions I would like my Lambda service to perform. I built the following list, earlier:

  1. Read new, tagged malware samples uploaded into an AWS S3 bucket
  2. Write a copy of these into a destination S3 Bucket
  3. Publish an SNS notification to a specific topic, announcing the transfer has completed

In addition to the above, Lambda itself has some requirements as well for proper execution and debugging. Namely: Writing log entries to CloudWatch.

As a general best practice, detailed logging in order to debug code and diagnose problems is important. AWS Lambda provides a very nice outlet for achieving this using Lambda and CloudWatch logs. I will also need to explicity permit my Lambda code to use this service so that I can use CloudWatch to log run time details.

Create the CloudWatch Log Infrastructure

As I'd like to maintain a least-privileged model of execution, rathre than rely upon Lambda to create my Log Group for my application, I am going to manually create it using the AWS API. By default, AWS Lambda log streams will be put into the group named /aws/lambda/Lambda-Name, where Lambda-Name is the name of your lambda function created in AWS.

This means I'll need to decide what to call my Lambda function. For this particular one, I will use the name ckane-blog-archive-malware, and therefore will want to create a new log group for it named /aws/lambda/ckane-blog-archive-malware.

aws logs create-log-group --log-group-name '/aws/lambda/ckane-blog-archive-malware'

Create IAM Role & Policy

Next, I need to create an IAM role in AWS. A Role is kind of like an identity in most computing systems: it is granted authorization to perform specific tasks, and may even be explicitly restricted from performing other tasks. The sets of permissions are called "policies" in AWS. So, the next step will be to write IAM policies that will be attached to the new IAM Role, so that the code deployed to AWS Lambda will be able to perform the necessary actions using the other AWS services: SNS, CloudWatch logs, S3. The term describing an IAM Role for AWS Lambda deployments is an Execution Role. In other words, it is the role that will be associated with the running context while my code is executing.

The first step is creating new policies. I am going to create a single policy, within which I will define the individualized permissions, and then this policy will be the only policy attached to the IAM Execution Role. However, it is entirely reasonable to break the permissions into individual policies, after which all of these would need to be associated with the IAM Execution Role. This might be beneficial, for instance, if I were to make other interfaces intended to accept new malware into the system. In this case, there would be some policy content that could be shared between both IAM Roles, as well as individualized policies for both.

Each service has a family of operations that can be permitted/restricted via these policies. The way AWS organizes these is via a hierarchy where a service has a prefix for all operations that are permissable for it. For instance, S3 has GetObject and PutObject operations. These are in the s3 family of policy attributes, so that some other future AWS service may also opt to describe similar service actions. Due to this, AWS requires that these be described as s3:GetObject and s3:PutObject, respectively, in any policies. These are known as Actions in the IAM policy.

In addition to this, some operations act upon one or more named entities within the service, called Resources. In addition to the operations described above, it is also sometimes necessary to author policies which include patterns or regular strings that describe the resource or resources that the operation will be limited to. For the CloudWatch log group that I created above, the group /aws/lambda/ckane-blog-archive-malware is one such resource. The actual name of this resource will be longer and more complex than the simple string name above, for reasons that will be somewhat explained below.

In any case, I will be defining policies on the following services, which have the policy family names listed below:

AWS S3: s3:*, (AWS Documentation on S3 Actions) AWS SNS: sns:*, (AWS Documentation on SNS Actions)

Grant Permission for S3 Read From Inbound Bucket

I want to grant S3 bucket read access. In our architecture, I am going to make the Lambda function rely upon what is known as a "trigger", which will notify the function to execute when a new object is written into the S3 bucket. This will contain the name of the object uploaded, therefore I only need to grant read access to objects in the bucket, at a minimum. In addition to the content of the object, I also want to be able to read the user-provided tags from the object as well.

        {
            "Sid": "InputBucket1",
            "Effect": "Allow",
            "Action": [
                "s3:GetObjectTagging",
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::ckane-blog-new-malware/*"
        },

Grant Permission for S3 Write To Output Bucket

The Lambda function is expected to copy the new malware sample into a destination bucket. In order to facilitate this, a policy needs to be created which allows the function to write into the bucket I created. This will allow me to retain any of the tag information, including the event tag, that was provided by the uploader. The naming convention is similar to the read actions earlier, granting permission for s3:PutObject and s3:PutObjectTagging.

        {
            "Sid": "OutputBucket1",
            "Effect": "Allow",
            "Action": [
                "s3:PutObjectTagging",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::ckane-blog-mwzoo/*"
        },

Grant Permission for SNS Notification

Finally, I need to grant permission for the function to publish notifications to the SNS topic that was created. This involves granting privileges for the sns:Publish action on the arn:aws:sns:*:*:ckane-blog-mwzoo-new-sample SNS topic that was created in Part0 of this series.

        {
            "Sid": "NotifyQueue1",
            "Effect": "Allow",
            "Action": "sns:Publish",
            "Resource": "arn:aws:sns:*:*:ckane-blog-mwzoo-new-sample"
        },

Final Lambda Policy

The final policy document should look like this:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "InputBucket0",
            "Effect": "Allow",
            "Action": [
                "s3:GetObjectTagging",
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::ckane-blog-new-malware/*"
        },
        {
            "Sid": "DestinationBucket0",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectTagging"
            ],
            "Resource": "arn:aws:s3:::ckane-blog-mwzoo/*"
        },
        {
            "Sid": "NotifyQueue0",
            "Effect": "Allow",
            "Action": "sns:Publish",
            "Resource": "arn:aws:sns:*:*:ckane-blog-mwzoo-new-sample"
        }
    ]
}

The above policy can be saved into a file, I used the name acceptor_policy.json. After this, the following command will create a new policy in the IAM system which will manage these permissions and enable them to be granted to roles:

aws iam create-policy --policy-name ckane-blog-mwzoo-acceptor --policy-document 'file://acceptor_policy.json' \
    --description 'This is a policy granting additional permissions for the MWZoo malware acceptor role'

Once the custom policy is created, I am presented with output similar to the following:

{
    "Policy": {
        "PolicyName": "ckane-blog-mwzoo-acceptor",
        "PolicyId": "POL0ID1VALUE2HERE",
        "Arn": "arn:aws:iam::123456789012:policy/ckane-blog-mwzoo-acceptor",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2019-08-28T03:23:10Z",
        "UpdateDate": "2019-08-28T03:23:10Z"
    }
}

Note that the values POL0ID1VALUE2HERE and 123456789012 are both placeholders masking unique identifiers specific to my account. These will be different from account to account. The Arn value above is the unique identifier that will be used in the next section to attach this newly created policy to the role that I will create. I will need to save that value for future use. However, if I ever forget it, I can use the following command to list all of my custom policies, and find it again:

aws iam list-policies --scope "Local"

Create the Lambda Execution Role

Next, an AWS Lambda Execution role needs to be created and needs to be granted the permissions outlined in the policy above, as well as the AWSLambdaBasicExecutionRole. This will come in two phases: First, create the role and provide it an "Assume Role Policy Document", which defines what subsystems are allowed to assume this role. Second, I will attach the policies to the role that grant the permission my Lambda service needs to execute my code.

To meet the first objective, I create a new file called acceptor_assumerole_policy.json, and define that the role can only be assumed by AWS Lambda:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

Next, I execute the following command to create the new role:

aws iam create-role --role-name "ckane-blog-acceptor-lambda-role" \
    --description "AWS Lambda Execution Role for MWZoo Acceptor" \
    --assume-role-policy-document "file://acceptor_assumerole_policy.json"

I can see that everything was successful when I received the following output:

{
    "Role": {
        "Path": "/",
        "RoleName": "ckane-blog-acceptor-lambda-role",
        "RoleId": "ALPHA0NUM1ID2VAL",
        "Arn": "arn:aws:iam::123457689012:role/ckane-blog-acceptor-lambda-role",
        "CreateDate": "2019-08-28T02:52:32Z",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "lambda.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}

The above yields a role Id (which I have redacted and relabled), ALPHA0NUM1ID2VAL, as well as an AWS ARN for the role. These identifiers uniquely identify the role, and will be used later on for configuring the AWS Lambda service to assume this role. Note that the value 123456789012 is merely a placeholder I have added to represent the AWS account Id. This value will have to be updated with your account Id from AWS. An easy way to determine this would be to use aws iam list-policies --scope Local and look at the ARNs for the local policies.

The following two commands grant the privileges I just defined in my IAM Policy named ckane-blog-archive-malware-policy, as well as the AWS-supplied policy named AWSLambdaBasicExecutionRole:

aws iam attach-role-policy --role-name "ckane-blog-acceptor-lambda-role" \
    --policy-arn "arn:aws:iam::123456789012:policy/ckane-blog-archive-malware-policy"

aws iam attach-role-policy --role-name "ckane-blog-acceptor-lambda-role" \
    --policy-arn "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"

Conclusion

In conclusion, I have constructed the access control infrastructure that I will be using for my service. AWS offers very fine-grained access control so that developers can utilize a least-privilege approach to permissions. This project is intended to execute code that will be analyzing undetermined input, and if something were to go wrong and an unexpected vulnerability or destructive failure were to occur, this approach ensures that any damage & exposure will be limited to this module and the handful of subsystems it has access to.

The next entry (part 2) will cover creating the AWS Lambda service, assigning its execution role, and working with some initial test data.