Solution to Bornhack 2020 CTF challenge caesar_with_a_twist

16/08/20 — capitol

caesar

Name:

caesar_with_a_twist

Category:

crypto

Points:

75

Writeup

The third challenge in the crypto category, this was the first one with some actual encryption.

We got an encrypted text:

CLLJE{QtLo_wF_r_YqOI_bXrH_gpJw_WxPh_Sm_QraVmo_Se_IoSvvn_XyR_GeU_Ack_EDRDTIosscf_rj}

And a Python program that described how it was generated, analysing that program showed that it was a regular substitution cipher where the substitution key was changed for each character. The key was a function of that characters position in the string.

We reimplemented it in rust and wrote a reverse implementation:

use std::fs::File;
use std::io::prelude::*;

#[derive(Debug, Clone)]
struct CaesarError;

fn caesar_encrypt(original: char, base: u32) -> Result<char, CaesarError> {
    let letter = original as u32;

    // Ensures the base rotation does not exceed 26
    // If the base is 28, it exceeds 26, and ends up being 2 (after modulus)
    let base = base % 26;

    // If the letter is a space, underscore or curly brackets, just return it without rotating
    if letter == 32 || letter == 95 || letter == 123 || letter == 125 {
        return Ok(std::char::from_u32(letter).unwrap());
    }

    // If capital letter
    if 'A' as u32 <= letter && letter <= 'Z' as u32 {
        // If the base exceeds the alphabet
        return if ('Z' as u32) < (letter + base) {
            Ok(std::char::from_u32(letter + base - 26).unwrap())
        } else {
            Ok(std::char::from_u32(letter + base).unwrap())
        }
    }

    // If non-capital letter
    if 'a' as u32 <= letter && letter <= 'z' as u32 {
        // If the base exceeds the alphabet
        return if ('z' as u32) < (letter + base) {
            Ok(std::char::from_u32(letter + base - 26).unwrap())
        } else {
            Ok(std::char::from_u32(letter + base).unwrap())
        }
    }

    Err(CaesarError {})
}

fn caesar_decrypt(original: char, base: u32) -> Result<char, CaesarError> {
    let letter = original as u32;

    // Ensures the base rotation does not exceed 26
    // If the base is 28, it exceeds 26, and ends up being 2 (after modulus)
    let base = base % 26;

    // If the letter is a space, underscore or curly brackets, just return it without rotating
    if letter == 32 || letter == 95 || letter == 123 || letter == 125 {
        return Ok(std::char::from_u32(letter).unwrap());
    }

    // If capital letter
    if 'A' as u32 <= letter && letter <= 'Z' as u32 {
        // If the base exceeds the alphabet
        return if ('A' as u32) > (letter - base) {
            Ok(std::char::from_u32(letter - base + 26).unwrap())
        } else {
            Ok(std::char::from_u32(letter - base).unwrap())
        }
    }

    // If non-capital letter
    if 'a' as u32 <= letter && letter <= 'z' as u32 {
        // If the base exceeds the alphabet
        return if ('a' as u32) > (letter - base) {
            Ok(std::char::from_u32(letter - base + 26).unwrap())
        } else {
            Ok(std::char::from_u32(letter - base).unwrap())
        }
    }

    Err(CaesarError {})
}

fn encrypt(s: &String) -> Result<String, CaesarError> {
    let mut result:Vec<char> = vec![];
    for (i, c) in s.chars().enumerate() {
        result.push(caesar_encrypt(c, ((i + 1) * (i + 1)) as u32)?);
    }
    Ok(result.iter().collect::<String>())
}

fn decrypt(s: &String) -> Result<String, CaesarError> {
    let mut result:Vec<char> = vec![];
    for (i, c) in s.chars().enumerate() {
        result.push(caesar_decrypt(c, ((i + 1) * (i + 1)) as u32)?);
    }
    Ok(result.iter().collect::<String>())
}

