Network traffic over DNS

之前试着实现了一下黑暗幽灵(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~ ヽ(`∀´)=(°∀°)ノ
ギリギリeye ~ギリギリmind~ギリギリeye ~ギリギリmind~ ヽ(`∀´)=(°∀°)ノ

 

Wireshark抓包的效果,可以看到我们发出去的包都被认为是正常的。当然,服务器那边也可以在实际数据前增加对DNS的回复,然后两边在Wireshark里看起来都会是正常的包了。

Walkure Wireshark抓包
Walkure Wireshark抓包

 

那么下面就是实现了~


首先是监听在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); 

Leave a Reply

Your email address will not be published. Required fields are marked *

4 + 11 =