Under the Hood

Although AWS doesn't make its details public, there's nothing wrong about reversing whats available to us. In fact, some already did it before.

In this example, we'll work on Lambda Shell, a tool to grab parts of the AWS Java Runtime for Lambda, and make a few investigations of our own.1

1. Credit goes to Eric Hammond of Alestic for doing this first

Downloading the Examples

From now on, we'll refer to the lambda examples as the contents of the lambada-book-examples git repository, under https://github.com/ingenieux/lambada-book-examples. At this point, all you need to do is to clone it using git, and import into your favorite IDE.

Regarding the Decompilation part of the drill, I'll be using IntelliJ Idea with its Decompiler Plugin. There's no reason to think the Community Version can't do this, so I'm taking it for granted for now.

Introducing Lambda Shell

So, what if we could run applications under Lambda? There's nothing wrong about it, as long as you remember to keep your memory footprint low, since Java has several problems when it comes to executing large process.

Under the lambda-shell project, there's a lambda function which allows us to run commands. Lets give a look at it.

NOTE: For this example, in particular, you need to create a lambda_s3_full role with the AmazonS3Full policy for Lambda on IAM.

Once you've got the permission, you can deploy it with:

$ mvn -f lambda-shell/pom.xml -Pdeploy deploy

Testing the Environment

Now, open your AWS Console, and select the lb_shell function. Then on "Actions" | "Configure Test Event", enter these commands:

[
    "env",
    "ps -aux",
    "find / -type f -iname '*.jar'"
]

Then click "Test" and wait. Here are my results:

{
  "env": [
    "+ env",
    "AWS_LAMBDA_FUNCTION_VERSION=$LATEST",
    "AWS_SESSION_TOKEN=AQoDYXdzEDAawAJN9qlL8Svy3ITqY9r8dduc1cxfH++yt2QnZTGvcnhBdLCqG60lOo0uV1rKaviSr542nbYxdDsMYvrBflUPtALmNjf4BCQd4ipVdaKcR6omEYj98siQCrqZwgz4T9ymVQO1F2Ptd/UCRja8+amnyKaSzBw+1q+FRGcPTfP+O8I+8BDSLc8j8284Oj05N1xLKa7pRf8V76EdUvpUlb/E5kymJZmy0nKZhhH9DwVa1oV+ZtiNEAEONLgLbU8fuXZ4EndeGLaTy5SrkcLEGQflY3DD4+jsJ6O6h7ykA/Vdg0mHWXkO4UckICMq6xDQkEvEHJKFWuu3NAGek0A+QKKgZx5EwPqXGgDnpqmP1DDR0dEUID04itCuryk+oxIbW0SgojFwDZqO3e20uD6hnAtTDX5WMZnKZfdOb/0q7Mfa7fw5bSDTj7S4BQ==",
    "LD_LIBRARY_PATH=/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib",
    "AWS_LAMBDA_LOG_GROUP_NAME=/aws/lambda/lb_runCommand",
    "LAMBDA_TASK_ROOT=/var/task",
    "AWS_LAMBDA_LOG_STREAM_NAME=2016/04/12/[$LATEST]d34bbf383e8c4f80b0684b09f7471611",
    "NLSPATH=/usr/dt/lib/nls/msg/%L/%N.cat",
    "AWS_LAMBDA_FUNCTION_NAME=lb_runCommand",
    "PATH=/usr/local/bin:/usr/bin/:/bin",
    "AWS_DEFAULT_REGION=us-east-1",
    "PWD=/var/task",
    "AWS_SECRET_ACCESS_KEY=ZGPFZ9Z0SeY2WdAl7V5JUHScPdgOwpVC9mNQTz5g",
    "LAMBDA_RUNTIME_DIR=/var/runtime",
    "AWS_REGION=us-east-1",
    "XFILESEARCHPATH=/usr/dt/app-defaults/%L/Dt",
    "AWS_ACCESS_KEY_ID=ASIAJ5YESJ3CS6ROQXZA",
    "SHLVL=1",
    "CLASSPATH=/var/runtime:/var/runtime/lib/LambdaSandboxJavaAPI-1.1.jar",
    "AWS_ACCESS_KEY=ASIAJ5YESJ3CS6ROQXZA",
    "AWS_LAMBDA_FUNCTION_MEMORY_SIZE=512",
    "AWS_SECRET_KEY=ZGPFZ9Z0SeY2WdAl7V5JUHScPdgOwpVC9mNQTz5g",
    "_=/usr/bin/env"
  ],
  "ps -aux": [
    "+ ps -aux",
    "Warning: bad syntax, perhaps a bogus '-'? See /usr/share/doc/procps-3.2.8/FAQ",
    "USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND",
    "496          1  0.1  1.5 2425956 58548 ?       Ssl  14:36   0:00 java -XX:MaxHeapSize=445645k -XX:MaxMetaspaceSize=52429k -XX:ReservedCodeCacheSize=26214k -XX:+UseSerialGC -Xshare:on -XX:-TieredCompilation lambdainternal.LambdaRTEntry",
    "496         51  0.0  0.0  11608  2540 ?        S    14:43   0:00 /bin/bash -x /tmp/tmp-6740848046668077297.sh",
    "496         53  0.0  0.0  13592  2024 ?        R    14:43   0:00 ps -aux"
  ],
  "find / -type f -iname '*.jar'": [
    "+ find / -type f -iname '*.jar'",
    "find: `/root': Permission denied",
    "find: `/proc/tty/driver': Permission denied",
    "/tmp/runtime.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/ext/dnsns.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/ext/nashorn.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/ext/sunec.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/ext/sunpkcs11.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/ext/zipfs.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/ext/cldrdata.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/ext/sunjce_provider.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/ext/jaccess.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/ext/localedata.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/charsets.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/security/US_export_policy.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/security/local_policy.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/rt.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/jce.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/jsse.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/management-agent.jar",
    "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.8.amzn1.x86_64/jre/lib/resources.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/ext/dnsns.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/ext/sunpkcs11.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/ext/zipfs.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/ext/sunjce_provider.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/ext/localedata.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/charsets.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/security/US_export_policy.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/security/local_policy.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/rt.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/jce.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/jsse.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/management-agent.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/rhino.jar",
    "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.95.x86_64/jre/lib/resources.jar",
    "find: `/builddir': Permission denied",
    "find: `/etc/pki/CA/private': Permission denied",
    "find: `/var/cache/ldconfig': Permission denied",
    "/var/runtime/lib/joda-time-2.8.2.jar",
    "/var/runtime/lib/lambda-sandbox.jar",
    "/var/runtime/lib/jackson-databind-2.5.x.jar",
    "/var/runtime/lib/jackson-core-2.5.x.jar",
    "/var/runtime/lib/gson-2.3.1.jar",
    "/var/runtime/lib/jackson-annotations-2.5.x.jar",
    "/var/runtime/lib/LambdaSandboxJavaAPI-1.1.jar",
    "find: `/var/lib/yum/history/2016-03-30/2': Permission denied",
    "find: `/var/lib/yum/history/2016-03-30/4': Permission denied",
    "find: `/var/lib/yum/history/2016-03-30/1': Permission denied",
    "find: `/var/lib/yum/history/2016-03-30/3': Permission denied"
  ]
}

