We have a binary that prints a welcome message and asks for 128 bytes of input. The input is decrypted using textbook RSA with a known modulus and 0x10001
as the exponent, and if the padding is (almost) correct, marked as executable and executed.
int main()
{
// print welcome banner
if(fread(ciphertext,1,0x80,stdin) != 0x80) {
("Your wish list is too short!");
puts(-1);
exit}
(N);
mpz_init(N,0x80,1,1,0,0,pubkey,0x1013e3);
mpz_import(C);
mpz_init(C,0x80,1,1,0,0,ciphertext,0x101426);
mpz_import(C,C,0x10001,N);
mpz_powm_ui
(plaintext,&len_plain,1,1,0,0,C,0x101479);
mpz_export
(plaintext + (0x80 - len_plain),plaintext,len_plain);
memmove(plaintext,0,0x80 - len_plain);
memset[128] = 0;
plaintext
// checks that plaintext is of the form "\x00\x02" + g + "\x00" + c and returns c
// (where g is anything with no zero-bytes)
= rsa_pkcs1_unpad(plaintext);
exec
if (exec == (undefined *)0x0) {
("Naughty!");
puts}
else {
("Nice!");
puts= strlen(exec);
len (exec,len);
make_executable(*(code *)exec)();
}
return 0;
}
If the program used 3 as the exponent, it would be vulnerable to purely crypto-based attacks, such as the one in Bleichenbacher ’06, “Forging some RSA signatures with pencil and paper”. They, however, won’t work here (the basic idea they rely on is that a message can be short enough its cube is still less than the modulus).
We will exploit two vulnerabilities:
00 02 xx xx .. xx 00 <data>
, where xx
are arbitrary non-zero bytes, andmake_executable
will mark both the plaintext and ciphertext buffers (which are adjacent) as executable.Consider a ciphertext that looks like the following, where <shellcode>
is something we want to execute (like execve("/bin/sh", 0, 0)
) and <padding>
is a few random bytes,
90 90 90 .. 90 <shellcode> <padding>
such that it decrypts to something like
00 02 xx xx .. xx 00 eb XX ...
After removing the padding, the program will execute eb XX
, which is a near jump that can move the program counter by up to ~128 bytes in either direction (PC += sign_extend(XX)
). If the XX
offset is such that this lands in the NOP sled (90 90 ...
), it will jump to the ciphertext and execute our shellcode.
For this to work, we need to guess three bytes (00
, 02
and eb
right after the first zero-byte), and we need an offset that lands in the correct place. Even if there was only one valid value of XX
, this would require reasonably many (around 2^32) choices of padding, and since there are many such valid values, we can brute-force a working payload in around fifteen minutes.
Flag: AOTW{Nev3r_Ev3r_r0ll_ur_0wn_crypt0}