之前试着实现了一下黑暗幽灵(DCM)木马的数据传输方式,A rather marvelous means to send data anonymously via fake DNS query,于是就想着,DCM是通过DNS包附带信息,传给DNS服务器,然后在关键节点上抓包来实现的数据传输。那么这些数据也会到指定的DNS服务器上,假如我们自己实现这样的一个DNS服务器,那不就可以当作代理使用了?
在Google之后,我发现这样的想法已经有人实现了——iodine。不过这里并不想讲如何使用这个软件,下面要讲的就是自己实现一个!
服务端的构想如下,有一个进程监听UDP 53端口,客户端发送身份请求,服务端回应一串字符作为身份代码(后文作identification),然后就算是建立起了链接。这一步类似于cookie或者session的概念。
随后,客户端带着identification,再去请求各种服务。那么这些服务是怎么来的呢?服务端上,有不同的"应用"向监听在UDP 53端口的进程注册服务。这一点类似于node.js里express的想法。
并且,很多需要登录的公共热点都是对DNS包放行的,于是23333
下面就演示一个利用DNS查询包,用node.js写的一个音乐服务器。实际的效果如下
![ギリギリeye ~ギリギリmind~ギリギリeye ~ギリギリmind~ ヽ(`∀´)=(°∀°)ノ](/wp-content/uploads/2016/05/Walkure.png)
Wireshark抓包的效果,可以看到我们发出去的包都被认为是正常的。当然,服务器那边也可以在实际数据前增加对DNS的回复,然后两边在Wireshark里看起来都会是正常的包了。
![Walkure Wireshark抓包](/wp-content/uploads/2016/05/Walkure_Wireshark.png)
那么下面就是实现了~
首先是监听在UDP 53端口的服务器实现
var dgram = require('dgram'); var server = dgram.createSocket('udp4'); var crypto = require('crypto'); var EventEmitter = require('events'); // Listen on port 53 const PORT = 53; var connections = {}; var running = false; var applications = {}; server.on('message', function (msg, rinfo) { // Drop DNS Query Header msg = msg.toString().trim(); msg = msg.substr(17, msg.length - 17); // New connection if (msg == 'RUNERUNE') { // Get rune time var runetime = new Date().getTime(); // Calculate identification var identification = crypto.createHash('md5').update(rinfo.address + runetime).digest('hex').substr(0, 8); // This identification expires after 2 days runetime += 1000 * 60 * 60 * 48; connections[identification] = runetime; // Issue this identification console.log('[INFO] Issue New Identification: [' + identification + '] for ' + rinfo.address + ':' + rinfo.port); var message = new Buffer(identification); server.send(message, 0, message.length, rinfo.port, rinfo.address, function(err, bytes){ if (err) { console.log('[ERROR] UDP Server error: ' + err); delete connections[identification]; } else { // Emit connect event for each application if everything is fine for (var app in applications) { applications[app].connect(identification); } } }); } else if (msg.length == 16 && msg.startsWith('RUNERUNE')) { // This means a client wants to leave module.exports.emit('disconnect', identification); // Get the identification var identification = msg.substr(8); // Emit disconnect event for each application console.log('[INFO] Drop Identification: [' + identification + '] for ' + rinfo.address + ':' + rinfo.port); delete connections[identification]; for (var app in applications) { applications[app].disconnect(identification); } } else { // Try to extract identification try { var runetime = new Date().getTime(); var identification = msg.substr(0, 8); // If this is a valid identification if (identification in connections) { // If current identification does not expire if (connections[identification] > runetime) { // Extend to 2 days from now runetime += 1000 * 60 * 60 * 48; connections[identification] = runetime; // Drop 8 bytes of identification to get data msg = msg.substr(8); console.log('[INFO] UDP Server Received: [' + msg + '] from ' + rinfo.address + ':' + rinfo.port); // If message starts with 'app:' // This means our client is requesting the service that provided by a registered application if (msg.startsWith('app:')) { // Found out which service out client is requesting var app = ''; for (var i = 4; i < msg.length; i++) { if (msg[i] != ' ') { app += msg[i]; } else { break; } } // If there is such serivce if (app in applications) { // Emit message event to the very application msg = msg.substr(5 + app.length); applications[app].message(server, identification, msg, rinfo.port, rinfo.address); } else { // No such service, probably application data malformed var message = new Buffer('bad ADMF'); server.send(message, 0, message.length, rinfo.port, rinfo.address, function(err, bytes){ if (err) console.log('[ERROR] UDP Server error: ' + err); }); } } else { // Not requesting a service // Just make '[Data over DNS] ' the prefix of that message // Then reply it to client var message = new Buffer('[Data over DNS] ' + msg); server.send(message, 0, message.length, rinfo.port, rinfo.address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } } else { // Current identification expired // Delete it delete connections[identification]; var message = new Buffer('bad IDEP'); console.log('[ERROR] Identification Expired: ' + rinfo.address + ':' + rinfo.port); // Emit disconnect event to every registered application for (var app in applications) { applications[app].disconnect(identification); } server.send(message, 0, message.length, rinfo.port, rinfo.address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } } else { // Invalid Identification var message = new Buffer('bad IVID'); console.log('[ERROR] Invalid Identification: ' + rinfo.address + ':' + rinfo.port); server.send(message, 0, message.length, rinfo.port, rinfo.address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } } catch (err) { // The packet doesn't fit our scheme var message = new Buffer('bad IVDA'); console.log('[ERROR] Invalid Data: ' + rinfo.address + ':' + rinfo.port); server.send(message, 0, message.length, rinfo.port, rinfo.address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } } }); server.on('error', function (err) { console.log('[ERROR] UDP Server error:' + err.stack); server.close(); }); server.on("listening", function () { var address = server.address(); console.log("[INFO] UDP Server is listening " + address.address + ":" + address.port); }); module.exports = { // start start: function () { if (!running) { server.bind(PORT); running = true; } }, // register serivice register: function (path, app) { applications[path] = app; } };
然后是歌词文件服务
const fs = require('fs'); // Cache recently used files var lrc_lrc_caches = {}; module.exports = { // Ignore connect event connect: function (identification) { }, // Respond to message event message: function (server, identification, msg, port, address) { // Try to parse msg as JSON string try { // Get parameters var param = JSON.parse(msg); // Append '.lrc' to name var name = param['name'] + '.lrc'; var type = param['type']; // If client requests the info of the lyric file if (type == 'info') { fs.stat(name, function (err, stats) { // No such file exists if (err) { var message = new Buffer(JSON.stringify({status:'bad', reason:'No such file exists'})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } else { // Check cache var base64_size; if (name in lrc_caches) { base64_size = lrc_caches[name]['base64']; var visit = new Date().getTime(); visit += 1000 * 60 * 60; lrc_caches[name]['expire'] = visit; } else { // Or load from disk var file = fs.readFileSync(name).toString('base64'); var visit = new Date().getTime(); visit += 1000 * 60 * 60; base64_size = file.length; lrc_caches[name] = {size:stats.size, base64:base64_size, file:file, expire:visit}; } // Return actual size and base64 encoded size to client var message = new Buffer(JSON.stringify({status:'ok', info:{size:stats.size, base64:base64_size}})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } }); } else if (type == 'lost') { // If client just needs some lost chunks const packet_size = 1024; fs.stat(name, function (err, stats) { // Load data from cache or disk var data; if (name in lrc_caches) { data = lrc_caches[name]['file']; var visit = new Date().getTime(); visit += 1000 * 60 * 60; lrc_caches[name]['expire'] = visit; } else { data = fs.readFileSync(name).toString('base64'); var visit = new Date().getTime(); visit += 1000 * 60 * 60; base64_size = data.length; lrc_caches[name] = {size:stats.size, base64:base64_size, file:data, expire:visit}; } // Send requested chunk const size = data.length; var chunk = param['chunk']; if (chunk < 0 || chunk > Math.ceil(size / packet_size)) { var message = new Buffer(JSON.stringify({c:-1, d:'EOT'})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } else if (chunk == Math.ceil(size / packet_size) - 1) { var message = new Buffer(JSON.stringify({c:chunk, d:data.substr(chunk * packet_size, size % packet_size)})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } else { var message = new Buffer(JSON.stringify({c:chunk, d:data.substr(chunk*packet_size, packet_size)})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } }); } } catch (err) { var message = new Buffer(JSON.stringify({status:'bad', reason:'Invalid Data'})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } }, // Ignore disconnect event disconnect: function (identification) { } }
接下来是music服务,实现和lyric差不多,之后可以考虑写成一个内部的file服务,不过现在没那么多精力了23333
const fs = require('fs'); // Cache recently used files var caches = {}; // Wipe out expire caches every 30 minutes setInterval(function () { var current = new Date().getTime(); for (name in caches) { if (caches[name]['expire'] <= current) { console.log('[INFO] Expired ' + name); delete caches[name]; } } }, 60000 * 30); module.exports = { // Ignore connect event connect: function (identification) { }, // Respond to message message: function (server, identification, msg, port, address) { // Try to parse msg as JSON string try { // Get parameters var param = JSON.parse(msg); // Append '.mp3' to name var name = param['name'] + '.mp3'; var type = param['type']; // If client requests the info of the music file if (type == 'info') { fs.stat(name, function (err, stats) { if (err) { // No such file exists var message = new Buffer(JSON.stringify({status:'bad', reason:'No such file exists'})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } else { // Check cache var base64_size; if (name in caches) { base64_size = caches[name]['base64']; var visit = new Date().getTime(); visit += 1000 * 60 * 60; caches[name]['expire'] = visit; } else { // Or load from disk var file = fs.readFileSync(name).toString('base64'); var visit = new Date().getTime(); visit += 1000 * 60 * 60; base64_size = file.length; caches[name] = {size:stats.size, base64:base64_size, file:file, expire:visit}; } // Return actual size and base64 encoded size to client var message = new Buffer(JSON.stringify({status:'ok', info:{size:stats.size, base64:base64_size}})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } }); } else if (type == 'lost') { // If client just needs some lost chunks const packet_size = 1024; fs.stat(name, function (err, stats) { // Load data from cache or disk var data; if (name in caches) { data = caches[name]['file']; var visit = new Date().getTime(); visit += 1000 * 60 * 60; caches[name]['expire'] = visit; } else { data = fs.readFileSync(name).toString('base64'); var visit = new Date().getTime(); visit += 1000 * 60 * 60; base64_size = data.length; caches[name] = {size:stats.size, base64:base64_size, file:data, expire:visit}; } // Send requested chunk const size = data.length; var chunk = param['chunk']; if (chunk < 0 || chunk > Math.ceil(size / packet_size)) { var message = new Buffer(JSON.stringify({c:-1, d:'EOT'})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } else if (chunk == Math.ceil(size / packet_size) - 1) { var message = new Buffer(JSON.stringify({c:chunk, d:data.substr(chunk * packet_size, size % packet_size)})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } else { var message = new Buffer(JSON.stringify({c:chunk, d:data.substr(chunk*packet_size, packet_size)})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } }); } } catch (err) { var message = new Buffer(JSON.stringify({status:'bad', reason:'Invalid Data'})); server.send(message, 0, message.length, port, address, function(err, bytes) { if (err) console.log('[ERROR] UDP Server error: ' + err); }); } }, // Ignore disconnect event disconnect: function (identification) { } }
最后,服务端的就可以组合到一起啦(≧∇≦)
var rune = require('./rune.js'); var lyric = require('./lyric.js'); var music = require('./music.js'); rune.register('lyric', lyric); rune.register('music', music); rune.start();
那么现在就需要实现一下客户端的了~
const dgram = require('dgram'); const fs = require('fs'); const lame = require('lame'); const parseLrc = require('parse.lrc'); const readline = require('readline'); const Speaker = require('speaker'); const sprintf = require('sprintf-js').sprintf; const auth = dgram.createSocket('udp4'); const DNS_Header = new Buffer('\x23\x33\x01\x01\x00\x01\x00\x00\x00\x00\x00\x00\x01\x61\x00\x01\x00\x00\x01'); // Read server IPv4 address and music name from command line argument const host = process.argv[2]; var music_name = process.argv[3]; // Identification that issued by server var identification = ''; // In case of requesting more than one time var request_time = 0; // Playing status var playing = false; // Down load music from server via DNS function download_music(with_lyric, lsize, local) { // Enable lyric function by default var use_lyric = true; var lreceived = ''; if (local) { lreceived = with_lyric; } else { var lbase64_data = ''; for (var cc = 0; cc < with_lyric.length; cc++) { lbase64_data += with_lyric[cc]; } lreceived = new Buffer.from(lbase64_data, 'base64'); } if (lreceived.length != lsize) { console.log('[ERROR] UDP Data lost. Give up lyric'); lreceived = ''; } else { // Cache lyric file if (!local) { var ws = fs.createWriteStream(music_name + '.lrc'); ws.write(lreceived); ws.end(); } } lreceived = lreceived.toString(); // Request music info var music_info = dgram.createSocket('udp4'); // Request music file var packet_lost = dgram.createSocket('udp4'); music_info.send([DNS_Header, new Buffer(identification + 'app:music ' + JSON.stringify({name:music_name, type:'info'}))], 53, host, (err) => { music_info.on('message', (msg, rinfo) => { music_info.close(); // Get info of the music var info = JSON.parse(msg.toString()); // If server says ok if (info['status'] == 'ok') { const packet_size = 1024; var size = info['info']['size']; var base64size = info['info']['base64']; var music_data = ''; // Try to load music data from local cache try { music_data = fs.readFileSync(music_name + '.mp3'); } catch (err) { } if (music_data.length == size) { console.log('[INFO] Using local caching of ' + music_name); play(music_data, size, lreceived, true); } else { // Download from server by chunks // Each chunk except the last one takes 1024 bytes var total = Math.ceil(base64size / packet_size); var current = 0; var chunks = []; var base64_data = ''; console.log('[INFO] Downloading ' + music_name); // Check data every 5 seconds var retry = setInterval(function () { if (current != total) { console.log(sprintf('[INFO] Downloading %.2f%% of %s', (current / total) * 100, music_name)); var packet = 0; for (; packet < total; packet++) { // If this chunk fills nothing or the lenth of this chunk is wrong // We request it again if (chunks[packet] == undefined || (packet != total -1 && chunks[packet].length != 1024)) { packet_lost.send([DNS_Header, new Buffer(identification + 'app:music ' + JSON.stringify({name:music_name, type:'lost', chunk:packet}))], 53, host, (err) => { }); } } } }, 5000); // Store received chunk packet_lost.on('message', (msg, rinfo) => { var chunk = JSON.parse(msg.toString()); chunks[chunk['c']] = chunk['d']; current++; // If all the chunks are stored if (current == total) { packet_lost.close(); clearInterval(retry); play(chunks, size, lreceived); } }); } } else { console.log(info['reason']); } }); }); } // Play music function play(chunks, size, lrc_text, local) { var received; if (local) { // If the data is load from local received = chunks; } else { // Assemble and decode chunks into a complete file var base64_data = ''; for (var cc = 0; cc < chunks.length; cc++) { base64_data += chunks[cc]; } received = new Buffer.from(base64_data, 'base64'); } // Check size if (received.length != size) { console.log('[ERROR] UDP Data lost.'); } else { // Cache music file if (!local) { var ws = fs.createWriteStream(music_name + '.mp3'); ws.write(received); ws.end(); } console.log('[OK] Ready to play!'); // Decode the music file var decoder = new lame.Decoder(); decoder.write(received); decoder.end(); // Play it! var speaker = new Speaker; decoder.on('format', function (format) { }).pipe(speaker); playing = true; // If we have lyric if (lrc_text.length != 0) { // Parse lyric var lrc = parseLrc(lrc_text); lrc = lrc['lrcArray']; // Display the lyric var lrc_line = -1; var startTime = new Date().getTime(); var timer = setInterval(function () { var timeNow = new Date().getTime(); if (lrc_line + 1 < lrc.length) { var lrc_info = lrc[lrc_line + 1]; if (lrc_info['timestamp']*1000 < (timeNow - startTime)) { console.log(lrc_info['lyric']); lrc_line++; } } }, 500); // Finished playing speaker.on('close', function () { console.log('[INFO] Finished playing ' + music_name); music_name = undefined; playing = false; // Destory the timer clearInterval(timer); lrc_line = -1; }); } else { speaker.on('close', function () { console.log('[INFO] Finished playing ' + music_name); music_name = undefined; playing = false; }); } } } // Read music name function get_music_name() { process.stdin.pause(); if (music_name.length > 0) { walkure(); } var rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); rl.on('line', function(line){ music_name = line; walkure(); }); } // Get identification from server function get_auth(callback) { auth.send([DNS_Header, new Buffer('RUNERUNE')], 53, host, (err) => { auth.on('message', (msg, rinfo) => { auth.close(); identification = msg.toString(); callback(); }); }); } // Walkure! function walkure() { // Return if we're playing some music if (playing == true) { return; } var timeNow = new Date().getTime(); if (timeNow - request_time < 3000) { return; } request_time = timeNow; // Create sockets to get lyric info and file respectively var lyric_info = dgram.createSocket('udp4'); var lyric = dgram.createSocket('udp4'); lyric_info.send([DNS_Header, new Buffer(identification + 'app:lyric ' + JSON.stringify({name:music_name, type:'info'}))], 53, host, (err) => { lyric_info.on('message', (msg, rinfo) => { lyric_info.close(); // Parse info var linfo = JSON.parse(msg.toString()); if (linfo['status'] == 'ok') { const lpacket_size = 1024; var lsize = linfo['info']['size']; var lbase64size = linfo['info']['base64']; var lyric_data = ''; var ltotal = Math.ceil(lbase64size / lpacket_size); var lcurrent = 0; // Load file from local or server try { lyric_data = fs.readFileSync(music_name + '.lrc'); } catch (err) { } if (lyric_data.length == lsize) { console.log('[INFO] Using local lyric caching of ' + music_name); download_music(lyric_data, lsize, true); } else { var lchunks = []; console.log('[INFO] Downloading lyric of ' + music_name); var lretry = setInterval(function () { if (lcurrent != ltotal) { console.log(sprintf('[INFO] Downloading %.2f%% of %s lyric', (lcurrent / ltotal) * 100, music_name)); var packet = 0; for (; packet < ltotal; packet++) { if (lchunks[packet] == undefined || (packet != ltotal -1 && lchunks[packet].length != 1024)) { lyric.send([DNS_Header, new Buffer(identification + 'app:lyric ' + JSON.stringify({name:music_name, type:'lost', chunk:packet}))], 53, host, (err) => { }); } } } }, 2000); lyric.on('message', (msg, rinfo) => { var chunk = JSON.parse(msg.toString()); lchunks[chunk['c']] = chunk['d']; lcurrent++; if (lcurrent == ltotal) { lyric.close(); clearInterval(lretry); download_music(lchunks, lsize, false); } }); } } }); }); } // Start from here get_auth(get_music_name);