From here, we can make a few statements:

  • Your lambda code runs under a container (notice no parent process), and it is NOT a root process, thus limiting your autonomy on your Lambda Runtime Environment
  • Lambda sets you the AWS Permissions using IAM, and it is reflected in its Environment Variables
  • There's stub java code under /var/runtime

If you read the LambdaShell source code again, you'll notice that it contains a function to copy files to S3. Create a S3 Bucket and invoke lb_runCommand with this payload:

[
    "tar czvf /tmp/runtime.tar.gz /var/runtime/",
    "!aws s3 cp /tmp/runtime.tar.gz s3://<yourbucket>/aws-lambda-runtime.tar.gz"
]

Now you can copy it locally to your machine:

$ aws cp s3:///aws-lambda-runtime.targz .

On IntelliJ IDEA, for instance, you can unpack this tar.gz under a project workspace and evaluate. I used fernflower, the IDEA Decompiler, and got into a git repo so you can browse into this repository:

https://github.com/aldrinleal/lambda-java-runtime

Understanding the Lambda Shell Source

Lets see the relevant parts of the source:

/**
 * Represents a Shell Interface to AWS Lambda
 */
public class LambdaShell {
    public static void main(String[] args) throws Exception {
        final Map<String, List<String>> result = new LambdaShell().runCommands(Collections.singletonList("ps -aux"));

        System.err.println(result);
    }

    @LambadaFunction(name="lb_runCommand", memorySize = 512, timeout = 300, role="*/lambda_s3_full")
    public Map<String, List<String>> runCommands(List<String> inCommands) throws Exception {
        Map<String, List<String>> result = new LinkedHashMap<>();

        for (String e : inCommands) {
            List<String> commandResult = runCommand(e);

            result.put(e, commandResult);
        }

        return result;
    }

    public List<String> runCommand(String command) throws Exception {
        String[] args = command.split("\\s+");

        if (command.startsWith("!aws s3 cp")) {
            // !aws s3 cp <sourceFile> <targetPath>
            String sourceFile = args[3];

            String targetPath = args[4];

            copyFile(sourceFile, targetPath);
        } else {
            final ByteArrayOutputStream os = new ByteArrayOutputStream();

            runCommandArray(os, args);

            return Arrays.asList(new String(os.toByteArray()).split("\n"));
        }

        return null;
    }

    private void copyFile(String sourceFile, String targetPath) {
        AmazonS3 s3Client = new AmazonS3Client();

        AmazonS3URI uri = new AmazonS3URI(targetPath);

        String key = uri.getKey();

        String bucketName = uri.getBucket();

        s3Client.putObject(new PutObjectRequest(bucketName, key, new File(sourceFile)));
    }
}

The main() method is a test stub. Lambda won't use it under any circunstances, so we could test/debug it locally on my workstation. However, the next method is more interesting:

    @LambadaFunction(name="lb_runCommand", memorySize = 512, timeout = 300, role="*/lambda_s3_full")
    public Map<String, List<String>> runCommands(List<String> inCommands) throws Exception {

It tells us several things:

  • By default, we use the lambda_basic_execution role (it is configurable on the lambda-maven-plugin). But here, we are overwriting its role to use lambda_s3_full - which we created previously.
  • We are also overwriting memory size and timeout. Timeouts larger than 60 (seconds) don't have their output shown into the AWS Lambda Console (but you can see it under CloudWatch Logs though)
  • We're using bare structures. Reading an array and return a map of list of strings.

The method runCommand actually delegates between spawning a shell (wrapped into a .sh file) and/or running the aws s3 cp command (which is defined on the copyFile method.

Understanding the Runtime Source Code

TBD

Concluding

This method is useful, since we can validate the contents of the built-in container used for AWS Lambda (it is often updated - see its docs for reference), as well as investigate its inner workings and allowing us to run commands.