fn main() -> std::io::Result<()> {
    let mut file = File::open("cwat_encrypted_flag.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

    println!("{}", decrypt(&contents).unwrap());
    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::CaesarError;
    use crate::caesar_decrypt;
    use crate::caesar_encrypt;
    use crate::encrypt;
    use crate::decrypt;

    static ASCII_LOWER: [char; 26] = [
        'a', 'b', 'c', 'd', 'e',
        'f', 'g', 'h', 'i', 'j',
        'k', 'l', 'm', 'n', 'o',
        'p', 'q', 'r', 's', 't',
        'u', 'v', 'w', 'x', 'y',
        'z',
    ];

    static ASCII_UPPER: [char; 26] = [
        'A', 'B', 'C', 'D', 'E',
        'F', 'G', 'H', 'I', 'J',
        'K', 'L', 'M', 'N', 'O',
        'P', 'Q', 'R', 'S', 'T',
        'U', 'V', 'W', 'X', 'Y',
        'Z',
    ];

    #[test]
    fn loop_all_ascii_chars() -> Result<(), CaesarError> {
        for base in 0..25 {
            println!("base = {}", base);
            for c in &ASCII_LOWER {
                assert_eq!(*c, caesar_decrypt(caesar_encrypt(*c, base)?, base)?);
            }
            for c in &ASCII_UPPER {
                assert_eq!(*c, caesar_decrypt(caesar_encrypt(*c, base)?, base)?);
            }
        }

        Ok(())
    }

    #[test]
    fn encrypt_test() -> Result<(), CaesarError> {
        assert_eq!("Ulri sp d ksfh", encrypt(&"This is a test".to_string())?);

        Ok(())
    }

    #[test]
    fn encrypt_loop() -> Result<(), CaesarError> {
        assert_eq!("This is a test", decrypt(&encrypt(&"This is a test".to_string())?)?);

        Ok(())
    }
}

Flag was BHCTF{ThIs_iS_a_VeRY_lOnG_flAg_MaDe_By_CaeSar_To_EnSure_YoU_DiD_Not_BRUTUSforce_it}.

Solution to Bornhack 2020 CTF challenge alice_bob_playing_telepathy

16/08/20 — capitol

moon

Name:

alice_bob_playing_telepathy

Category:

crypto

Points:

400

Writeup

This was a challenge centered around attacking the Lua runtime in a C program.

We got this program

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <string.h>

#include <assert.h>

ssize_t readn(int fd, char *buf, size_t n){
    size_t off = 0;
    while(off < n){
        ssize_t res = read(fd, &buf[off], n-off);
        if(res <= 0) return res;
        off += res;
    }
    return off;
}

int main(int argc, char **argv){
    alarm(30);

    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
    puts("Give me some Alice and Bob:");

    char program[256 + 1] = {0};
    assert(readn(STDIN_FILENO, program, sizeof(program)-1) == sizeof(program)-1);

    lua_State *alice = luaL_newstate();
    //luaL_openlibs(alice); is too powerful
    luaL_requiref(alice, "math", luaopen_math, 1);
    luaL_requiref(alice, "table", luaopen_table, 1);
    luaL_dostring(alice, program);

    lua_State *bob = luaL_newstate();
    //luaL_openlibs(bob); is too powerful
    luaL_requiref(bob, "math", luaopen_math, 1);
    luaL_requiref(bob, "table", luaopen_table, 1);
    luaL_dostring(bob, program);

    int urandom = open("/dev/urandom", O_RDONLY);
    assert(urandom > 0);

    for(int i = 0; i < 1000; i++){
        unsigned int num = 0;
        assert(readn(urandom, (char *)&num, sizeof(num)) == sizeof(num));
        num = num % 100;

        // alice(num)
        lua_getglobal(alice, "alice");
        lua_pushnumber(alice, (float)num);
        assert(lua_pcall(alice, 1, 0, 0) == 0);

        // assert(num == bob())
        lua_getglobal(bob, "bob");
        assert(lua_pcall(bob, 0, 1, 0) == 0);
        assert(lua_isnumber(bob, -1));
        assert(num == (int)lua_tonumber(bob, -1));
        lua_pop(bob, 1);
    }

    lua_close(alice);
    lua_close(bob);

    puts(
#include "flag.h"
    );

}

It initializes two different lua_State objects from the same source code that the attacker supplies. Source that should have one function named alice that accepts a parameter, and another function bob that should return a number.

It then generates a series of 1000 random numbers between 0 and 100. Each of those gets sent to the alice function in the first lua_State and a number is read from the bob function in the second lua_State. Those two number are verified to be equal.

This means that we need to communicate between functions in different lua_State.

That the lua math module is loaded gave us a hint, and the random number generator seed turns out to not be part of the lua_State object in lua 5.3 and below, this security hole has been fixed in 5.4.

We wrote this function to reveal the relationship between the randomseed() and the random() function.


function alice(n)
  math.randomseed(0)
  for i=0,n do
    math.random()
  end
end
function bob()
  a = math.random()
  math.randomseed(0)
  for i=0,101 do
    if math.random() == a then
        return i-1
    end
  end
end

Sadly the CTF closed just before we sent this in, so we didn’t manage to get the flag, and could only verify this locally.

CVE-2020-14423: Convos 4.19 Generates a Predictable Secret

19/06/20 — sgo

Convos is an open source web based irc client.

While packaging convos for nixpkgs, we found that the application generated a predictable local_secret. This was caused by the following code:

my $secret = Mojo::Util::md5_sum(join ':', $self->core->home->to_string, $<, $(, $0);

The secret is derived from the:

  • Home directory of the application
  • Real UID of the process
  • Real GID of the process
  • Name of the executed program.

The local_secret will therefore be predictable when running under Docker, and easily guessable on other platforms.

Impact

A remote attacker can possibly use a derived secret to create invite links or reset passwords for existing users.

Vulnerable

Convos 4.19 and earlier are vulnerable.

Mitigation

Users should upgrade to Convos 4.20 or newer, and regenerate secrets for their installation.

Timeline

  • 2020-06-12: Vulnerability discovered, Vendor notified
  • 2020-06-14: Patch created by vendor
  • 2020-06-18: Version 4.20 released, CVE assigned.

References

Credits

Thanks to Jan Henning Thorsen for quickly fixing the vulnerability.

Vulnerability discovered by Stig Palmquist.

Fuzzing Sequoia-PGP

28/05/20 — capitol

sequoia

Sequoia is a promising new OpenPGP library that’s written in Rust. As Rust has excellent interoperability with C it also exposes itself as a C library in the sequoia_openpgp_ffi crate. This would be the way that you would call this library from other programming languages, as C often acts as the lowest common denominator.

As Sequoia is making progress towards a 1.0 release, I thought that it would be time to help out by trying to discover bugs in it by fuzzing, a technique where you generate random input to functions and observe the execution flow in order to detect problems.

When attacking a system it’s often useful to determine where the trust boundaries of the system lie, and the ffi crate is one of those boundaries. Parser code is a large source of bugs, it’s very hard to foresee all different types of invalid input when writing a parser so pgp_packet_parser_from_bytes looked like a prime candidate for fuzzing.

The Setup

I decided to use the cargo fuzz framework which turned out to be a good choice, it’s very simple to get started with.

Initial setup:

cargo install cargo-fuzz
git clone https://gitlab.com/sequoia-pgp/sequoia.git
cd sequioa
cargo fuzz init

This creates the setup you need for fuzzing and adds a skeleton file in fuzz/fuzz_targets/fuzz_target_1.rs

I implemented the target like this:

#![no_main]
use libfuzzer_sys::fuzz_target;

extern crate sequoia_openpgp_ffi;

fuzz_target!(|data: &[u8]| {
    if data.len() > 0 {
        let _ = sequoia_openpgp_ffi::parse::pgp_packet_parser_from_bytes(core::option::Option::None, &data[0], data.len());
    }
});

The framework generates random data and uses that to call the fuzz_target function. I then simply pass that data onto the sequoia_openpgp_ffi::parse::pgp_packet_parser_from_bytes function.

I started to fuzzing like this:

cargo fuzz run fuzz_target_1 -- -detect_leaks=0

The framework also detects memory leaks, and there was some output related to that. But that’s not what I’m looking for today so I disable that with -- -detect_leaks=0.

Findings

It seems like I was the first person to run a fuzzer against that function, so I found the following:

  1. An integer overflow on a shift left
  2. An attempt to parse invalid UTF-8
  3. An attempt to read nonexisting data

The framework is very explicit in telling us what data caused the broken behaviour, so it’s trivial to take that input and build a unit test from it. This helps the maintainers to triage and fix the problems.

It can also try to minimize the test input automatically, so to keep the tests small. But that functionality seemed to be broken.

Security Implications

Since this is Rust, these panics don’t cause undefined behaviour as they might have done in for example C. So these findings will at most cause a denial of service due to the process crashing.

Thanks

Big thanks to the Sequoia team who was very responsive when I reported the issues and especially Neal on that team.

Packaging Rust for Debian - part II

26/05/20 — capitol

rusty-steel

Let’s do another dive into packaging Rust for Debian with a slightly more complicated example.

One great tool in the packager’s toolbox is cargo-debstatus. By running it in the root of your crate you will get a list of your dependencies, together with information regarding its packaging status in Debian.

For ripasso a part of the tree looks like below at the time of writing. ripasso depends on gpgme which depends on a number of other Rust libraries (and a number of native ones which aren’t shown here).

├── gpgme v0.9.2
│   ├── bitflags v1.2.1 (in debian)
│   ├── conv v0.3.3
│   │   └── custom_derive v0.1.7
│   ├── cstr-argument v0.1.1 (in debian)
│   ├── gpg-error v0.5.1 (in debian)
│   ├── gpgme-sys v0.9.1
│   │   ├── libc v0.2.66 (in debian)
│   │   └── libgpg-error-sys v0.5.1 (in debian)
│   ├── libc v0.2.66 (in debian)
│   ├── once_cell v1.3.1 (in debian)
│   ├── smallvec v1.1.0 (in debian)
│   └── static_assertions v1.1.0

One of the dependencies is static_assertions which is actually already packaged in Debian, but version 0.3.3 and we need 1.1.0. Let’s investigate how to fix this one.

In order to verify that we won’t break any other package by upgrading the existing Debian package to 1.1.0 we run list-rdeps.sh.

$ ./dev/list-rdeps.sh static-assertions
APT cache is a bit old, update? [Y/n] n
Versions of rust-static-assertions in unstable:
  librust-static-assertions-dev                    0.3.3-2

Versions of rdeps of rust-static-assertions in unstable, that also exist in testing:
  librust-lexical-core-dev                         0.4.3-1+b1       depends on     librust-static-assertions-0.3+default-dev (>= 0.3.3-~~),

Here we see that the lexical-core package depends on static-assertions and a quick git clone and compilation confirms that it doesn’t compile with version 1.1.0.

We can take this one step further

$ ./dev/list-rdeps.sh lexical-core
APT cache is a bit old, update? [Y/n] n
Versions of rust-lexical-core in unstable:
  librust-lexical-core+correct-dev                 0.4.3-1+b1
  librust-lexical-core+default-dev                 0.4.3-1+b1
  librust-lexical-core-dev                         0.4.3-1+b1
  librust-lexical-core+dtoa-dev                    0.4.3-1+b1
  librust-lexical-core+grisu3-dev                  0.4.3-1+b1
  librust-lexical-core+ryu-dev                     0.4.3-1+b1
  librust-lexical-core+stackvector-dev             0.4.3-1+b1

Versions of rdeps of rust-lexical-core in unstable, that also exist in testing:
  librust-lexical-core+correct-dev                 0.4.3-1+b1       depends on     librust-lexical-core+table-dev (= 0.4.3-1+b1),
  librust-lexical-core+default-dev                 0.4.3-1+b1       depends on     librust-lexical-core+correct-dev (= 0.4.3-1+b1), librust-lexical-core+std-dev (= 0.4.3-1+b1),
  librust-nom+lexical-core-dev                     5.0.1-4          depends on     librust-lexical-core-0.4+default-dev,
  librust-nom+lexical-dev                          5.0.1-4          depends on     librust-lexical-core-0.4+default-dev,

And we see that nom depends on lexical-core.

$ ./dev/list-rdeps.sh nom
APT cache is a bit old, update? [Y/n] n
Versions of rust-nom in unstable:
  librust-nom+default-dev                          5.0.1-4
  librust-nom-dev                                  5.0.1-4
  librust-nom+lazy-static-dev                      5.0.1-4
  librust-nom+lexical-core-dev                     5.0.1-4
  librust-nom+lexical-dev                          5.0.1-4
  librust-nom+regex-dev                            5.0.1-4
  librust-nom+regexp-dev                           5.0.1-4
  librust-nom+regexp-macros-dev                    5.0.1-4
  librust-nom+std-dev                              5.0.1-4

Versions of rdeps of rust-nom in unstable, that also exist in testing:
  librust-cexpr-dev                                0.3.3-1+b1       depends on     librust-nom-4+default-dev, librust-nom-4+verbose-errors-dev,
  librust-dhcp4r-dev                               0.2.0-1          depends on     librust-nom-5+default-dev (>= 5.0.1-~~),
  librust-iso8601-dev                              0.3.0-1          depends on     librust-nom-4+default-dev,
  librust-nom-4+lazy-static-dev                    4.2.3-3          depends on     librust-nom-4-dev (= 4.2.3-3),
  librust-nom-4+regex-dev                          4.2.3-3          depends on     librust-nom-4-dev (= 4.2.3-3),
  librust-nom-4+regexp-macros-dev                  4.2.3-3          depends on     librust-nom-4-dev (= 4.2.3-3), librust-nom-4+regexp-dev (= 4.2.3-3),
  librust-nom-4+std-dev                            4.2.3-3          depends on     librust-nom-4-dev (= 4.2.3-3), librust-nom-4+alloc-dev (= 4.2.3-3),
  librust-nom+default-dev                          5.0.1-4          depends on     librust-nom+std-dev (= 5.0.1-4), librust-nom+lexical-dev (= 5.0.1-4),
  librust-nom+regexp-macros-dev                    5.0.1-4          depends on     librust-nom+regexp-dev (= 5.0.1-4),
  librust-nom+std-dev                              5.0.1-4          depends on     librust-nom+alloc-dev (= 5.0.1-4),
  librust-pktparse-dev                             0.4.0-1          depends on     librust-nom-4+default-dev (>= 4.2-~~),
  librust-rusticata-macros-dev                     2.0.4-1          depends on     librust-nom-5+default-dev,
  librust-tls-parser-dev                           0.9.2-3          depends on     librust-nom-5+default-dev,
  librust-weedle-dev                               0.10.0-3         depends on     librust-nom-4+default-dev,

And a lot of things depends on nom.

So in order to package static_assertions so that we can package gpgme we can choose one of three different strategies:

  1. Package both versions of static_assertions
  2. Upgrade lexical-core, nom and everything nom depends on to newer versions
  3. Patch version 0.4.3 of lexical-core to use a newer version of static_assertions

Packaging both versions of static_assertions

This is a working strategy, but packaging both means that we need to create a new package for version 0.3 of static_assertions. New packages in Debian go through the new queue, where a member of the ftp masters team needs to manually verify so that it doesn’t contain any non-free software.

Therefore we will not choose this strategy.

Upgrading lexical-core, nom and everything nom depends on to newer versions

There exists a new version of lexical-core that depend on static_assertions 1, but the newer version of nom is a beta version of nom 6, and upgrading to that version would mean that we would need to patch all the incompatibilities in the applications that use nom.

A lot of non-trivial work, specially as we in that case would like to upstream the patches so that the maintenance burden doesn’t grow too much.

Patching version 0.4.3 of lexical-core to use a newer version of static_assertions

It turns out that there is an upgrade commit in lexical-core that applies cleanly to version 0.4.3. This is what we will use.

So we take that commit as a patch and place it into the patches directory together with a series file that just lists what order to patches should be applied in.

That enables us to upgrade the static_assertions package to version 1.1.0 without breaking any other package.