Crystal Malware

I enjoy learning about new programming languages, so I decided to have a look at Crystal - a general purpose, object-oriented language. Unfortunately, the title is complete clickbait - this will just be a short post about my first impressions of the language and some of the things I found interesting about it. Crystal is only 10 years old at the time of writing, which is relatively young as far as programming languages go. It's cross-platform, so can be used on Windows, macOS, and Linux.

Syntax

The syntax was inspired by a number of languages, including Ruby, Rust, C, C#, and Python. If you're familiar in any of those, Crystal should be easy to pick up. Multi-line objects like classes, methods, and structs are closed with the end keyword rather than using curly braces. The naming convention is PascalCase for types snake_case for methods and variables, and SCREAMING_SNAKE_CASE for constants.

Here's a simple program:

class Person
    def initialize(name : String)
      @name = name
    end
  
    def name
      @name
    end
end

rasta = Person.new("rasta")
puts("My name is #{rasta.name}")

test.cr

Crystal source code is top-level scoped, so there is no main method or equivalent thereof. Objects like classes and variables are placed at the top of a file, and the main code body afterwards. A class is defined with the class keyword and a method (or more accurately, an object) with the def keyword. The initialize method is synonymous with a class constructor.

In Crystal, everything is an object (which is similar to other OOP languages like C#). Class properties are objects in their own right, so they are also defined using the def keyword. Their actual values, otherwise called 'instance variables', are defined using the @ character.

New objects are instantiated with the new method, which is an extension on the base object type. puts will print to standard out, and string interpolation is made possible with #{}.

PS C:\> crystal .\person.cr
My name is rasta

person.cr

The opening and closing brackets are optional in Crystal, so rasta = Person.new("rasta") and rasta = Person.new "rasta" are functionally identical; as is puts("My name is #{rasta.name}") and puts "My name is #{rasta.name}".

Type Inference

One of the more intriguing aspects is how Crystal handles type safety.

def sum(a, b)
    a + b
end

puts sum 10, 5

sum.cr

PS C:\> crystal .\sum.cr
15

You'll notice in the snippet above that sum has an implicit return (i.e. it returns the result of a + b without needing to specify the return keyword), but there is nothing stating what data type a or b should be. What would happen if you tried to do puts sum 10, "a"?

Turns out, the compiler will throw an error:

Error: expected argument #1 to 'Int32#+' to be Float32, Float64, Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64 or UInt8, not String

Overloads are:
 - Int32#+(other : Int8)
 - Int32#+(other : Int16)
 - Int32#+(other : Int32)
 - Int32#+(other : Int64)
 - Int32#+(other : Int128)
 - Int32#+(other : UInt8)
 - Int32#+(other : UInt16)
 - Int32#+(other : UInt32)
 - Int32#+(other : UInt64)
 - Int32#+(other : UInt128)
 - Int32#+(other : Float32)
 - Int32#+(other : Float64)
 - Number#+()

So it's smart enough to know what the data types should be based on the context and builds the appropriate overloads without the developer having to do so manually. We can throw a mixture of integers and floats in there and it handles it without issue.

def sum(a, b)
    a + b
end

puts sum 10, 5
puts sum 10, 6.7_f32

sum.cr

PS C:\> crystal .\sum.cr
15
16.7

Variable Assignment

Another crazy feature is that the same variable can be re-assigned with data of a different type. For example:

my_variable = 1
puts my_variable

my_variable = "My Variable"
puts my_variable

var.cr

PS C:\> crystal .\var.cr
1
My Variable

There is also no Type, let, or var keyword when defining a variable - just its name and value.

Shards

A 'shard' is to Crystal what a 'crate' is to Rust. They are ways of writing modular libraries that can be consumed by other projects. I didn't dive into them too much, so don't have anything to say other than they're there.

C-Bindings

Crystal can bind to C libraries, including the Win32 APIs. The syntax for doing so is a little like Rust. @[Link("...")] will pass the given library name to the linker and the lib keyword declares a group of external functions. Each function is defined with the fun keyword followed by the method signature.

@[Link("kernel32")]
lib Kernel32
    fun ExitProcess(exitCode : LibC::UInt) : NoReturn
end

Kernel32.ExitProcess 42

interop.cr

PS C:\> crystal .\interop.cr
PS C:\> $LASTEXITCODE
42

Inline Assembly

Crystal also provides a number of low-level primitives, such as sizeof (which are useful when using c-bindings), and the ability to write inline assembly.

asm("xor %r10, %r10")   # ProcessHandle
asm("mov $$1337, %rdx") # ExitStatus
asm("mov $$44, %rax")   # NtTerminateProcess
asm("syscall")

asm.cr

PS C:\> crystal .\asm.cr
PS C:\> $LASTEXITCODE
1337

You can go much deeper by using values that are bound to your variables. The example that's provided in their documentation is something like:

dst = 0
asm("mov $$1234, $0" : "=r"(dst))
p! dst

asm.cr

PS C:\> crystal .\asm.cr
dst # => 1234

Crystal Complier

The compiler is obviously capable of outputting an executable file.

PS C:\> crystal build .\hello-world.cr --release --no-debug
PS C:\> .\hello-world.exe
Hello World

But since Crystal uses an LLVM backend, it's also capable of emitting the LLVM IR, raw assembly, and object files (Crystal BOFs anyone?).

PS C:\> crystal build .\hello-world.cr --emit asm,obj,llvm-ir

Conclusion

Overall, Crystal seems like a really solid language. The type inference in particular helps keeps the code clean and makes it feel very dynamic. It packs a lot of power but the syntax feels much more elegant and accessible than some other languages (cough, Rust). There are a bunch of other features like the stdlib, union types and fibres that I didn't explore this time around, but it makes me excited to do so.

From an infosec perspective, being LLVM-backed opens a world of opportunity in terms of obfuscation. Being cross-platform, plus c-bindings, plus inline assembly, makes it so versatile. I honestly think you would make wicked malware with this.