автордың кітабын онлайн тегін оқу Black Hat Go: Программирование для хакеров и пентестеров
Научный редактор Д. Старков
Перевод Д. Брайт
Том Стил, Крис Паттен, Дэн Коттманн
Black Hat Go: Программирование для хакеров и пентестеров. — СПб.: Питер, 2022.
ISBN 978-5-4461-1795-6
© ООО Издательство "Питер", 2022
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Об авторах
Том Стил (Tom Steele) работает на Go с момента выхода его первой версии в 2012 году и одним из первых в своей области использовал этот язык для создания инструментов противодействия киберугрозам. Том — ведущий консультант по исследованиям в Atredis Partners, более десяти лет работает в области оценки систем безопасности как в исследовательских целях, так и для борьбы с хакерскими атаками. Том проводил обучающие курсы на множестве конференций, включая Defcon, BlackHat, DerbyCon и BSides. Имеет черный пояс по бразильскому джиу-джитсу и регулярно участвует в соревнованиях регионального и национального уровня, а также основал в Айдахо собственную школу по этому боевому искусству.
Крис Паттен (Chris Patten) — сооснователь и ведущий специалист STACKTITAN — компании, занимающейся консультированием по безопасности специализированных служб защиты. Крис уже более 25 лет работает в этой сфере и за это время занимал множество различных должностей. Последние десять лет он консультировал ряд коммерческих и государственных организаций по многим направлениям, включая техники противодействия кибератакам, возможности выявления угроз и стратегии минимизации ущерба. На своей последней должности Крис выступал в роли лидера одной из крупнейших команд по противодействию атакам в Северной Америке.
Прежде чем стать консультантом, Крис служил в военно-воздушных силах США, оказывая технологическую поддержку в процессе боевых операций. Он был активным участником сообщества специальных операций Министерства обороны США в USSOCOM, консультировал группы по спецоперациям относительно чувствительных инициатив в области кибервойны. По завершении службы в армии Крис занимал ведущие должности во многих телекоммуникационных компаниях из списка Fortune 500, работая с партнерами в исследовательской сфере.
Дэн Коттманн (Dan Kottmann) — сооснователь и ведущий консультант STACKTITAN. Сыграл важную роль в росте и развитии крупнейшей североамериканской компании по противодействию киберугрозам, непосредственно участвуя в формировании навыков персонала, повышении эффективности процессов, улучшении пользовательского опыта и качества реализации услуг. На протяжении 15 лет Дэн занимался межотраслевым клиентоориентированным консультированием и развитием этой сферы, в первую очередь фокусируясь на информационной безопасности и поставке приложений.
Дэн выступал на разных национальных и региональных конференциях по безопасности, включая Defcon, BlackHat Arsenal, DerbyCon, BSides и др. Увлекается разработкой ПО и создал многие как открытые, так и проприетарные приложения, начиная с инструментов командной строки и заканчивая сложными трехуровневыми и облачными веб-приложениями.
О научном редакторе
Алекс Харви (Alex Harvey) всю жизнь посвятил работе в технологической сфере, занимался робототехникой, программированием, разработкой встраиваемых систем. Около 15 лет назад он перешел в сферу информационной безопасности, где сосредоточился на тестировании и исследованиях. Алексу всегда нравилось создавать инструменты для работы, и очередным средством для этого был выбран именно язык Go.
Предисловие
Языки программирования всегда влияли на индустрию информационной безопасности. Ограничения архитектуры, стандартные библиотеки и реализации протоколов, доступные в рамках каждого языка, в итоге определяли плоскость атаки любого создаваемого на их основе приложения. То же касается и инструментов безопасности. Правильно подобранный язык может упростить сложные задачи, а чрезвычайно сложные сделать совершенно тривиальными. Обширная экосистема Go делает этот язык очень привлекательным средством для разработки инструментов безопасности. Он переписывает правила как для разработки безопасных приложений, так и для создания соответствующих инструментов, позволяя задействовать более быстрые, безопасные и портативные средства.
За более чем 15 лет, которые я работал с Metasploit Framework, этот проект дважды полностью переписывался. Сначала лежащий в основе Perl был заменен на Ruby, а позднее добавлена поддержка ряда мультиязычных модулей, расширений и полезных нагрузок. Эти изменения отражают постоянно меняющиеся принципы разработки ПО. Если вы хотите поспевать за тенденциями развития систем безопасности, то должны своевременно адаптировать свои инструменты. При этом использование грамотно подобранного языка позволит сэкономить уйму ценного времени. Но так же, как и Ruby, Go не стал применяться повсеместно в одночасье. Выбор нового языка с неокрепшей экосистемой для разработки продукта, представляющего ценность, требует не только решимости, но и веры, ведь этот процесс, помимо прочего, будет сопряжен с трудностями, вызванными недостатком нужных библиотек, не успевающих появляться в нужное время.
Авторы «Black Hat Go» одними из первых начали использовать этот язык для разработки инструментов безопасности, создав самые ранние открытые проекты, включая BlackSheepWall, Lair Framework, sipbrute и многие другие. Все они представляют собой прекрасные примеры возможностей применения Go. Авторам одинаково легко дается как создание софта, так и его разбор по крупицам, и данная книга отлично демонстрирует их способность ловко комбинировать эти навыки.
Издание предоставляет все необходимое для начала разработки в области безопасности, не перегружая читателя малоиспользуемыми возможностями языка. Хотите написать до смешного быстрый сетевой сканер, вредоносный HTTP-прокси или кросс-платформенный фреймворк командования и управления (Command and Control)? Вы обратились по адресу! Если у вас уже есть опыт программирования и вы ищете знания по разработке инструментов безопасности, то эта книга предоставит вам как основные концепции, так и компромиссы, которые хакеры всех мастей принимают в расчет при написании инструментов. На приведенных в этой книге техниках многому могут научиться даже опытные Go-разработчики, так как создание инструментов для атаки ПО требует особого склада ума, отличающегося от типичного, характерного для разработки приложений, хода мысли. Компромиссы, используемые вами при проектировании программного обеспечения, скорее всего, будут сильно отличаться, когда к задачам добавятся обход систем безопасности и намерение избежать обнаружения.
Если вы уже работаете в сфере наступательной безопасности, эта книга поможет вам создать утилиты, существенно опережающие по скорости существующие решения. Если же являетесь сотрудником обороняющейся стороны или состоите в команде реагирования на инциденты, то с ее помощью научитесь выполнять парсинг и организовывать защиту от вредоносных программ, написанных на Go.
Успехов в освоении!
Эйч Ди Мур, основатель Metasploit и Critical Research Corporation, вице-президент по исследованиям и разработкам в Atredis Partners
Благодарности
Вы бы не держали сейчас в руках данную книгу, не разработай Роберт Гризмер (Robert Griesmer), Роб Пайк (Rob Pike) и Кен Томпсон (Ken Thompson) столь замечательный язык программирования. Эти люди, а также вся команда разработчиков Go не перестают с каждым релизом выпускать всё новые полезные обновления. Мы бы ни за что не взялись писать о языке, не будь он столь легок и интересен для изучения и использования.
Мы также благодарим команду No Starch Press: Лаурель, Франсис, Билла, Энни, Барбару и всех тех, с кем имели честь взаимодействовать. Все вы направляли нас по непроторенной территории написания нашей первой книги. Несмотря на все жизненные обстоятельства, будь то семейные неурядицы или смена места работы, вы все это время были терпеливы, в то же время продолжая подталкивать нас к завершению начатого. Мы были рады работать с каждым сотрудником всей огромной команды No Starch Press.
Хочу отдельно поблагодарить Джен за поддержку, за то что воодушевляла меня и хранила семейный очаг, пока я пропадал в офисе по вечерам и выходным, работая над этой никак не кончавшейся книгой. Джен, ты помогла мне больше, чем можешь представить, и твои вдохновляющие слова сыграли в этом не последнюю роль. Я искренне признателен жизни за то, что ты у меня есть. Я должен поблагодарить Ти — мою верную собаку за ее присутствие рядом со мной в офисе и за то, что регулярно выдергивала меня из виртуального мира с напоминанием о реальном, в который необходимо возвращаться. В завершение от всего сердца я хочу посвятить эту книгу детишкам Ти, Луне и Энни, которые ушли, пока я писал ее. Вы, девочки, значили и продолжаете значить для меня очень многое, и этот труд будет всегда служить напоминанием о моей любви к вам.
Крис Паттен
Искренне благодарю свою жену и лучшего друга Кэти за ее непрерывную поддержку, ободрение и веру в меня. В моей жизни нет ни дня, когда я не ощущаю благодарности за все, что ты делаешь для меня и нашей семьи. Спасибо вам, Брукс и Сабс, за то, что дали мне повод к столь усердному труду. Для меня нет лучшей работы, чем быть вашим отцом. Также благодарю лучших «офисных сторожевых», о каких только мог мечтать, — Лео (покойся с миром), Арло, Мерфи и даже Хоуи (да, Хоуи тоже). Вы систематически разносили мой дом и периодически ставили под вопрос мой жизненный выбор, но ваше присутствие и товарищество заполняли весь мой мир. Вы все получите по подписанному экземпляру книги, которые сможете вдоволь пожевать.
Дэн Коттманн
Спасибо тебе, любовь моей жизни — Джекки, за заботу и вдохновение. Ничто из того, что я делаю, не было бы возможным без твоей поддержки и всего, что ты делаешь для нашей семьи. Благодарю своих друзей и коллег в Atredis Partners и всех, с кем я делил общее рабочее пространство в прошлом. Я оказался там, где я есть, благодаря вам. Спасибо моим наставникам и друзьям, которые верили в меня с первого дня. Вас слишком много, чтобы перечислять каждого по имени. Я благодарен всем невероятным людям, которых встречал на протяжении жизни. Спасибо тебе, мама, за то, что отвела меня в кружок по программированию. Оглядываясь назад, могу сказать, что это было пустой тратой времени и в основном я играл там в Myst, но именно тогда во мне зажегся интерес ко всему этому (скучаю по 1990-м). Отдельно хочу от всего сердца поблагодарить своего Спасителя, Иисуса Христа.
Том Стил
Это был немалый путь — почти три года. За это время произошло очень многое, но вот наконец мы здесь. Хочу выразить от всех нас искреннюю признательность за отзывы, которые присылали нам друзья, коллеги, семьи и читатели ранней версии книги. Вас, наш уважаемый читатель, мы благодарим за терпение и надеемся, что вы получите такое же наслаждение от прочтения этой книги, какое получали мы при ее написании. Всех вам благ!
Введение
На протяжении почти шести лет мы втроем вели одну из крупнейших в Северной Америке практик по консультированию в сфере пентеста. Будучи старшими консультантами, мы выполняли техническую работу, включая тесты на сетевое проникновение в интересах наших клиентов, а также инициировали разработку улучшенных инструментов, процессов и методологий. И в определенный момент в качестве одного из основных языков разработки начали использовать Go.
Go объединяет лучшие возможности аналогичных языков, достигая баланса между производительностью, безопасностью и удобством применения. Вскоре мы сделали его основным средством разработки инструментов, а в итоге даже стали в какой-то степени продвигать его, подталкивая своих коллег по индустрии к знакомству с ним. Нам казалось, что обеспечиваемые Go преимущества по меньшей мере заслуживают рассмотрения.
В этой книге мы предлагаем вам путешествие по миру возможностей программирования на этом языке с позиции специалистов по безопасности и хакеров. В отличие от других изданий по хакингу, здесь мы будем не просто показывать вам, как автоматизировать сторонние или коммерческие инструменты (хотя об этом все же позже поговорим), а погрузимся в разнообразные практические тематики, связанные с конкретными задачами, протоколами или тактиками, которые пригодятся для противодействия атакам. Мы затронем TCP, HTTP и DNS, станем взаимодействовать с Metasploit и Shodan, изучим поиск по файловым системам и базам данных, портируем эксплойты из других языков в Go, напишем ключевые функции SMB-клиента, атакуем Windows, кросс-компилируем бинарные файлы, поработаем с криптосистемами, используем вызов библиотек C, воздействуем на Windows API и сделаем многое другое. В общем, замысел грандиозен, так что приступим!
Для кого эта книга
Книга предназначена для всех, кто хочет научиться разрабатывать собственные хакерские инструменты с помощью Go. Как профессионалы, и в особенности как консультанты, мы всегда определяли программирование как фундаментальный навык для пентестеров и специалистов по безопасности. Способность писать код, в частности, расширяет ваше понимание принципов работы ПО, а следовательно, и понимание того, как его взломать. Кроме того, если вы уже побывали в роли разработчика, то сможете более целостно оценивать сложности, с которыми он сталкивается в работе с ПО, отвечающим за безопасность. При этом, исходя из личного опыта, вы будете способны давать более эффективные рекомендации как уменьшать негативные последствия, устранять ложные срабатывания и определять скрытые уязвимости. Написание кода зачастую вынуждает вас взаимодействовать со сторонними библиотеками, а также разными наборами приложений и фреймворками. Для многих, включая нас, максимальный успех разработки обусловливается именно практическим опытом и доработкой мелких деталей.
Чтобы получить максимальную пользу от прочтения книги, рекомендуем скопировать официальный репозиторий с кодом: так у вас под рукой будут все рабочие примеры, о которых мы расскажем. Вы найдете примеры в репозитории https://github.com/blackhat-go/bhg/.
Чего в этой книге нет
Эта книга — не руководство по программированию на Go в целом, а инструкция по использованию языка для разработки инструментов безопасности. В первую очередь мы хакеры и только потом программисты. Никто из нас никогда не был инженером ПО. Это значит, что как хакеры мы ставим во главу угла функциональность, а не элегантность. Во многих случаях мы были склонны писать код в хакерской манере, не учитывая некоторые из идиом или лучших методов создания структуры ПО. У консультантов вечно не хватает времени. Разработка более простого кода зачастую быстрее, а значит, это предпочтительнее, чем добиваться его элегантности. Когда вам требуется быстро найти решение, вопрос стиля отходит на второй план.
Это может не понравиться идеалистам Go, которые наверняка будут писать в соцсетях, что мы «некорректно обрабатываем все условия ошибки», «наши примеры недостаточно оптимизированы» или «желаемых результатов можно было добиться с помощью лучших конструкций или методов». В большинстве случаев нашей задачей не было научить вас наилучшим, максимально элегантным или на 100 % идиоматическим решениям, если это не влияло положительно на итоговый результат. Несмотря на то что мы вкратце рассмотрим синтаксис языка, сделаем это лишь для того, чтобы обозначить для вас фундамент, на котором вы сможете строить. В конце концов, это не книга «Обучение элегантному программированию на Go» — это «Black Hat Go».
Почему Go
До появления Go можно было расставить приоритеты по простоте применения, рассматривая такие динамически типизированные языки, как Python, Ruby или PHP. В любом случае требовалось пожертвовать определенной долей производительности и безопасности. В качестве альтернативы были доступны статически типизированные языки, например С или C++, которые обеспечивают высокую производительность и безопасность, но не особо удобны в использовании. А Go лишен большей части устрашающих сторон его прямого предка — C, что существенно облегчает разработку. В то же время это статически типизированный язык, который показывает синтаксические ошибки в процессе компиляции, что дает разработчику уверенность в безопасном выполнении кода. При этом после компиляции он выполняется оптимальнее интерпретируемых языков. В его структуру также заложена возможность многопоточных вычислений, что делает легкодоступным параллельное программирование.
Эти причины использования Go не особо интересуют специалистов по безопасности. Тем не менее многие из возможностей языка особенно полезны для хакеров и специалистов по защите.
• Отчетливая система управления пакетами. Решение по управлению пакетами реализовано здесь очень элегантно и интегрировано прямо с инструментами Go. Используя исполняемый файл go, вы можете с легкостью скачивать, компилировать и устанавливать пакеты и зависимости, что делает процесс привлечения сторонних библиотек простым и, как правило, бесконфликтным.
• Кросс-компиляция. Одна из наилучших возможностей Go — его способность кросс-компилировать исполняемые файлы. До тех пор пока ваш код не взаимодействует с чистым C, можно легко писать его в Linux или Mac, а компилировать в Windows-совместимом формате Portable Executable.
• Богатая стандартная библиотека. Время, проведенное за разработкой на других языках, позволило нам оценить обширность собственной библиотеки Go. Многим современным языкам недостает стандартных библиотек, требующихся для выполнения таких привычных задач, как шифрование, сетевые коммуникации, подключение к базам данных и кодирование данных (JSON, XML, Base64, hex). Go содержит многие из этих жизненно важных функций и библиотек в собственной стандартной библиотеке, что сокращает количество действий, необходимых для правильной настройки среды разработки или вызова функций.
• Многопоточность. В отличие от более зрелых языков, Go вышел почти одновременно с массовым появлением первых многоядерных процессоров. Поэтому в нем шаблоны многопоточности и оптимизации производительности настроены как раз под эту модель обработки.
Чем может не понравиться Go
Мы понимаем, что Go не является идеальным решением для каждой задачи. Вот некоторые из его недостатков.
• Размер двоичного файла. При компиляции этого файла в Go он чаще всего имеет размер в несколько мегабайт. Вы, конечно, можете обрезать символы отладки и уменьшить объем с помощью упаковщика, но эти приемы требуют особой внимательности, так как могут сыграть и в обратную. Специалистам по безопасности, которым требуется прикреплять двоичный файл к электронным письмам, размещать его на файлообменниках или передавать по сети, нужно быть очень внимательными.
• Громоздкость. Несмотря на то что синтаксис Go более компактен, чем C#, Java или даже C/C++, вы все равно столкнетесь с тем, что простая конструкция языка требует излишней выразительности в отношении таких компонентов, как списки (называемые срезами), процессинг, циклы и обработка ошибок. Однострочная инструкция из Python здесь легко может стать трехстрочной.
Краткий обзор
В главе 1 происходит базовое знакомство с синтаксисом Go и его философией. Затем мы переходим к изучению примеров, которые вы можете использовать для разработки инструментов, включая такие сетевые протоколы, как HTTP, DBS и SMB. После этого идет углубление в тактики и задачи, с которыми мы встречались как пентестеры. Здесь вы познакомитесь с такими темами, как кража данных, парсинг пакетов и разработка эксплойтов. В завершение мы оглянемся назад и вкратце поговорим о том, как создавать динамические встраиваемые инструменты, после чего перейдем к шифрованию, атаке Microsoft Windows и реализации стеганографии.
Во многих случаях вы сможете расширить приводимые нами инструменты, чтобы они соответствовали вашим конкретным задачам. Несмотря на то что мы везде приводим надежные примеры, в реальности мы хотим обеспечить вас знаниями и основами, с помощью которых вы расширите или переработаете эти примеры для достижения собственных целей. Говоря образно, мы не просто даем вам рыбу, но хотим, чтобы вы научились рыбачить.
Прежде чем читать далее, обратите внимание на то, что мы — авторы и издатель — создали этот контент только для законного использования. Мы не несем никакой ответственности за злонамеренные и незаконные действия, которые вы можете с его помощью совершить. Все содержимое служит исключительно образовательным целям. Не проводите тестирование на проникновение в отношении систем или приложений, не получив на это авторизованного разрешения.
Далее приведено краткое описание содержания каждой главы.
Глава 1. Go. Основы
Задача этой главы — познакомить вас с основами Go и обеспечить фундамент, необходимый для понимания концепций, рассматриваемых на протяжении всей книги. Сюда входит сокращенный обзор базового синтаксиса и идиом Go. В этой главе мы поговорим о его экосистеме, включая поддерживаемые инструменты, IDE, управление зависимостями и др. Читатели, совсем не знакомые с этим языком, узнают здесь самое необходимое, что, как мы рассчитываем, позволит им понимать, реализовывать и расширять примеры из последующих глав.
Глава 2. TCP, сканеры и прокси
Глава знакомит читателя с базовыми понятиями Go, примитивами и шаблонами многопоточности, вводом/выводом (I/O), а также использованием интерфейсов в TCP-приложениях. Сначала мы научим вас создавать простой сканер TCP-портов, сканирующий список портов на основе параметров командной строки. Это подчеркнет простоту кода Go в сравнении с созданным на других языках и сформирует у вас понимание его базовых типов, пользовательского ввода, а также обработки ошибок. Далее мы покажем, как повысить эффективность и скорость созданного сканера путем добавления параллельных функций. После этого мы познакомимся с I/O. Для этого создадим TCP-прокси, выполняющий функцию переадресации портов, начав с простых примеров и постепенно дорабатывая код для повышения надежности нашего решения. В заключение воссоздадим Netcat-функцию «зияющая дыра в безопасности» на Go, научив вас выполнять команды операционной системы, манипулируя stdin и stdout и перенаправляя их по TCP.
Глава 3. HTTP-клиенты и инструменты удаленного доступа
HTTP-клиенты являются важнейшим компонентом при взаимодействии с современными архитектурами веб-серверов. В этой главе вы увидите, как создавать HTTP-клиенты, необходимые для выполнения множества стандартных сетевых взаимодействий. В ней вы займетесь обработкой различных форматов для коммуникации с Shodan и Metasploit. Помимо этого мы покажем, как работать с поисковыми движками, используя их для сбора и парсинга метаданных с целью извлечения полезной для организации и профайлинга информации.
Глава 4. HTTP-серверы, маршрутизация и промежуточное ПО
Эта глава представит принципы и соглашения, необходимые для создания HTTP-сервера. Здесь мы обсудим стандартную маршрутизацию, промежуточное ПО и шаблонные методы, применив эти знания для создания сборщика учетных данных и кейлогера. В завершение покажем, как мультиплексировать соединения управления и контроля (command-and-control, C2), создав обратный HTTP-прокси.
Глава 5. Эксплуатация DNS
Эта глава знакомит вас с основными принципами DNS с помощью Go. В ней мы сначала выполним клиентские операции, включая просмотр записей конкретного домена, а затем продемонстрируем написание собственного DNS-сервера и DNS-прокси, которые пригодятся для операций C2.
Глава 6. Взаимодействие с SMB и NTLM
Тут мы изучим протоколы SMB и NTLM, взяв их в качестве основы для обсуждения реализаций протоколов в Go. На основе частичной реализации SMB мы рассмотрим маршалинг и демаршалинг данных, использование тегов настраиваемых полей и др. Мы расскажем и покажем, как задействовать эту реализацию для извлечения подписи SMB, а также выполнения атак по подбору пароля.
Глава 7. Взлом баз данных и файловых систем
Кража данных является важнейшим аспектом тестирования на проникновение. Данные находятся во множестве ресурсов, включая БД и файловые системы. Эта глава представляет основные способы подключения к базам данных и взаимодействия с ними на ряде распространенных SQL- и NoSQL-платформ. В ней вы узнаете основы подключения к SQL-базам данных и выполнения запросов. Мы покажем вам, как выполнять поиск чувствительной информации по БД и таблицам, стандартную технику, используемую после внедрения эксплойта. Помимо этого вы узнаете, как обходить файловые системы и просматривать файлы на предмет чувствительной информации.
Глава 8. Обработка сырых пакетов
В этой главе мы покажем, как парсить и обрабатывать сетевые пакеты с помощью библиотеки gopacketn, использующей libpcap. Вы научитесь идентифицировать доступные сетевые устройства, задействовать пакетные фильтры и обрабатывать эти пакеты. После этого мы разработаем сканер портов, способный проверять надежность механизмов защиты посредством различных типов сканирования, включая SYN-флуд и SYN-куки, которые приводят к чрезмерному количеству ложных срабатываний при сканировании портов.
Глава 9. Написание и портирование эксплойтов
Эта глава почти полностью посвящена разработке эксплойтов. Она начинается с создания фаззера для обнаружения различных типов уязвимостей. Вторая половина главы рассказывает, как портировать имеющиеся эксплойты из других языков в Go. Сюда входит перенос эксплойта десериализации Java и эксплойта повышения привилегий Dirty COW. В завершение мы говорим о создании и преобразовании шелл-кода для применения в ваших программах Go.
Глава 10. Плагины и расширяемые инструменты Go
Здесь мы представим вам два отдельных метода создания расширяемых инструментов. Первый, появившийся в Go 1.8, использует внутренний механизм плагинов. Мы рассмотрим случаи применения этого подхода и обсудим второй метод, задействующий для создания расширяемых инструментов язык Lua. Мы также приведем практические примеры, показав, как с помощью любого из этих подходов выполнять стандартную задачу безопасности.
Глава 11. Реализация криптографии и криптографические атаки
Глава раскрывает базовые понятия симметричной и асимметричной криптографии с использованием Go. Ее материал посвящен пониманию и применению криптографии на примере стандартного пакета Go. Go является одним из немногих языков, который, вместо того чтобы применять для шифрования стороннюю библиотеку, прибегает к собственной реализации. Это упрощает навигацию по коду, его изменение и понимание.
Мы изучим стандартную библиотеку, рассмотрев общие случаи ее использования и создание инструментов. Эта глава покажет, как выполнять хеширование, аутентификацию сообщений и шифрование. В конце вы узнаете, как дешифровывать RC2-криптограммы методом грубой силы (brute-force).
Глава 12. Взаимодействие с системой Windows и ее анализ
В ходе рассмотрения атак Windows мы познакомим вас с методами взаимодействия с внутренним Windows API, изучим пакет syscall для внедрения процессов и узнаем, как создавать двоичный парсер Portable Executable (PE). Завершится эта глава обсуждением вызова собственных библиотек С через механизмы межъязыковой совместимости Go.
Глава 13. Сокрытие данных с помощью стеганографии
Стеганография — это метод сокрытия сообщения или файла внутри другого файла. Эта глава знакомит вас с одним из видов стеганографии: сокрытием произвольных данных внутри содержимого PNG-файла. Рассматриваемые в ней техники могут пригодиться для извлечения информации, создания замаскированных сообщений C2 и обхода детективных или превентивных средств контроля.
Глава 14. Создание C2-трояна удаленного доступа
Последняя глава посвящена практическим реализациям имплантатов и серверов управления и контроля (C2) в Go. В ней мы задействуем все приобретенные в процессе чтения книги знания и опыт для построения канала C2. Реализация «клиент — сервер» C2 благодаря своей настраиваемой природе будет избегать контроля безопасности на основе сигнатур и пытаться обмануть эвристику, а также сетевые средства контроля выхода.
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.
1. Go. Основы
Эта глава познакомит вас с процессом настройки среды разработки Go и синтаксисом. Об основах механики этого языка написаны целые книги, здесь же мы рассмотрим основные принципы и понятия, которые вам понадобятся для работы с приводимыми примерами кода. Мы затронем все, начиная от примитивных типов данных и заканчивая реализацией многопоточности. Для тех же, кто уже хорошо знаком с Go, большая часть этой главы окажется просто обзором.
Настройка среды
Для начала работы с Go вам потребуется функциональная среда разработки. В данном разделе мы проведем вас по всем необходимым этапам, включающим скачивание, а также настройку рабочего пространства и переменных среды для языка Go. Здесь будут рассмотрены различные варианты для интегрированной среды и некоторые из стандартных инструментов, поставляемых с Go.
Скачивание и установка Go
Начните со скачивания соответствующего вашей операционной системе и архитектуре установочного файла Go с официального сайта (https://golang.org/dl/). Здесь вы найдете файлы для Windows, Linux и macOS. Если вы используете систему, для которой нет установочного файла, можете скачать по той же ссылке исходный код Go.
Запустите скачанный установщик и следуйте инструкциям, по ходу которых вы установите весь набор ключевых пакетов Go. Пакеты, в большинстве языков называемые библиотеками, содержат полезный код, который вы сможете задействовать в своих программах.
Настройка GOROOT для определения расположения двоичного файла
Далее нужно сообщить операционной системе, где находится установленная программа. В большинстве случаев, если вы установили Go в путь по умолчанию, например в /usr/local/go на системах *Nix/BSD, то ничего дополнительно делать не потребуется. Если же решили поместить Go в иное место или устанавливаете его в Windows, то нужно сообщить ОС путь к файлу для его запуска.
Это можно сделать из командной строки, указав его в зарезервированной переменной среды GOROOT. Настройка переменных сред зависит от операционной системы. В Linux или macOS вы можете добавить ~/.profile в следующее:
set GOROOT=/path/to/go
В Windows эту переменную среду можно добавить через панель управления, найдя в ней раздел Переменные среды.
Настройка GOPATH для определения местоположения рабочего пространства
В отличие от GOROOT, которая необходима только в конкретных сценариях установки, переменную среды GOPATH необходимо определять постоянно, сообщая таким образом набору инструментов Go, где будут находиться исходный код, сторонние библиотеки и скомпилированные программы. Для этого можно использовать любое место. Как только вы создадите этот основной каталог рабочего пространства, создайте в нем три подкаталога: bin, pkg и src (подробнее о них мы напишем чуть позже). Затем нужно настроить саму переменную GOPATH. Например, если вы хотите поместить проекты в каталог gocode, расположенный в домашней папке Linux, то устанавливаете в GOPATH следующее значение:
GOPATH=$HOME/gocode
Каталог bin будет содержать скомпилированные и установленные исполняемые файлы Go, помещаемые в него автоматически при сборке и установке. Каталог pkg служит для хранения объектов пакетов, включая сторонние зависимости Go, необходимые для вашего кода. К примеру, таким образом вы можете применять код другого разработчика, более изящно обрабатывающий HTTP-маршрутизацию. В pkg будут содержаться исполняемые артефакты, необходимые для использования их реализации в вашем коде. И наконец, каталог src будет служить хранилищем для всего вредоносного исходного кода, который вы создадите.
Расположение вашего рабочего пространства может быть произвольным, но его внутренние каталоги должны соответствовать указанным именам и структуре. Команды компиляции, сборки и управления пакетами, о которых вы узнаете в этой главе чуть позже, все полагаются на эту стандартную структуру каталогов. Без этого важного этапа настройки проекты Go не будут компилироваться и не смогут обнаружить необходимые зависимости.
Настроив переменные GOROOT и GOPATH, нужно подтвердить завершение данного процесса. В Linux и Windows для этого можно использовать команду set. Дополнительно убедитесь, что ваша система видит установленный исполняемый файл и вы задействуете требуемую версию Go, введя команду goversion:
$ go version
go version go1.11.5 linux/amd64
В ответ должна вернуться версия установленного двоичного файла.
Выбор интегрированной среды разработки
Далее вы, скорее всего, решите выбрать интегрированную среду разработки (IDE), в которой и будете писать код. Несмотря на то что этот инструмент не является необходимым, многие его возможности помогают снизить количество ошибок, добавить горячие клавиши для доступа к системе контроля версий, облегчают управление пакетами и многое другое. Поскольку Go — довольно молодой язык, спектр предлагаемых для него IDE может быть ограничен.
К счастью, за последние несколько лет появилось несколько полноценных вариантов, часть из которых мы рассмотрим в текущей главе. Более подробный список IDE или редакторов можно найти на вики-странице Go (https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins/). Наша книга подразумевает полную свободу выбора IDE/редактора, и мы не призываем вас к использованию их конкретных вариантов.
Редактор Vim
Текстовый редактор Vim, доступный во многих дистрибутивах операционных систем, предоставляет гибкую, расширяемую и полностью открытую среду разработки. Одна из наиболее соблазнительных функций Vim заключается в том, что он позволяет пользователям выполнять все из терминала без посредничества замысловатых GUI.
Этот редактор содержит обширную экосистему плагинов, с помощью которой вы можете настраивать темы, добавлять инструменты контроля версий, определять сниппеты, макеты и навигацию по коду, включать автоподстановку, задействовать выделение синтаксиса и линтинг, а также многое другое. К наиболее распространенным системам управления плагинами Vim относятся Vundle и Panthogen.
Чтобы работать через этот редактор с Go, установите плагин vim-go (https://github.com/fatih/vim-go/), показанный на рис. 1.1.
Рис. 1.1. Плагин vim-go
Естественно, чтобы эффективно вести через него разработку на Go, сначала потребуется освоить сам редактор. Следующий же этап настройки этой среды со всеми необходимыми вам функциями может оказаться несколько пугающим. Используя бесплатный Vim, приходится жертвовать рядом удобств, предлагаемых коммерческими IDE.
GitHub Atom
IDE GitHub, называемая Atom (https://atom.io/), представляет собой редактор с богатым набором поддерживаемых сообществом пакетов. В отличие от Vim, он предоставляет отдельное IDE в виде приложения, а не работающее через терминал решение.
Как и Vim, Atom бесплатен. Он по умолчанию предлагает тайлинг, управление пакетами, контроль версий, отладку, автоподстановку и множество дополнительных возможностей прямо из коробки. Поддержка Go в нем реализуется через плагин go-plus (https://atom.io/packages/go-plus/).
Рис. 1.2. Atom с поддержкой Go
Microsoft Visual Studio Code
Visual Studio Code (VS Code) от Microsoft является, вероятно, одной из наиболее богатых функционалом и легких в настройке IDE. Она абсолютно бесплатна и распространяется под лицензией MIT.
Рис. 1.3. IDE VS Code с поддержкой Go
Эта IDE поддерживает разнообразные расширения для тем, управления версиями, автодополнения кода, отладки, линтинга и форматирования. Интеграцию с Go в этом случае можно реализовать с помощью расширения vscode-go (https://github.com/Microsoft/vscode-go/).
JetBrains GoLand
Коллекция инструментов разработки от JetBrains очень эффективна и богата возможностями, что упрощает реализацию как любительских, так и профессиональных проектов. На рис. 1.4 показано, как выглядит IDE JetBrains GoLand.
Рис. 1.4. Коммерческая IDE GoLand
GoLand — коммерческая IDE, посвященная языку Go. Стоимость этого инструмента варьируется от бесплатной для студентов до 89 долларов в год для частных клиентов и 199 долларов в год для организаций. GoLand предлагает все возможности многофункциональной IDE, включая отладку, автодополнение кода, контроль версий, линтинг, форматирование и др. Несмотря на то что платность может немного отпугивать, коммерческие продукты, подобные этому, обычно предлагают официальную поддержку, документацию, своевременное исправление ошибок и ряд других гарантий, сопровождающих корпоративное ПО.
Использование стандартных команд Go tool
В Go есть ряд полезных команд, упрощающих процесс разработки. Они обычно имеются также в IDE, позволяя согласованно работать с инструментами в различных средах. Давайте разберем некоторые из них.
Команда go run
Это одна из наиболее часто применяемых в процессе разработки команд, которая компилирует и выполняет основной пакет — точку входа в программу.
В качестве примера сохраните следующий код в каталоге проекта $GOPATH/src (вы создали его при установке) как main.go:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, Black Hat Gophers!")
}
Теперь в командной строке перейдите к каталогу с этим файлом и выполните gorunmain.go. В результате должно отобразиться сообщение Hello,BlackHatGophers!.
Команда go build
Обратите внимание, что gorun выполняет файл, но не генерирует самостоятельный двоичный файл. Для этого как раз и используется команда gobuild. Она компилирует приложение, включая все пакеты и их зависимости, не инсталлируя полученный результат. То есть она создает на диске двоичный файл, но программу не запускает. Создаваемые ею файлы соответствуют общему соглашению именования, но при этом вы можете самостоятельно изменить имя итогового файла с помощью команды -ooutput.
Переименуйте main.go из предыдущего примера в hello.go. Затем в окне терминала выполните gobuildhello.go. Если все будет сделано, как задумано, на выходе вы получите исполняемый файл с именем hello. Теперь введите команду
$ ./hello
Hello, Black Hat Gophers!
Она запустит полученный двоичный файл.
По умолчанию этот файл содержит отладочную информацию и таблицу символов, что увеличивает его размер. Чтобы избежать излишнего раздувания, нужно добавить в процессе сборки дополнительные флаги, которые предотвратят включение этой информации. Например, следующая команда сократит размер файла примерно на 30 %:
$ go build -ldflags "-w -s"
Более компактный размер упростит дальнейшую передачу или вложение файла при реализации ваших злодейских замыслов.
Кросс-компиляция
Использование gobuild отлично работает для выполнения двоичного файла в текущей системе или в имеющей идентичную архитектуру, но что если вы хотите создать файл, способный работать в системе с другой архитектурой? Для этого и служит кросс-компиляция, являясь одной из крутейших возможностей Go, так как ни один прочий язык не реализует ее с той же легкостью. Команда build позволяет кросс-компилировать программу для нескольких операционных систем и архитектур. Обратитесь к официальной документации (https://golang.org/doc/install/source#environment/) Go за подробным описанием возможных сочетаний компиляций совместимых ОС и архитектур.
Для выполнения кросс-компиляции вам понадобится установить ограничение. Это подразумевает просто передачу в команду build информации об операционной системе и архитектуре, для которых вы собираетесь компилировать код. Эти ограничения описываются атрибутами GOOS (для операционных систем) и GOARCH (для архитектур).
Ограничения сборки можно устанавливать тремя способами: через командную строку, комментарии в коде или с помощью расширений файлов. Здесь мы рассмотрим способ с использованием командной строки, предоставив вам возможность изучить остальные самостоятельно.
Предположим, вам нужно кросс-компилировать ранее созданную программу hello.go, расположенную на MacOS, для выполнения на архитектуре Linux x64. Это можно реализовать через командную строку, сопроводив выполнение команды build соответствующими атрибутами GOOS и GOARCH:
$ GOOS="linux" GOARCH="amd64" go build hello.go
$ ls
hello hello.go
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
Вывод подтверждает, что получающийся двоичный файл — это 64-битный ELF (Linux).
Процесс кросс-компиляции намного проще в Go, чем в любом другом современном языке программирования. Единственная реальная сложность возникает, когда вы пытаетесь кросс-компилировать приложения, использующие внутренние привязки языка C. Мы же будем держаться подальше от подобных «сорняков» и позволим вам решить эту проблему самостоятельно. В зависимости от импортируемых вами пакетов и разрабатываемых проектов они могут беспокоить вас не слишком часто.
Команда go doc
Эта команда позволяет связываться с документацией пакета, функции, метода или переменной. Документация вкладывается в код в виде комментариев. Посмотрим, как можно узнать подробности о функции fmt.Println():
$ go doc fmt.Println
func Println(a ...interface{}) (n int, err error)
Println formats using the default formats for its operands and writes to
standard output. Spaces are always added between operands and a newline
is appended. It returns the number of bytes written and any write error
encountered.
Вывод информации берется непосредственно из комментариев исходного кода. При условии адекватного комментирования пакетов, функций, методов и переменных вы сможете без проблем автоматически просматривать эту документацию с помощью команды godoc.
Команда go get
Многие из программ Go, которые вы будете разрабатывать в процессе чтения книги, потребуют сторонних пакетов. Команда goget служит для получения исходного кода нужных пакетов. Предположим, вы написали следующий код, импортирующий пакет stacktitan/ldapauth:
package main
import (
"fmt"
"net/http"
❶ "github.com/stacktitan/ldapauth"
)
Несмотря на то что вы импортировали пакет stacktitan/idapauth❶, обратиться к нему пока не получится — сначала нужно выполнить команду gogetgithub.com/stacktitan/ldapauth, которая загрузит фактический пакет и поместит его в каталог $GOPATH/src.
Следующее дерево каталогов отражает размещение пакета Idapauth в рабочем пространстве GOPATH:
$ tree src/github.com/stacktitan/
❶ src/github.com/stacktitan/
└── ldapauth
├── LICENSE
├── README.md
└── ldap_auth.go
Обратите внимание на то, что путь ❶ и имя импортированного пакета создаются так, чтобы избежать присваивания одного имени нескольким пакетам. Использование вводной части github.com/stacktitan перед Idapauth гарантирует, что его имя останется уникальным.
Хотя Go-разработчики традиционно устанавливают зависимости с помощью goget, при этом могут возникнуть проблемы, если эти зависимые пакеты получают обновления, нарушающие обратную совместимость. В связи с этим в Go были введены два отдельных инструмента — dep и mod, которые фиксируют зависимости, предотвращая возникновение подобных проблем. Тем не менее в книге для получения зависимостей практически повсеместно используется команда goget. Это поможет избежать несогласованности с текущими инструментами управления зависимостями и облегчит для вас реализацию приводимых примеров.
Команда go fmt
Эта команда автоматически форматирует исходный код. К примеру, выполнение gofmt/path/to/your/package стилизует код, обеспечив использование правильных разрывов строк, отступов и выравнивание скобок.
Применение произвольных настроек стилизации поначалу может показаться странным, особенно если они отличаются от привычных вам. Как бы то ни было, со временем такая согласованность покажется вполне уместной, поскольку код станет похож на другие сторонние пакеты и выглядеть будет более строгим. В большинстве IDE присутствуют хуки, которые автоматически выполняют gofmt при сохранении файла, так что вам не придется задействовать эту команду явно.
Команды golint и go vet
В то время как gofmt изменяет стилизацию синтаксиса кода, golint сообщает об ошибках стиля, таких как пропущенные комментарии, не соответствующее соглашению именование переменных, бесполезные определения типов и др. Обратите внимание на то, что golint — это самостоятельный инструмент, а не подкоманда основного исполняемого файла go. Поэтому установить ее потребуется отдельно с помощью goget-ugolang.org/x/lint/golint.
Аналогичным образом govet проверяет код и на основе эвристики выявляет подозрительные конструкции, такие как вызов Printf() с некорректным форматом строковых типов. Команда govet старается обнаружить проблемы, часть из которых могут оказаться незаметными для компилятора и быть работоспособными багами.
Go Playground
Go Playground (песочница) — это среда выполнения, размещенная по адресу https://play.golang.org/, где разработчики могут онлайн разрабатывать и тестировать код и делиться его фрагментами с другими. Данный сайт упрощает знакомство с различными возможностями языка, не требуя установки или запуска Go на локальной системе. Это отличный способ тестирования фрагментов кода до их интеграции в проекты.
Здесь вы можете также просто поиграть с различными нюансами языка в заранее сконфигурированной среде. Стоит отметить, что песочница Go не позволяет вызывать определенные опасные функции, чтобы не допустить, например, выполнения команд операционной системы или взаимодействия со сторонними сайтами.
Другие команды и инструменты
Мы не будем прямо рассматривать другие инструменты и команды, и вам стоит изучить их самостоятельно. По мере создания все более сложных проектов вы наверняка столкнетесь с необходимостью использования, например, команды gotest для выполнения тестов и бенчмарков, cover для проверки области охвата теста, imports для исправления инструкций импорта и др.
Синтаксис Go
Исчерпывающий обзор всего языка Go занял бы несколько глав, если не всю книгу. Этот раздел дает краткое описание его синтаксиса, в частности, относящегося к типам данных, структурам управления и стандартным паттернам. Для новичков он послужит введением, а для бывалых Go-программистов — напоминанием.
Для более глубокого поэтапного разбора языка рекомендуем пройти замечательный обучающий курс A tour of Go (https://tour.golang.org/). Он представляет собой всестороннее практическое знакомство с языком, разбитое на мини-уроки и использующее встроенную песочницу, которая позволяет тут же опробовать каждое из разъясняемых понятий.
Сам по себе язык представляет намного более чистую версию C, лишенную множества низкоуровневых нюансов, что повышает читаемость и упрощает его освоение.
Типы данных
Как и большинство современных языков, Go предоставляет разнообразные примитивные и сложные типы данных. Примитивные типы состоят из базовых составляющих элементов (строк, чисел и логических значений), которые встречаются повсеместно в других языках и создают основу всей задействуемой в программе информации. Сложные типы являются определяемыми пользователем структурами, включающими в себя комбинацию одного или нескольких примитивных или сложных типов.
Примитивные типы данных
К ним относятся bool, string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, byte, rune, float32, float64, complex64 и complex128.
Обычно при определении переменной вы объявляете ее тип. Если этого не сделать, система назначит тип автоматически. Рассмотрите следующие примеры:
var x = "Hello World"
z := int(42)
В первом для определения переменной x мы используем ключевое слово var и присваиваем ей значение "HelloWorld". Go неявно назначает x как string, так что объявлять этот тип не требуется. Во втором примере для определения переменной z мы задействуем оператор :=, присваивая ей целочисленное значение 42. Между этими двумя операторами реальной разницы нет, и на протяжении книги мы будем использовать оба. Но некоторые считают, что := в силу своей невзрачности ухудшает читаемость кода. Вы же вольны выбирать любой вариант.
В предыдущем примере мы явно обернули значение 42 в вызов int, обусловив его тип. Можно опустить вызов int, но тогда придется принять тот тип, который система использует для данного значения автоматически. В некоторых случаях им может оказаться вовсе не тот, который нам нужен. Предположим, вам требуется, чтобы 42 представляло беззнаковое целое число, а не тип int, в этом случае его придется явно обернуть соответствующим образом.
Срезы и карты
В Go есть и более сложные типы, такие как срезы и карты. Срезы подобны массивам, размер которых вы можете изменять динамически, что позволяет более эффективно передавать их функциям. Карты — это ассоциативные массивы, неупорядоченные списки пар «ключ/значение», позволяющие эффективно и быстро находить значения для уникального ключа.
Определение, инициализация и работа со срезами и картами реализуются разными способами. Следующий пример показывает стандартный способ определения среза s и карты m совместно с их элементами:
var s = make([]string, 0)
var m = make(map[string]string)
s = append(s, "some string")
m["some key"] = "some value"
В этом коде используются две встроенные функции: make() для инициализации каждой переменной и append() для добавления новых элементов в срез. Последняя строка вносит в карту m пару «ключ/значение», представленную somekey и somevalue. Мы советуем вам изучить все методы определения и использования этих типов данных в документации Go.
Указатели, структуры и интерфейсы
Указатель указывает на конкретную область памяти, позволяя извлекать хранящиеся там значения. Как и в C, в данном случае оператор & служит для извлечения адреса переменной, а оператор * — для разыменования этого адреса, то есть получения по нему значения. Вот пример:
❶ var count = int(42)
❷ ptr := &count
❸ fmt.Println(*ptr)
❹ *ptr = 100
❺ fmt.Println(count)
Этот код определяет целое число count❶, после чего с помощью оператора & создает указатель ❷, возвращая адрес переменной count. Далее в процессе вызова fmt.Println() происходит разыменовывание этой переменной ❸ для вывода значения count в stdout. Затем с помощью оператора *❹ области памяти, на которую указывает ptr, присваивается новое значение. Поскольку это адрес переменной count, то присваивание изменяет значение данной переменной, что подтверждается выводом этого значения на экран ❺.
Тип struct (структура) используется для создания новых типов данных путем определения связанных полей и методов этого типа. Например, здесь мы объявляем тип Person:
❶ type Person struct {
❷ Name string
❸ Age int
}
❹ func (p *Person) SayHello() {
fmt.Println("Hello,", p.Name ❺)
}
func main() {
var guy = new ❻ (Person)
❼ guy.Name = "Dave"
❽ guy.SayHello()
}
Этот код с помощью ключевого слова type❶ определяет новую структуру, содержащую два поля: string с именем Name❷ и int с именем Age❸.
Далее в типе Person, присвоенном переменной p❹, мы определяем метод SayHello(). Он выводит приветственное сообщение в stdout, обращаясь к структуре p❺, которая получает этот вызов. Рассматривайте p как ссылку на self или this в других языках. Здесь мы также определяем функцию main(), выступающую в роли точки входа в программу. Данная функция с помощью ключевого слова new❻ инициализирует нового Person (человека), присваивая ему имя Dave❼ и давая указание выполнить SayHello()❽.
В структурах отсутствуют модификаторы области, такие как закрытая (private), публичная (public) или защищенная (protected), которые обычно присутствуют в других языках и служат для управления доступом к их членам. Вместо этого в Go область доступности определяется величиной регистра: типы и поля, начинающиеся с прописной буквы, экспортируются и являются доступными вне пакета, в то время как начинающиеся со строчной — закрытые и доступны только внутри пакета.
Тип интерфейс в Go можно понимать как схему или контракт. Эта схема определяет ожидаемый набор действий, которые любая конкретная реализация должна выполнить, чтобы считаться типом этого интерфейса. Для создания интерфейса нужно определить набор методов: любой тип данных, содержащий эти методы с верными сигнатурами, выполняет контракт и считается типом этого интерфейса. Рассмотрим пример:
❶ type Friend interface {
❷ SayHello()
}
В этом примере мы определили интерфейс Friend❶, который требует реализации одного метода — SayHello()❷. Это означает, что любой тип, реализующий метод Sayhello(), будет считаться Friend. Обратите внимание на то, что интерфейс Friend фактически не реализует эту функцию — он просто говорит, что если вы Friend, то должны уметь SayHello().
Следующая функция, Greet(), получает интерфейс Friend в качестве ввода и выполняет приветствие в соответствующей Friend форме:
func Greet ❶ (f Friend ❷) {
f.SayHello()
}
Этой функции можно передать любой тип Friend. К счастью, использованный нами ранее тип Person умеет SayHello(), то есть является Friend. Следовательно, если функция Greet()❶, как показано в предыдущем коде, ожидает в качестве вводного параметра Friend❷, можно передать ей Person:
func main() {
var guy = new(Person)
guy.Name = "Dave"
Greet(guy)
}
Интерфейсы и структуры позволяют вам определить несколько типов, которые затем можно передавать функции Greet(), при условии что они реализуют интерфейс Friend. Рассмотрим несколько измененный пример:
❶ type Dog struct {}
func (d *Dog) SayHello() ❷
{ fmt.Println("Woof woof")
}
func main() {
var guy = new(Person)
guy.Name = "Dave"
❸ Greet(guy)
var dog = new(Dog)
❹ Greet(dog)
}
Здесь мы видим новый тип, Dog❶, который тоже умеет SayHello()❷ и, следовательно, является Friend. Теперь вы можете Greet() как Person❸, так и Dog❹, поскольку оба умеют SayHello().
На протяжении книги мы будем касаться темы интерфейсов еще несколько раз, чтобы помочь вам лучше усвоить специфику их применения.
Управляющие конструкции
Go содержит меньше управляющих конструкций, чем другие современные языки. Но это не мешает вам реализовывать сложную обработку, включая условия и циклы.
Главной условной конструкцией в этом языке выступает if/else:
if x == 1 {
fmt.Println("X is equal to 1")
} else {
fmt.Println("X is not equal to 1")
}
В Go для нее используется своеобразный синтаксис. Например, в нем вы не заключаете проверку условия — в этом случае x==1 — в скобки. Здесь необходимо заключать все блоки кода, даже приведенные ранее однострочные, в фигурные скобки. В отличие от Go, во многих других языках однострочные блоки не требуют фигурных скобок.
Что касается условных выражений, включающих более двух вариантов, в Go предусмотрена инструкция switch. Вот пример:
switch x ❶ {
case "foo" ❷:
fmt.Println("Found foo")
case "bar" ❸:
fmt.Println("Found bar")
default ❹:
fmt.Println("Default case")
}
Здесь инструкция switch сравнивает содержимое переменной x❶ с различными значениями — foo❷ и bar❸, — выводя в stdout сообщение о соответствии x одному из условий. Этот пример включает кейс default❹, который выполняется, если ни одно из условий не проходит проверку.
Заметьте, что здесь кейсы не требуют инструкции break, в отличие от других языков, где выполнение кода условия продолжается до момента достижения инструкции break или конца всего выражения switch. В Go же выполняется не более одного совпадающего или предустановленного (default) кейса.
В этом языке есть также особая вариация switch, называемая type switch, которая выполняет утверждение типов с помощью инструкции switch. Переключатели типов помогают понять внутренний тип интерфейса.
Например, для извлечения внутреннего типа интерфейса i вы можете использовать такой переключатель:
func foo(i ❶ interface{}) {
switch v := i.(type)❷ {
case int:
fmt.Println("I'm an integer!")
case string:
fmt.Println("I'm a string!")
default:
fmt.Println("Unknown type!")
}
}
Здесь представлен специальный синтаксис, i.(type)❷, позволяющий извлечь тип переменной интерфейса i❶. Это значение используется в инструкции switch, где каждый кейс соответствует определенному типу. В нашем случае кейсы выполняют проверку на соответствие примитивам int или string, но вы также можете проверять, например, указатели или пользовательские типы структур.
Последней конструкцией управления потоком кода в Go является цикл for. Здесь он выступает единственным средством выполнения итерации или повторения разделов кода. Может показаться странным отсутствие таких конструкций, как циклы do или while, но их можно воссоздать с помощью вариаций синтаксиса цикла for. Вот одна из таких вариаций:
for i := 0; i < 10; i++ {
fmt.Println(i)
}
Здесь происходит перебор чисел от 0 до 9 и вывод каждого в stdout. Обратите внимание на точку с запятой в первой строке. В отличие от многих языков, где этот знак используется в качестве разделителя строк, в Go он применяется в различных управляющих конструкциях для выполнения нескольких разных, но связанных подзадач в одной строке кода. Первая строка с помощью этого знака разделяет логику инициализации (i:=0), условное выражение (i<10) и оператор увеличения (i++). Эта конструкция должна быть знакома всем, кто писал код на любом современном языке, поскольку очень похожа на синтаксис этих языков.
Приведенный далее пример показывает слегка измененный цикл for, перебирающий коллекцию, такую как срез или карта:
❶ nums := []int{2,4,6,8}
for idx ❷, val ❸ := range ❹
nums { fmt.Println(idx, val)
}
Здесь мы инициализируем срез целых чисел nums❶. Затем используем в цикле for ключевое слово range❹ для перебора этого среза. range возвращает два значения: текущий индекс ❷ и копию текущего значения ❸ по этому индексу. Если вы не собираетесь применять этот индекс, можете в цикле for заменить idx на нижнее подчеркивание, сообщив тем самым Go, что в нем не нуждаетесь.
Такую же логику перебора можно использовать в работе с картами для возврата каждой пары «ключ/значение».
Многопоточность
Во многом аналогично рассмотренным управляющим конструкциям в Go реализована упрощенная по сравнению с другими языками модель многопоточности. Для параллельного выполнения кода задействуются горутины, представляющие собой функции или методы, способные выполняться одновременно. Их нередко описывают как легковесные потоки, потому что, если сравнивать с реальными потоками, затраты на создание горутин минимальны.
Для этого используется ключевое слово go, размещаемое перед вызовом методов/функций, которые требуется выполнить параллельно:
❶ func f() {
fmt.Println("f function")
}
func main() {
❷ go f()
time.Sleep(1 * time.Second)
fmt.Println("main function")
}
В этом примере мы определяем функцию f()❶, которую вызываем в точке входа программы — функции main(). Перед этим вызовом указывается ключевое слово go❷, означающее, что программа будет выполнять f() параллельно. Другими словами, функция main() продолжит выполняться, не ожидая завершения f(). Затем мы используем time.Sleep(1*time.Second), чтобы временно приостановить main() для завершения f(). Если не приостановить main(), программа может выйти до завершения f() и ее результат не будет отражен в stdout. При правильной же реализации мы увидим вывод сообщений в stdout, отражающий завершение обеих функций.
В Go есть тип данных под названием каналы, который предоставляет механизм, позволяющий горутинам синхронизировать выполнение и взаимодействовать друг с другом. Рассмотрим пример использования каналов для одновременного отображения и суммирования длин разных строк:
❶ func strlen(s string, c chan int) {
❷ c <- len(s)
}
func main() {
❸ c := make(chan int)
❹ go strlen("Salutations", c)
go strlen("World", c)
❺ x, y := <-c, <-c
fmt.Println(x, y, x+y)
}
Сначала мы определяем и используем переменную с типа chanint. Можно определять каналы разных типов в зависимости от типа данных, которые планируется через них передавать. В нашем случае будем передавать между горутинами длину разных строк в виде целочисленных значений, значит, нужно применить канал int.
Обратите внимание на новый оператор <-. Он указывает, поступают ли данные из канала или в канал. Можете сравнить действие этого оператора с помещением элементов в корзину или удалением их из нее.
Функция strlen()❶ получает слово в виде строки, а также канал, который будет использоваться для синхронизации данных. Эта функция содержит одну инструкцию, c<-len(s)❷, которая с помощью встроенной функции len() определяет длину полученной строки и посредством оператора <- помещает результат в канал c.
Функция main() собирает все вместе. Сначала мы выполняем вызов make(chanint)❸ для создания канала int. Затем делаем несколько параллельных вызовов функции strlen() с помощью ключевого слова go❹, которое запускает несколько горутин. Далее в функцию передаются два строковых значения, а также канал, в который нужно будет поместить результаты. В завершение мы считываем данные из этого канала с помощью оператора <-❺. Это тоже можно сравнить с извлечением элементов из корзины и присваиванием их значений переменным x и y. Обратите внимание на то, что до момента считывания нужных данных из канала выполнение на этой строке прерывается.
По завершении происходит вывод длины каждой строки и их суммы в stdout. В данном примере мы увидим следующие числа:
5 11 16
Все это может показаться довольно сложным, но обозначить основные принципы параллельности необходимо, так как Go здесь выделяется особо. Поскольку многопоточность и параллельность в этом языке могут принимать весьма сложные формы, не пожалейте времени на самостоятельное изучение соответствующего материала. Далее в книге мы будем рассматривать более реалистичные и сложные реализации многопоточности, вводя буферизованные каналы, группы ожидания, мьютексы и другие понятия.
Обработка ошибок
В отличие от большинства языков, в Go нет синтаксиса для обработки ошибок try/catch/finally. Вместо этого здесь реализован минималистичный подход, подразумевающий проверку ошибок в местах их появления и исключающий их выскакивание в других функциях на протяжении цепочки вызовов.
Для этого в Go используется встроенный тип ошибок, который определяется через объявление interface:
type error interface {
Error() string
}
Это означает, что вы можете использовать в качестве error любой тип данных, который реализует метод Error(), возвращающий значение string. К примеру, вот настраиваемая ошибка, которую можно определить и задействовать на протяжении кода:
❶ type MyError string
func (e MyError) Error() string ❷ {
return string(e)
}
Здесь мы создаем пользовательский строковый тип MyError❶ и реализуем для этого типа метод Error()string❷.
Когда дело дойдет до обработки ошибок, вы быстро привыкнете к следующему паттерну:
func foo() error {
return errors.New("Some Error Occurred")
}
func main() {
if err := foo() ❶;err != nil ❷ {
// Обработка ошибки
}
}
Вы обнаружите, что, как правило, функции и методы возвращают не менее одного значения. Одним из этих значений практически всегда является error. В Go возвращенная error может иметь значение nil, указывающее, что функция не произвела ошибки и все сработало, как ожидалось. Если же вернется значение не nil, значит, что-то в этой функции пошло не так.
Таким образом, можно выполнять проверку на ошибки с помощью инструкции if, как показано в функции main(). Обычно вы будете встречать несколько инструкций, разделенных точкой с запятой. Первая вызывает функцию и присваивает итоговую ошибку переменной ❶, после чего вторая инструкция проверяет полученное в error значение на равенство nil❷. Эта обработка ошибки выполняется в теле выражения if.
По мере погружения в язык Go вы узнаете, что мнения о лучшем способе обработки ошибок разнятся. Одна из сложностей здесь состоит в том, что, в отличие от других языков, встроенный тип ошибки не включает в себя отслеживание стека, который помог бы точно определить контекст или местоположение ошибки. Несмотря на то что вы можете сгенерировать такой стек и присвоить пользовательскому типу в своем приложении, его реализация оставлена на усмотрение самих разработчиков. Поначалу это может несколько раздражать, но при правильном построении приложения с этим можно вполне успешно работать.
Обработка структурированных данных
Специалисты сферы безопасности зачастую пишут код, обрабатывающий структурированные данные, или данные со стандартной кодировкой, такой как JSON или XML. В Go для подобной кодировки данных реализованы стандартные пакеты. Наиболее актуальными из них являются encoding/json и encoding/xml.
Оба пакета могут выполнять маршалинг и демаршалинг произвольных структур данных, то есть преобразовывать строки в структуры и наоборот.
Взглянем на пример, выполняющий сериализацию структуры в байтовый срез и последующую десериализацию этого среза байтов обратно в структуру:
❶ type Foo struct {
Bar string
Baz string
}
func main() {
❷ f := Foo{"Joe Junior", "Hello Shabado"}
b, _❸ := json.Marshal❹ (f❺)
❻ fmt.Println(string(b))
json.Unmarshal(b❼, &f❽)
}
Этот код, не соответствующий наилучшим практикам и игнорирующий возможные ошибки, определяет тип struct с именем Foo❶. Мы инициализируем его в функции main()❷, а затем вызываем json.Marshal()❹, передавая ей экземпляр Foo❺. Метод Marshal() кодирует struct в JSON, возвращая срез byte❸, который мы затем выводим в stdout❻. Показанный здесь вывод — это строковое представление структуры Foo в кодировке JSON:
{"Bar":"Joe Junior","Baz":"Hello Shabado"}
В завершение мы берем тот же срез byte❼ и декодируем его через вызов json.Unmarshal(b,&f), в результате чего получаем экземпляр структуры Foo❽. При обработке XML процесс почти идентичен.
Работая с JSON и XML, вы обычно будете использовать теги полей. Они представляют собой элементы метаданных, которые вы присваиваете полям структуры, определяя для логики маршалинга/демаршалинга способ обнаружения и трактовки связанных элементов. Для этих полей существует бесчисленное множество вариаций, мы приведем лишь краткий пример их применения для обработки XML:
type Foo struct {
Bar string `xml:"id,attr"`
Baz string `xml:"parent>child"`
}
Строковые значения, заключенные в обратные кавычки, и являются тегами полей. Теги полей всегда начинаются с имени тега (в этом случае xml), сопровождаемого двоеточием и директивой, заключенной в двойные кавычки. Эта директива указывает, как поля должны обрабатываться. В нашем случае первая директива объявляет, что Bar нужно рассматривать не как элемент, а как атрибут с именем id. Вторая же указывает, что Baz нужно искать в подэлементе child, принадлежащем parent. Если изменить предыдущий пример, чтобы закодировать эту структуру как XML, то мы увидим такой результат:
<Foo id="Joe Junior"><parent><child>Hello Shabado</child></parent></Foo>
Кодировщик XML на основе этих директив соответствующим образом определяет имена элементов, и каждое поле в итоге обрабатывается нужным вам образом.
По мере чтения книги вы будете встречать использование этих тегов для работы и с другими форматами сериализации данных, включая ASN.1 и MessagePack. Мы также обсудим некоторые примеры определения собственных тегов, в частности, когда будем изучать обработку протокола блока серверных сообщений (Server Message Block, SMB).
Резюме
В этой главе мы настроили среду разработки и изучили фундаментальные аспекты языка. Это далеко не исчерпывающий список его характеристик, так как все нюансы и особенности совершенно невозможно обобщить в одной главе. Мы включили в нее лишь самое необходимое для освоения последующего материала книги. Далее переключимся на практическую сторону применения Go в области безопасности и хакерства.
2. TCP, сканеры и прокси
Рассмотрение практического применения Go мы начнем с разбора протокола контроля передачи (TCP), являющегося преобладающим стандартом и основой современных сетевых коммуникаций. TCP встречается повсеместно и отличается наличием множества отлично документированных библиотек, образцов кода и в большинстве случаев несложными потоками пакетов. Его понимание необходимо для грамотной реализации оценки, парсинга запросов и управления сетевым трафиком.
В роли атакующего вы должны понимать принцип работы TCP, чтобы справляться с разработкой пригодных вариантов его конструкций, позволяющих определять открытые/закрытые порты, распознавать такие потенциально ошибочные результаты, как ложные срабатывания — например, SYN-флуд защиты, — и обходить ограничения на исходящий трафик посредством переадресации портов. В этой главе вы изучите основы TCP-коммуникаций в Go, реализуете многопоточный правильно отрегулированный сканер портов, создадите TCP-прокси, который можно использовать для переадресации портов, а также воссоздадите Netcat-функцию «зияющая дыра в безопасности».
Были написаны целые учебники, раскрывающие каждый нюанс TCP, включая такие темы, как потоки и структура пакетов, надежность, повторная сборка сегментов и многие другие. Настолько подробная детализация выходит за рамки темы данной книги, поэтому мы рекомендуем глубже изучить эту тему, прочитав книгу Чарльза М. Козиерока (Charles M. Kozierok) The TCP/IP Guide
