Heartbleed на Rust

http://www.tedunangst.com/flak/post/heartbleed-in-rust
  • Перевод
В комментариях к одной из ссылок на Hacker News некто утверждал, что использование Rust предотвратило бы Heartlbeed, что код бы даже не скомпилировался. Это прозвучало как вызов!

Тред начинается вот здесь. Я не собирался ни к кому придираться, но утверждение о предотвращении Heartbleed оказалось удачно сформулировано. В отличие от расплывчатых заявлений о безопасности работы с памятью в целом, конкретно данное утверждение можно протестировать.

Я не планирую реализовать весь стек TLS на Rust, поэтому срежу путь и уменьшу масштаб проблемы. Надеюсь, что моя модель сохранит суть проблемы. В двух словах, цель такова: написать программу, которая читает файл (пакет) из файловой системы (сети) и отправляет его обратно (этакий сетевой вариант echo). Длина echo-запроса будет закодирована одним байтом, за которым следуют данные. Это эквивалентно уязвимости TLS. Наша программа будет принимать пару таких пакетов, yourping и myping, и отвечать пакетами yourecho и myecho. Если какие-либо данные из пакета your просочатся в пакет my, у нас проблема: heartbleed1.

Начнём с простой программы на Rust.

use std::old_io::File;

fn pingback(path : Path, outpath : Path, buffer : &mut[u8]) {
        let mut fd = File::open(&path);
        match fd.read(buffer) {
                Err(what) => panic!("say {}", what),
                Ok(x) => if x < 1 { return; }
        }
        let len = buffer[0] as usize;
        let mut outfd = File::create(&outpath);
        match outfd.write_all(&buffer[0 .. len]) {
                Err(what) => panic!("say {}", what),
                Ok(_) => ()
        }
}


fn main() {
        let buffer = &mut[0u8; 256];
        pingback(Path::new("yourping"), Path::new("yourecho"), buffer);
        pingback(Path::new("myping"), Path::new("myecho"), buffer);
}

Программа компилируется, хотя и с предупреждениями из-за ламерского использования std::old_io. Не бог весть какой код, но и не самый ужасный. К примеру, мне удалось не использовать небезопасные межъязыковые интерфейсы (FFI) для вызова memcpy из C.

Давайте посмотрим, что программа делает с простыми входными данными.

$ echo \#i have many secrets. this is one. > yourping
$ echo \#i know your > myping
$ ./bleed
$ cat yourecho
#i have many secrets. this is one.
$ cat myecho
#i know your
secrets. this is one.

Бинго! Секретные данные утекли.

Конечно же, настоящий программист на Rust никогда не напишет подобной программы, поэтому, вероятно, я ещё и не продемонстрировал Heartbleed на Rust.

Давайте отдохнём от Rust и рассмотрим эквивалентный код на C.

#include <fcntl.h>
#include <unistd.h>
#include <assert.h>

void
pingback(char *path, char *outpath, unsigned char *buffer)
{
        int fd;
        if ((fd = open(path, O_RDONLY)) == -1)
                assert(!"open");
        if (read(fd, buffer, 256) < 1)
                assert(!"read");
        close(fd);
        size_t len = buffer[0];
        if ((fd = creat(outpath, 0644)) == -1)
                assert(!"creat");
        if (write(fd, buffer, len) != len)
                assert(!"write");
        close(fd);
}

int
main(int argc, char **argv)
{
        unsigned char buffer[256];
        pingback("yourping", "yourecho", buffer);
        pingback("myping", "myecho", buffer);
}

Анкетирование показало, что ни один настоящий программист на C никогда не напишет такой программы. Что же мы имеем?

код, который не напишет ни один настоящий программист на C: heartbleed
код, который не напишет ни один настоящий программист на Rust: (задачка для читателя)


Смысл поста не в порицании Rust. Я мог бы написать похожую программу на Go, или даже на Haskell, если бы я был достаточно умён для понимания буррито. Смысл в том, что пока мы не поймём, что представляют собой уязвимости наподобие Heartbleed, мы едва ли сможем избежать их простым переключением на волшебный уязвимостойкий язык. Да, каждый слышал о Heartbleed, но это не обязательно делает его хорошим примером.

