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:
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 #{}
.
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.
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.
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:
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.
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.
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:
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.