Спам-скрипт для группы в vk.com

машина на службе человека

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

Однажды мне понадобилось сделать рассылку с приглашением на один ивент в vk. В этой статье мы рассмотрим, как может быть автоматизирован такой процесс.

Все в DOM

Итак, задача состоит в сборе айди целевых пользователей (жертв) и рассылке инвайтов и текстовых приглашений по ним. Немного погуглив, я нашёл программу VKBot для этих целей, но она полуплатная, и это скучно. Особенно не разбираясь в API vk.com могу сказать, что он у него есть, как и XMPP-шлюз. Но ведь в первую очередь vk.com — это обычный сайт. А значит мы можем манипулировать им через старый-добрый DOM и выполнить всю работу без применения специализированных API.

Инъекция

Для работы с DOM сайта, необходимо в него инъектироваться. Это можно сделать, например, написав расширение для браузера, подобное тому, какое я описываю тут. По ссылке описывается набор модулей, состоящий из управляющего приложения с личным TCP-сервером и Chrome-расширения, инъектирующегося в нужные сайты.

Плюс этого решения в том, что его легко дополнять новым функционалом, так что я просто добавил новую команду spam в Chrome-приложение

Commands.addCommand("spam",
  "spambot",
  function(con, args) {
    chrome.runtime.sendMessage(
        extid, {action: 'vkspam'}, function(response) {}
    );
    return "ok";
  });

и весь спам-код разместил в модуле расширения injection.js.

Спам-скрипт

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

Сбор айди

Сбор айди пользователей производится тривиальным сканированием DOM-структуры страницы приглашения пользователей группы на ивент:

function gather_users() {
  var ubs = document.getElementsByClassName('user_block')
  var users = []
  for (var i = 0; i < ubs.length; i++) {
    var ub = ubs[i]
    var uid = ub.id.match(/\d+/)[0]
    var info = ub.getElementsByClassName('info')[0]
    var ff = info.getElementsByClassName('friends_field')[0]
    var name = ff.getElementsByTagName('b')[0]
    name = name.innerText.split(' ')[0]
    users.push(
      [uid, name, 0]
    )
  }
  return [users, ubs]
}

В результате мы получаем массив вида

[2984948, 'Александр', 0],
[5959484, 'Михаил', 0],
[9683459, 'Анна', 0],
. . .
[9594488, 'Иосиф', 0],

где последнее число — это статус обработки (0 значит необработано). Также функция возвращает соответствующий вектор ubs с div-ами из DOM-структуры страницы соцсети, которые нужны будут для дальнейшей манипуляцией DOM.

Единственное, — и это сразу даёт нам понятие о крутости нашего скрипта, — перед выполнением этого кода нужно вручную прокрутить страницу со списком пользователей до самого конца, чтобы они все подгрузились.

Маленькая БД

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

Local Storage — это универсальное строковое HTML5-хранилище данных, привязанных к сайту. Работа с Local Storage заключается в трёх функциях:

localStorage.setItem('item_name', 'item_value')
item_value = localStorage.getItem('item_name')
localStorage.removeItem('item_name')

Так как хранилище строковое, то нам также пригодится встроенный класс JSON, преобразующий javascript-объект в json-представление и обратно:

object = JSON.parse('json_string')
json_string = JSON.stringify(object)

Отправка приглашений

Отправку приглашений нужно (легче) проводить, пока мы находимся на странице со списком пользователей, так как тут нам достаточно эмулировать нажатия на ссылки "Выслать приглашение":

var actions = ub.getElementsByClassName('actions')[0]
var link = actions.getElementsByTagName('a')[0]
if (link.innerText == 'Выслать приглашение') {
  link.click()
  list[j][2] = 1 // invitation sent
  localStorage.setItem('spam', JSON.stringify(list))
}

где ubdiv-элемент пользователя, возвращаемый выше описанной функцией gather_users. Как видим, здесь также всё довольно тривиально: из DOM-структуры извлекается элемент ссылки и она нажимается. Делается проверка на строку "Выслать приглашение", чтобы не отменить уже высланное приглашение. Наконец, мы сохраняем новое состояние жертвы (1) в Local Storage.

Однако, как выяснилось, хотя такая активации ссылок работает в других местах соцсети и даже конкретно для данного вида ссылок она также работает, будучи выполненной из консоли, тем не менее из расширения click() не срабатывает, вызывая ошибки где-то внутри соцсети. Поэтому мы применим обходной манёвр и вызовем данный код от лица самой страницы — с помощью инъекции элемента:

//link.click()
// execute click on invite link from context of the page
var script = document.createElement('script')
script.innerHTML =
  "var ubs = document.getElementsByClassName('user_block'); \
   var ub = ubs[" + i.toString() + "]; \
   var actions = ub.getElementsByClassName('actions')[0]; \
   var link = actions.getElementsByTagName('a')[0]; \
   link.click()"
document.documentElement.firstChild.appendChild(script)

Капча