Возможно, аргумент о Heartbleed использовался не как отсылка к самому Heartbleed, а к пачке других больших и страшных проблем. Не уверен, что это делает аргумент лучше. «Уязвимости, подобные Heartbleed, но не слишком похожие» — плохо определённый класс проблем. Сложно оценить какие-либо утверждения о таком классе.

Говоря об уязвимостях и их разрешении, нам нужно быть точными и осторожными. Поднятый вокруг Heartbleed (Shellshock, и т.п.) хайп делает его привлекательной целью для построения аргументов, но стоит проверять сочетаемость примера и аргумента. Ошибочные примеры приводят к ошибочным решениям.

Примечания

1. bleed — сочиться, испускать

Ссылки

Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 10
    –2
    Ну тем не менее в каких-то ЯП проще отстрелить.
      +6
      Бесспорно, так и есть. Но и неуязвимых ЯП не существует.
      То есть, любовь к языку не должна совсем уж затуманивать взгляд — баги есть и будут, хотя и меньше/другого вида.
      +4
      Да подобные высказывания всегда были. Типа язык А гарантирует защиту от проблемы Б. Хочется людям верить в сказку. Помню, когда-то любили говорить, что на Java невозможны утечки памяти…
        +7
        Я знаю компанию в которой все до сих пор в это верять.
        При этом говнокодя текущие сервисы, которые приходится перезапускать раз в сутки.
        +20
        Основная опасность Heartbleed была в возможности получить от сервера данные за пределами сетевого буфера, т.е. байты, находящиеся далее на стеке или куче (где, теоретически, могли находится секретные ключи, пароли и т.п., которые клиент ну никак не мог получить). Ваша программа не демонстрирует этой проблемы, она лишь позволяет получить назад байты, которые мы только что отправили, и не более того. Для того, чтобы воспроизвести эту проблему на Rust, нужно выйти за пределы буфера, а не только прочитать с него больше байт, чем требуется, что Rust не позволит сделать (если, конечно, вы не используете небезопасные (unsafe) блоки).
          –2
          Во-первых, там был свой аллокатор памяти. Ни Rust, ни Ada не справятся с такой ситуацией. У них мощные bound checker'ы, но если пользователь выделил себе буфер и ковыряется в нём сам — ничего не попишешь. Во-вторых, длина генерируемого в ответ сообщение основывалось только на поле входящего сообщения, игнорируя длину самого этого сообщения. В-третьих, буфер использовался несколько раз без обнуления.

          The RFC 6520 Heartbeat Extension tests TLS/DTLS secure communication links by allowing a computer at one end of a connection to send a «Heartbeat Request» message, consisting of a payload, typically a text string, along with the payload's length as a 16-bit integer. The receiving computer then must send exactly the same payload back to the sender.

          The affected versions of OpenSSL allocate a memory buffer for the message to be returned based on the length field in the requesting message, without regard to the actual size of that message's payload. Because of this failure to do proper bounds checking, the message returned consists of the payload, possibly followed by whatever else happened to be in the allocated memory buffer.

          Да, и пост не мой — это перевод.
            +2
            И все таки сравнение не корректно. Согласно приведенной вами же цитате, выделялся буфер размера X под данные ответа, а отсылалось данных больше чем X, что, как утверждается, в Rust'е сделать невозможно. В вашем же примере автор взял буфер размера X и прочитал из него не более X данных.
          +2
          Согласен. Утверждать что доступ к выделенному необнуленному буферу и доступ к почти любому участку памяти это одно и то же как то странно.
          0
          Аналог на haskell, но так написать можно только специально. )

          module Heartbleed where
          
          import System.IO
          import Data.Array.IO
          import Data.Word
          
          copy :: String -> String -> IOUArray Int Word8 -> IO ()
          copy from to buffer = do
              from' <- openFile from ReadMode
              to' <- openFile to WriteMode
              c <- hGetArray from' buffer 250
              hPutArray to' buffer 250
              hClose from'
              hClose to'
          
          
          main = do
              buffer <- newArray_ (0,250)
              copy "yourping" "yourecho" buffer
              copy "myping" "myecho" buffer
          
          


           ~/Tmp -$ cat yourecho                                                                                                     limbo-home@chemist :)
          #i have many secrets. this is one.
          �8d��=��d�T�%                                                                                                                                     ~/Tmp -$ cat myecho                                                                                                       limbo-home@chemist :)
          #i know your
          secrets. this is one.
          �8d��=��d�T�%
          

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое