Skip to content

RCRemote

mobier edited this page Jan 16, 2019 · 1 revision

目标设备介绍

目前市面上能见到的遥控玩具啦,通常类似的玩具遥控都工作的无线电频率在27Mhz,49Mhz,极个别在40Mhz这几个频段上

通过频谱仪观察信号

我们其实可以软件无线电的方式对这个遥控数据进行分析,按照惯例我们通过GQRX观察这几个频点的频谱来确定目前手上的遥控工作在27Mhz

分析信号数据格式

然后这个就是我通过电视棒采样原始信号,之后用Audacity分析一下这里面的信号数据内容

前导用于告诉接收有信号过来注意接收,后面的有效数据就是控制小车的前后左右,你会发现这个有效数据中并没有出现地址码之类的识别标志数据位(意思就是你买了两个相同的玩具,两个遥控发射出来的信号是相同的,可以互相的影响对方的汽车),这就代表我们只要频率能对上然后在伪造出我们想要的有效数据就能够去挟持小朋友的遥控汽车啦,然后我们来说这个有效数据重的编码规则

有效数据中控制方向的其实数据中这个短的的脉冲的数量来操作的,经过我们多次采样数据进行分析我们得到的控制小车的脉冲数量,前进:10,后退:40,转左:58,转右:64,打开车门:22然后我们只要模拟出这个信号的脉冲数据就可以去挟持小车了,接下来我们来介绍一下如何去产生这个27Mhz的信号

伪造27Mhz频率信号

先介绍一下这个DDS模块,能够输出正弦波方波,可以输出0-40MHz范围内的信号,如果是使用AD9851的话频率可以到0-70Mhz,因为是AD9850 的时钟频率最高 125MHz,AD9851 的时钟频率最高180MHz,输出最高频率 AD9851 要比 AD9850 高,并且AD9850没有6倍频而AD9851有,这个模块甚至能到一个扫频器用,但是这里我们使用型号AD9850去产生这个27Mhz的控制的信号

我们先来看看怎么使用HackCubeSpecial驱动这个DDS模块去产生控制信号的,我们先来看看这个接线,是吧正弦波输出引脚2(ZOUT2)接上红色面包线,当做这个信号的发射天线,然后复用SPI引脚当做数据的输入引脚,需要主意是可能会和板子上的射频模块互相干扰

看下这个AD9850芯片的Datasheet,发现输出的频率写入的方式是通过串行进行写入,也可以通过SPI的方式吧这个频率写入到DDS模块中,或者是像我这样通过Arduino下面的shiftOut函数写入频率数据

伪造信号程序

int W_CLK = 13;
int FQ_UD = 12;
int DATA = 11;// 定义连接DDS模块引脚

uint32_t deltaphase = 0;//频率
uint8_t phase = 0;
double calibFreq = 125000000;

void pulse(int pin) { //翻转引脚 复位使用
  digitalWrite(pin, HIGH);
  digitalWrite(pin, LOW);
}

void dds_begin() { //引脚初始化
  pinMode(W_CLK, OUTPUT);
  pinMode(FQ_UD, OUTPUT);
  pinMode(DATA, OUTPUT); //定义引脚输出状态
    
  pulse(W_CLK);
  pulse(FQ_UD);//翻转引脚
}
/*
shiftOut(dataPin,clockPin,bitOrder,val) //函数能够将数据通过串行的方式在引脚上输出,相当于一般意义上的同步串行通信,这是控制器与控制器、控制器与传感器之间常用的一种通信方式。
dataPin // 数据引脚 连接DDS 模块中的 DATA
clockPin // 时钟引脚 连接DDS 模块中的 W_CLK
bitOrder //分别有 MSBFIRST 与 LSBFIRST 区分是高写入还是低位写入
val //所要写入的值,一个字节
*/
void update() { //将要产生的频率数据通过 shiftOut 串行 写入到DDS 模块中
  for (int i = 0; i < 4; i++, deltaphase >>= 8) { //deltaphase被定义成了uint32_t类型 但是shiftOut每次只能写入8个bit(1字节),分四次写入
    shiftOut(DATA, W_CLK, LSBFIRST, deltaphase & 0xFF); //shiftOut 写入
  }
  shiftOut(DATA, W_CLK, LSBFIRST, phase & 0xFF);
  pulse(FQ_UD);
}

void setfreq(double f, uint8_t p) { 
  deltaphase = f * 4294967296.0 / calibFreq; //计算出频率的值
  phase = p << 3;
  update();
}
void down() {
  pulse(FQ_UD);
  shiftOut(DATA, W_CLK, LSBFIRST, 0x04);
  pulse(FQ_UD);
}
void calibrate(double TrimFreq)
{
  calibFreq = TrimFreq;
}
double freq = 27000000; //要产生的频率  27Mhz
double trimFreq = 124999500;

void setup() {
  dds_begin();
  setfreq(freq, phase);
}
void loop(){
}

然后这个就是在正弦波输出引脚2(ZOUT2)上产生27Mhz信号的Arduino程序

我们用示波器去观察一下ZOUT2引脚上产生出来的频率波形是否与我们写入的频率一致,一致的话就可以进行下一步的工作了,产生出对应频率的控制方向信号

int time=550; //信号脉冲的时间 
void preample (int len) {  //产生前导码的信号
  for (int i = 0; i < len; i++) {
    setfreq(freq, phase);//产生27Mhz正弦波信号
    delayMicroseconds (time * 3 -100); //前导码宽脉冲的时间  -100是因为被写入数据和底层操作消耗的
    down(); //关闭正弦波信号
    delayMicroseconds (1); //实际上延迟550 US 因为被写入数据和底层操作消耗的
  }
}

void command (int len) { //产生有效数据信号,len=有效数据中的脉冲数量,通过这个数量控制前后左右
  for (int i = 0; i < len; i++) {
    setfreq(freq, phase); //产生27Mhz正弦波信号
    delayMicroseconds (time -124); //产生有效数据中的时间
    down(); //关闭正弦波信号
    delayMicroseconds (1);//实际上延迟550 US 因为被写入数据和底层操作消耗的
  }
}

void sendcommand (int n) //传入有效脉冲的个数来控制 小车的上下左右
{
  for (int i = 0; i < 95; i++) { 
    preample (4); // 先产生出4个前导信号
    command (n); // 然后产生有效数据中的脉冲数量
  }
}
void loop(){
    sendcommand(10); //调用发射函数
}

我们就可以通过示波器器抓到这个信号了

之后就可以看到信号了,然后小车就可以动起来了~然后可以修改有效数据中的脉冲数量(N)去控制小车啦

WebSocket 控制指令

可以通过HackCubeSpecial上的ESP8266配合ATmega32u4单片机对DDS模块进行配置,我们在ESP8266 上面跑一个WEB 端,就是上面这个,通过WebSocket的方式送Web端的摇杆角度,然后ESP8266收到后根据摇杆角度对ATmega32u4发送串口指令执行不同的操作

<!DOCTYPE html>
<html>
<head>
<title>ESP8266 test</title>
<meta name="viewport" content="width=device-width, inicioial-scale=0.7, maximum-scale=0.7">
<meta charset="utf-8">
<style>
/*body { text-align: center; font-size: width/2pt;background-color: #A9E2F3; }*/
body { text-align: center; font-size: width/2pt;background-color: #A9E2F3; }
/* #remote { background-color: #A9E2F3; }
#remote{ background:black;}   */

h1 { font-weight: bold; font-size: width/2pt; }
h2 { font-weight: bold; font-size: width/2pt; }
button { font-weight: bold; font-size: width/2pt; }
</style>
<script>
var canvas_width = 400, canvas_height = 400;
var radio_base = 150;
var radio_handle = 72;
var radio_shaft = 120;
var rango = canvas_width/2 - 10;
var step = 18;
var ws;
var joystick = {x:0, y:0};
var click_state = 0;

var ratio = 1;

function inicio()
{
    var width = window.innerWidth;
    var height = window.innerHeight;

/*  width=1000;
    height=500;
    console.log("incio");
    console.log(width);
    console.log(height);
*/
    if(width < height)
        ratio = (width - 50) / canvas_width;
    else
        ratio = (height - 50) / canvas_width;
    ratio=1.5;


/*  canvas_width = Math.round(canvas_width*ratio);
    canvas_height = Math.round(canvas_height*ratio);*/
    canvas_width = Math.round(canvas_width);
    canvas_width = Math.round(500);
    canvas_height = Math.round(500);

    console.log("canvas_width")
    console.log(canvas_width)


    radio_base = Math.round(radio_base*ratio);
    radio_handle = Math.round(radio_handle*ratio);
    radio_shaft = Math.round(radio_shaft*ratio);
    rango = Math.round(rango*ratio);
    step = Math.round(step*ratio);
    
    var canvas = document.getElementById("remote");
    canvas.width = canvas_width;
    canvas.height = canvas_height;

    canvas.addEventListener("touchstart", mouse_down);
    canvas.addEventListener("touchend", mouse_up);
    canvas.addEventListener("touchmove", mouse_move);
    canvas.addEventListener("mousedown", mouse_down);
    canvas.addEventListener("mouseup", mouse_up);
    canvas.addEventListener("mousemove", mouse_move);
    
    var ctx = canvas.getContext("2d");

    ctx.translate(canvas_width/2, canvas_height/2);


    ctx.shadowBlur = 20;
    ctx.shadowColor = "LightGray";
    ctx.lineCap="round";
    ctx.lineJoin="round";
        
    actualizarVista();
}
function conectar()
{
    if(ws == null)
    {
         ws = new WebSocket('ws://' + window.location.hostname + ':81');
        //ws = new WebSocket('ws://' + "192.168.1.20" + ':81');
        document.getElementById("ws_state").innerHTML = "CONECTANDO";
        ws.onopen = ws_onopen;
        ws.onclose = ws_onclose;
        ws.onmessage = ws_onmessage;
    }
    else
        ws.close();
}
function ws_onopen()
{
    //document.getElementById("ws_state").innerHTML = "<font color='blue'>CONECTADO</font>";
    document.getElementById("bt_connect").innerHTML = "DESCONECTADO";
    actualizarVista();
}
function ws_onclose()
{
//  document.getElementById("ws_state").innerHTML = "<font color='gray'>CERRADO</font>";
    document.getElementById("bt_connect").innerHTML = "CONECTADO";
    ws.onopen = null;
    ws.onclose = null;
    ws.onmessage = null;
    ws = null;
    actualizarVista();
}
function ws_onmessage(e_msg)
{
    e_msg = e_msg || window.event; // MessageEvent
    
}
function enviarDatos()
{
    var x = joystick.x, y = joystick.y;
    var joystick_rango = rango - radio_handle;
    x = Math.round(x*100/joystick_rango);
    y = Math.round(-(y*100/joystick_rango));
    
    if(ws != null)
        ws.send(x + ":" + y + "\r\n");
}
function actualizarVista()
{
    var x = joystick.x, y = joystick.y;


    var canvas = document.getElementById("remote");

    var ctx = canvas.getContext("2d");
    
    ctx.clearRect(-canvas_width/2, -canvas_height/2, canvas_width, canvas_height);
    
    ctx.lineWidth = 3;


    ctx.strokeStyle="black";
/*  ctx.fillStyle = "hsl(0, 0%, 0%)";*/
    //ctx.fillStyle = "#A9E2F3";
    ctx.fillStyle = "#A9E2F3";
    ctx.beginPath();
    ctx.fillRect(-250,-250,500,500);
    ctx.stroke();
    ctx.fill();

    ctx.strokeStyle="black";
    ctx.fillStyle = "hsl(0, 0%, 35%)";
    ctx.beginPath();
    ctx.arc(0, 0, radio_base-50, 0, 2 * Math.PI);
    ctx.stroke();
    ctx.fill();
    
    ctx.strokeStyle="white";
    
    var lineWidth = radio_shaft;
    var pre_x = pre_y = 0;
    var x_end = x/5;
    var y_end = y/5;
    var max_count  = (radio_shaft - 10)/step;
    var count = 1;



    while(lineWidth >= 10)
    {
        var cur_x = Math.round(count * x_end / max_count);
        var cur_y = Math.round(count * y_end / max_count);

        ctx.lineWidth = lineWidth;
        ctx.beginPath();
        ctx.lineTo(pre_x, pre_y);
        ctx.lineTo(cur_x, cur_y);
        ctx.stroke();
        
        lineWidth -= step;
        pre_x = cur_x;
        pre_y = cur_y;
        count++;
    }
    
    var x_start = Math.round(x / 3);
    var y_start = Math.round(y / 3);
    lineWidth += step;
    
    ctx.beginPath();
    ctx.lineTo(pre_x, pre_y);
    ctx.lineTo(x_start, y_start);
    ctx.stroke();
        
    count = 1;
    pre_x = x_start;
    pre_y = y_start;
    
    while(lineWidth < radio_shaft)
    {
        var cur_x = Math.round(x_start + count * (x - x_start) / max_count);
        var cur_y = Math.round(y_start + count * (y - y_start) / max_count);
        ctx.lineWidth = lineWidth;
        ctx.beginPath();
        ctx.lineTo(pre_x, pre_y);
        ctx.lineTo(cur_x, cur_y);
        ctx.stroke();
        
        lineWidth += step;
        pre_x = cur_x;
        pre_y = cur_y;
        count++;
    }
    
    var grd = ctx.createRadialGradient(x, y, 0, x, y, radio_handle);
    for(var i = 85; i >= 50; i-=5)
        grd.addColorStop((85 - i)/35, "hsl(0, 100%, "+ i + "%)");
        

/*  ctx.strokeStyle="black";
    ctx.fillStyle = "hsl(0, 0%, 100%)";
    ctx.beginPath();
    ctx.fillRect(-250,-250,500,500);
    ctx.stroke();
    ctx.fill();*/

    ctx.fillStyle = "#89E2F3";;
    ctx.beginPath();
    ctx.arc(x, y, radio_handle, 0, 2 * Math.PI);
    ctx.fill();
}
function procesarEvento(event)
{
    var pos_x, pos_y;
    if(event.offsetX)
    {
        pos_x = event.offsetX - canvas_width/2;
        pos_y = event.offsetY - canvas_height/2;
    }
    else if(event.layerX)
    {
        pos_x = event.layerX - canvas_width/2;
        pos_y = event.layerY - canvas_height/2;
    }
    else
    {
        pos_x = (Math.round(event.touches[0].pageX - event.touches[0].target.offsetLeft)) - canvas_width/2;
        pos_y = (Math.round(event.touches[0].pageY - event.touches[0].target.offsetTop)) - canvas_height/2;
    }
    
    return {x:pos_x, y:pos_y}
}
function mouse_down()
{
    if(ws == null)
        return;
    
    event.preventDefault();
    
    var pos = procesarEvento(event);
    
    var delta_x = pos.x - joystick.x;
    var delta_y = pos.y - joystick.y;
    
    var dist = Math.sqrt(delta_x*delta_x + delta_y*delta_y);
    
    if(dist > radio_handle)
        return;
        
    click_state = 1;
    
    var radio = Math.sqrt(pos.x*pos.x + pos.y*pos.y);
    
    if(radio <(rango - radio_handle))
    {
        joystick = pos;
        enviarDatos();
        actualizarVista();
    }
}
function mouse_up()
{
    event.preventDefault();
    click_state = 0;
    var pos = procesarEvento(event);
    pos.x=0;
    pos.y=0;
    var radio = Math.sqrt(pos.x*pos.x + pos.y*pos.y);


    if(radio <(rango - radio_handle))
    {
        joystick = pos;
        enviarDatos();
        actualizarVista();
    }
}
function mouse_move()
{
    if(ws == null)
        return; 
    
    event.preventDefault();
    
    if(!click_state)
        return;
    
    var pos = procesarEvento(event);
    
    var radio = Math.sqrt(pos.x*pos.x + pos.y*pos.y);
    
    if(radio <(rango - radio_handle))
    {
        joystick = pos;
        enviarDatos();
        actualizarVista();
    }
}
window.onload = inicio;


</script>
</head>
<body>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>



<canvas id="remote"></canvas>
<h2>
<p>
Action : <span id="ws_state">null</span>
</p>
<!-- <button id="bt_connect" type="button" onclick="conectar();">CONECTAR</button> -->
</h2>
</body>
</html>

 <script type="text/javascript">
    conectar();
</script> 

这个是控制前端页面和JS,主要的作用是和ESP8266在81建立一个WebSocket的链接,然后在摇杆发生后,将摇杆的X,Y轴的数值传给ESP8266,然后ESP8266就可以根据这个数值判断摇杆的角度从而执行对应的功能去操控

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <WebSocketsServer.h>
#include <Hash.h>
#include <FS.h>

const char* ssid = "HackCube";
const char* password = "unicorn_team";

int OUTPUT4 = 16;
int OUTPUT3 = 5;
int OUTPUT2 = 4;
int OUTPUT1 = 0;
long duracion = 0;
WebSocketsServer webSocket = WebSocketsServer(81);
ESP8266WebServer server(80);

void setup(void) {
  delay(1000);
  //Velocidad
  Serial.begin(115200);
  //Configuracion de salidas
  pinMode (OUTPUT1, OUTPUT);
  pinMode (OUTPUT2, OUTPUT);
  pinMode (OUTPUT3, OUTPUT);
  pinMode (OUTPUT4, OUTPUT);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  IPAddress myIP = WiFi.localIP();
  Serial.print("IP: ");
  Serial.println(myIP);
  SPIFFS.begin();
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
  server.onNotFound([]() {
    if (!handleFileRead(server.uri()))
      server.send(404, "text/plain", "Archivo no encontrado");
  });
  //Servidor Web Iniciado
  server.begin();
  Serial.println("Servidor HTTP iniciado");
}
void loop(void) {
  webSocket.loop();
  server.handleClient();
}
//Funcion predefinida de un WebSocket
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t lenght) {
  switch (type) {
    //En caso de que un cliente se desconecte del websocket
    case WStype_DISCONNECTED: {
        Serial.printf("Usuario #%u - Desconectado\n", num);
        break;
      }
    //Cuando un cliente se conecta al websocket presenta la información del cliente conectado, IP y ID
    case WStype_CONNECTED: {
        IPAddress ip = webSocket.remoteIP(num);
        Serial.printf("Nueva conexión: %d.%d.%d.%d Nombre: %s ID: %u\n", ip[0], ip[1], ip[2], ip[3], payload, num);
        break;
      }
    //Caso para recibir información que se enviar vía el servidor Websocket
    case WStype_TEXT: {
        String entrada = "";
        for (int i = 0; i < lenght; i++) {
          entrada.concat((char)payload[i]);
        }
        String data = entrada;
        if (data) {
          int pos = data.indexOf(':');
          long x = data.substring(0, pos).toInt();
          long y = data.substring(pos + 1).toInt();

          if (y > 35 && x > 35) {
            Serial.println("^>");
            uprigth();
          } else if (y > 25 && x < -25) {
            Serial.println("<^");
            upleft();
          } else if (y < -25 && x < -25) {
            Serial.println("<-");
            downleft();
          } else if (y < -35 && x > 35) {
            Serial.println("->");
            downrigth();
          } else if (y > 35) {
            Serial.println("^");
            up();
          }  else if (y < -35) {
            Serial.println("-");
            down();
          } else {
            car_reset();
          }
        }
        break;
      }
  }
}

这个就是ESP8266上的代码,通过SPIFFS放置刚刚的前端页面,然后连接上我们的路由器后,我们可以用用手机访问ESP8266的ip,就看看到我们的前端的页面了,然后我们通过WebSocket方式与ESP8266进行通信,因为平常每次操作都需要发起一次HTTP请求,数据时延上面操作会受到影响机会导致,操作不同步的现象出现,然后我们用WebSocket只需要发起一次HTTP请求就能保持长链接,并且每次每次操作不需要增加额外的数据内容,这个X,Y轴就是前端页面中摇杆传递过来的数值了,可以根据这个去对小车进行控制 后期可能也会考虑在板子加上DDS模块或者使用YX-4116芯片的遥控模块供大家使用的

参考链接

https://yoamoprogramar.com/2018/02/12/carrito-wifi-nodemcu-esp8266-websocket-webserver/

http://www.hackrf.net/2014/03/%E7%94%A8hackrf%E5%92%8Cgnuradio%E6%9D%A5%E5%AE%9E%E7%8E%B0%E5%AF%B9%E9%81%A5%E6%8E%A7%E5%B0%8F%E8%BD%A6%E7%9A%84%E6%8E%A7%E5%88%B6/

https://www.riyas.org/2014/06/computer-controlling-27mhz-remote-control-car-ad9850-dds.html

快速开始

功能介绍

WIFI

  • deauther
  • Probe Request
  • Beacon

NFC

  • 低频卡嗅探
  • 低频卡写入
  • 低频卡模拟

RF

HID

  • 模拟键盘
  • 定义攻击脚本
  • 上传脚本

软件使用

  • web
  • Serial
Clone this wiki locally