При отправке приглашения сайт иногда требует доказать, что вы не робот. На лицо наглая дискриминация. Было бы очень интересно прикрутить один из каптча-ресолверов или написать на C или js свой собственный, но, боюсь, тогда рассылка мне бы уже не понадобилась, так как время бы уже ушло. Но мы можем просто детектировать появление капчи и издать звуковой сигнал, чтобы было известно, что скрипт остановлен и ждёт:

if (document.getElementsByClassName('captcha')[0]) {
  beep()
  return
}

а каптчи распознавать самим.

Здесь beep — это замечательная функция с форума Stack Overflow:

function beep() {
    var snd = new Audio("data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");
    snd.play();
}

Она не требует внешних wav-файлов, а использует встроенный посредством технологии dataURL непосредственно в код вав.

Рассылка

Когда база пользователей собрана и приглашения отправлены, можно начинать рассылку текстовых сообщений. Для этого достаточно перейти на страницу вида /write123123:

document.location = 'https://vk.com/write' + uid

и найти и воспользоваться текстовым полем и кнопкой отправки сообщения:

function sendSpam() {
  var content = document.getElementById('im_content')
  var text = content.getElementsByClassName('im_editable')[0]
  var s = text.innerText

  if (list[i][2] == 1) { // avoid double send
    if (s) {
      list[i][2] = 2 // message sent
      localStorage.setItem('spam', JSON.stringify(list))
      var btn = document.getElementById('imw_send')
      btn.click()
    } else {
      if (name) {
        text.innerText =
          spamtemplate.replace('%username%', ', ' + name)
      } else {
        text.innerText =
          spamtemplate.replace('%username%', '')
      }
      setTimeout(sendSpam, 1000)
    }
  }
}

Здесь мы находим необходимые нам DOM-элементы, затем заполняем текстовое поле im_editable и, после того, как оно будет заполнено, эмулируем нажатие по кнопке imw_send. На этот раз обычным способом. В качестве сообщения берётся шаблон, в котором подстрока %username% заменяется реальным именем пользователя, что было извлечено на этапе сбора айди пользователей.

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

Как активировать данный скрипт? В виду того, что мы должны как-то попасть на эту страницу, а затем и на страницы для всех остальных жертв, значит document.location будет постоянно меняться, а значит расширение будет каждый раз переинъектироваться. Поэтому удобно расположить код активации рассылки непосредственно в теле расширения, так как оно исполняется при каждой инъекции. А чтобы отличать "наши" страницы от обычных, можно при переходе через document.location добавить к URL параметр spam: document.location = 'https://vk.com/write' + uid + '?spam'

Соцсеть не будет против] Теперь в теле расширения мы можем определять, инъектированны ли мы в спам-страницу или нет:

if (document.location.toString().match(/write\d+.*spam/)) {
  ...
}

После отсылки сообщения скрипт производит поиск новой жертвы в БД и производит очередной переход на соответствующую /writeID-страницу и таким образом рекурсивно спамит всех жертв:

// spam next victim
for (var j = 0; j < list.length; j++) {
  var u = list[j]
  var uid = u[0]
  var state = u[2]
  if (state == 1) {
    document.location = 'https://vk.com/write' + uid + '?spam'
    break
  }
}

Как и в случае с приглашениями, соцсеть не даст просто так рассылать сообщения: после 20 лс будет исчерпан лимит сообщений, отсылаемых недругам. Этот момент также несложно отследить:

function check_limit() {
  if (!limit) {
    if (document.getElementsByClassName('box_body')[0]) {
      limit = true
      alert('vkspam: messages limit occured')
    } else {
      setTimeout(check_limit, 100)
    }
  }
}

setTimeout(check_limit, 100)

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

if (!limit) {
  document.location = 'https://vk.com/write' + uid + '?spam'
}

И это весь основной код, необходимый для автоматизации спама.

P.S.

Как выяснилось, временно заблокированным пользователям (не удалённым) через адрес /writeID можно отправлять сообщения. Это интересно, так как через обычный интерфейс по-моему эта функция закрыта. С другой стороны, соцсеть довольно быстро начинает помечать рассылку как спам, поэтому она не особенно эффективна, если не добавить в скрипт полиморфный генератор сообщений%] Усугубляет ситуацию лимит в 20 лс, дополнительный минус здесь состоит в том, что, чтобы обойти его, можно, конечно, воспользоваться дополнительными аккаунтами, но при этом может потребоваться вручную, через консоль копировать Local Storage между ними, что попутно исключает одновременную рассылку с разных аккаунтов.

Однако в целом можно сказать, что vk.com вполне поддаётся простой автоматизации даже без применения открытых API.

 

Исходники расширения и приложения для спам-рассылки.

Список сайтов-антикаптч, которые может использовать упомянутый в начале статьи VKBot (насколько я понимаю, они все платные):

anti-captcha.com
deathbycaptcha.eu
captchabot.com
ripcaptcha.com
rucaptcha.com
pixodrom.com

 

Всем успехов в автоматизации всего, что движется, и в перемещении и последующей автоматизации остального.

 

shitpoet@gmail.com

 



 

free hit counters