Introduction

The democratization of the Internet has connected most of the world’s population to the network. In Brazil alone, an estimated 96% of internet users accessed cyberspace via mobile phones in 2017. However, socioeconomic conditions often force users to rely on static access points and public Wi-Fi networks—making them prime targets for cybercriminals.

This project was my undergraduate thesis (TCC) at PUCRS, where I built an open-source phishing awareness system that’s:

  • Low-cost (mostly upcycled components)
  • Portable and weatherproof
  • Energy self-sufficient (solar-powered)
  • Educational rather than malicious

The goal? Teach people how easy it is to fall for phishing attacks by showing them firsthand—then immediately educating them on how to protect themselves.

GitHub Project: https://github.com/radzki/SunPhish

Project Architecture

The system uses a NodeMCU development board (ESP-12E chip from the ESP8266 family), solar panels, voltage dividers, and a multiplexer. Here’s the high-level architecture:

┌─────────────────┐     ┌──────────────────┐
│  Solar Panels   │────▶│  Charge Circuit  │
│    (6V/1W x4)   │     │   (from e-waste) │
└─────────────────┘     └────────┬─────────┘
                                 │
                                 ▼
┌─────────────────┐     ┌──────────────────┐
│  Li-ion Battery │◀───▶│     NodeMCU      │
│   (10000mAh)    │     │    (ESP-12E)     │
└─────────────────┘     └────────┬─────────┘
                                 │
        ┌────────────────────────┼────────────────────────┐
        │                        │                        │
        ▼                        ▼                        ▼
┌───────────────┐    ┌───────────────────┐    ┌──────────────────┐
│ Voltage Div.  │    │  Async Web Server │    │   DNS Server     │
│ (Solar/Batt)  │    │  (Captive Portal) │    │ (All → Gateway)  │
└───────────────┘    └───────────────────┘    └──────────────────┘

Why NodeMCU?

The NodeMCU is perfect for this project:

  • Full TCP/IP stack support
  • Built-in Wi-Fi (2.4GHz)
  • GPIO, PWM, I²C, and ADC pins
  • Extremely popular in the maker and offensive security communities
  • Low cost (~$5 USD)

The main limitation: when operating as an Access Point, it’s limited to 4 simultaneous users (expandable to 8), so we need to disconnect users after they complete the flow.

The Attack Flow

Here’s what happens when someone connects to the malicious network:

  1. User connects to the Wi-Fi network (e.g., “#NET-ESCURO-WIFI”)
  2. Captive portal opens automatically (mimicking legitimate portals)
  3. User selects a social network for “authentication” (Facebook, Instagram, Twitter, Gmail)
  4. Cloned login page appears - visually identical to the real thing
  5. User enters credentials and clicks login
  6. Awareness page displays - explaining what just happened and how to protect themselves
  7. User is disconnected from the network automatically

Important: The system counts how many users completed the authentication cycle but never stores actual credentials.

Software Deep Dive

Asynchronous Web Server

One key requirement was handling multiple simultaneous connections. The ESPAsyncWebServer library solved this:

#include <ESPAsyncWebServer.h>
#include <ESPAsyncTCP.h>

AsyncWebServer server(80);

void setup() {
    // Serve static files from LittleFS
    server.serveStatic("/", LittleFS, "/");

    // Handle authentication POST
    server.on("/login", HTTP_POST, [](AsyncWebServerRequest *request){
        // Increment victim counter (don't store credentials!)
        victimCount++;

        // Add MAC to disconnect queue
        addToDisconnectQueue(request->client()->remoteIP());

        // Redirect to awareness page
        request->redirect("/awareness.html");
    });

    server.begin();
}

Captive Portal Detection

Different devices check for internet connectivity differently. To ensure the portal opens automatically on all devices, I added routes for common connectivity checks:

// Android
server.on("/generate_204", HTTP_GET, redirectToCaptivePortal);
server.on("/gen_204", HTTP_GET, redirectToCaptivePortal);

// iOS/macOS
server.on("/hotspot-detect.html", HTTP_GET, redirectToCaptivePortal);
server.on("/library/test/success.html", HTTP_GET, redirectToCaptivePortal);

// Windows
server.on("/ncsi.txt", HTTP_GET, redirectToCaptivePortal);
server.on("/connecttest.txt", HTTP_GET, redirectToCaptivePortal);

// Firefox
server.on("/success.txt", HTTP_GET, redirectToCaptivePortal);

Pro tip: Some Samsung phones have Google’s DNS (8.8.8.8) hardcoded. Setting the gateway IP to 8.8.8.8 solved this edge case.

Reverse Engineering with Ghidra

This is where things got interesting. The ESP8266 SDK used to have a function called wifi_send_pkt_freedom() that allowed sending raw 802.11 frames—perfect for deauthentication attacks. Espressif removed it after discovering its abuse.

I needed user disconnection functionality without using an outdated SDK. Two options:

  1. Use an old SDK version (buggy, fewer features)
  2. Reverse engineer the firmware to understand what changed

I chose option 2.

The Investigation

Using Ghidra (the NSA’s open-source reverse engineering tool), I analyzed the compiled firmware.elf:

Ghidra analysis of ieee80211_freedom_output function

The function ieee80211_freedom_output had new validation checks on the first bytes of the deauth frame. I could patch the binary directly, but that would require re-patching after every compilation—tedious.

Digging deeper into the ELF exports, I found wifi_softap_deauth():

undefined4 wifi_softap_deauth(int param_1) {
    int iVar1;
    int iVar2;
    undefined4 uVar3;
    int iVar4;
    uint uVar5;
    uint uVar6;

    iVar1 = g_ic_20_4;
    iVar2 = wifi_get_opmode();
    if (((iVar2 == 1) || (iVar2 == 0)) || (g_ic[46])) {
        uVar3 = 0;
    }
    // ... deauthentication logic
}

This function wasn’t documented in C/C++ APIs, but searching the NodeMCU Lua firmware revealed wifi.ap.deauth() which calls this exact function. Problem solved—no binary patching needed!

Implementation

// External declaration for the undocumented function
extern "C" {
    int wifi_softap_deauth(uint8_t mac[6]);
}

// Ticker that runs every 5 seconds
Ticker disconnectTicker;

void checkDisconnectQueue() {
    for (auto& client : disconnectQueue) {
        if (millis() - client.timestamp > DISCONNECT_DELAY) {
            wifi_softap_deauth(client.mac);
            removeFromQueue(client);
        }
    }
}

void setup() {
    disconnectTicker.attach(5, checkDisconnectQueue);
}

Limitation: Modern devices use MAC address randomization. We can’t permanently block someone, but we can still disconnect them after they complete the flow.

Energy Management & Deep Sleep

The ESP8266 can enter deep sleep mode to save power. My implementation:

void checkEnergyLevels() {
    float batteryVoltage = readBatteryVoltage();
    float panelVoltage = readPanelVoltage();

    // If battery is critically low
    if (batteryVoltage < CUTOFF_VOLTAGE) {
        enterDeepSleep(SLEEP_DURATION);
    }

    // If it's nighttime (no solar) and battery is getting low
    if (panelVoltage < 1.0 && batteryVoltage < WARNING_VOLTAGE) {
        enterDeepSleep(UNTIL_MORNING);
    }

    // Scheduled sleep (default: 8PM to 8AM)
    if (currentHour >= SLEEP_MAX_HOUR || currentHour < SLEEP_MIN_HOUR) {
        enterDeepSleep(ONE_HOUR);
    }
}

Hardware Design

The Upcycling Philosophy

Following the principle of upcycling (reutilização), almost all components came from electronic waste:

  • MOSFETs from a discarded video projector
  • Optocouplers from old equipment
  • Battery charging circuit from a broken power bank
  • Diodes and resistors from various e-waste

Only the NodeMCU and prototyping board were purchased new.

Voltage Dividers

The ESP8266’s ADC accepts 0-1V (with the NodeMCU’s onboard divider, up to 3.3V). To measure the 6.4V solar panel and 4.2V battery, I designed custom voltage dividers:

Source Max V R1/R2 Max Vin Ratio
Solar Panel 6.4V 15kΩ/10kΩ 8.3V 0.4
Battery 4.2V 10kΩ/10kΩ 6.6V 0.5

The dividers are transistor-switched to avoid constant current drain:

       Vin
        │
       ┌┴┐
       │ │ R1
       └┬┘
        ├──────────► ADC
       ┌┴┐
       │ │ R2
       └┬┘
        │
      ──┴── MOSFET (controlled by GPIO)
        │
       GND

Temperature Compensation

Diodes, resistors, and MOSFETs are temperature-sensitive. I added an LM35 temperature sensor with a clever trick: two diodes on the GND pin create a virtual ground, adding ~1.2V offset so the ADC can read temperatures below 70°C:

            LM35
         ┌───┴───┐
    Vcc ─┤       ├─ Vout ──► ADC
         │       │
         └───┬───┘
             │
            ─┴─ D1 (1N4148)
             │
            ─┴─ D2 (1N4148)
             │
            GND

Reducing NodeMCU Power Consumption

The datasheet claims 10µA in deep sleep. My measurements showed 15mA—150,000% higher!

The culprit: the CH340G USB-to-UART chip consumes 12mA constantly. Since USB isn’t needed during normal operation, I desoldered pin 16 (VCC) and added a jumper for when programming is needed.

After this modification: <5mA in deep sleep.

The remaining power draw comes from the AMS1117 linear regulator—notoriously inefficient. Replacing it with a DC-DC converter would help, but time constraints prevented this optimization.

Solar Panel Dimensioning

I empirically tested different panel configurations:

# Panels Panel Voltage Battery Current
1 4.73V -20mA (discharging!)
2 4.96V +80mA
3 5.10V +180mA
4 5.74V +280mA
5 5.93V +380mA

Four panels provide enough current for charging while reducing thermal stress on the charge controller.

Energy Balance Calculation

Daily consumption:

C(t) = Im × t + Imds × (24 - t)

Where:

  • Im = average current during operation (~85mA)
  • Imds = average current in deep sleep (~5mA)
  • t = hours of operation (12h default: 8AM-8PM)

Daily surplus/deficit:

S(t, ts) = Ipv × ts - C(t)

Where:

  • Ipv = current from panels to battery (~200mA)
  • ts = hours of useful sunlight (~6h pessimistic)

Result: +108mA daily surplus. The 10000mAh battery can sustain the system for ~70 days of deep sleep without any solar input.

Charge Controller Reverse Engineering

The charge controller came from a discarded power bank. To understand it, I traced every PCB trace with a multimeter in continuity mode:

┌─────────────────────────────────────────────────────────┐
│                                                         │
│  GND ─┬─ IC1 (DW01) ─┬─ R1 ──┬── R4 ──┬── D1 ──┬─ Vout │
│       │              │  27k  │   10k  │  DIODE │  (5V) │
│       └── Q1 ────────┘       │        │        │       │
│           (8205S)            │        │        │       │
│                              │        │        │       │
│  BAT+ ───────────────────────┼────────┴────────┘       │
│                              │                          │
│  BAT- ─── R3 ────────────────┤                          │
│           100Ω               │                          │
│                              │                          │
│           TD8208 ────────────┴─── L1 ─── D8            │
│          (Boost IC)              2.2µH                  │
└─────────────────────────────────────────────────────────┘

Key finding: the DW01 provides battery protection, the 8205S dual MOSFET handles switching, and the TD8208 is a boost converter for the 5V output. The circuit accepts the full voltage range from my solar panels.

Web Development

Cloning Login Pages

The fake login pages needed to be convincing. I manually cloned the essential CSS and removed unnecessary features (account creation, password recovery):

The challenge: fitting everything into the NodeMCU’s 1MB filesystem while reserving 2MB for OTA updates.

The Awareness Page

After “authentication,” users see this page explaining:

  • What just happened
  • That their credentials were NOT stored
  • Tips for identifying phishing attacks
  • Links to security resources
  • A downloadable PDF with more information

The user is automatically disconnected after 60 seconds.

Field Testing Results

Validation

Test Expected Result
Environment setup IDE configured
Captive portal POC Auto-open portal
Energy consumption 140mA TX, 10µA sleep ⚠️ Partial*
Solar panel validation 170mA @ 6V ⚠️ Partial**
Login pages 4+ social networks
User disconnection Deauth after flow
Burn-in test 1 week outdoor
Field deployment Users complete flow

*Deep sleep was 15mA due to CH340G; fixed with hardware mod
**Actual output was lower than datasheet; compensated with more panels

Real-World Test

The system was deployed at a private event. Out of ~10 people exposed to the network, 3 completed the full authentication flow—a 30% “success” rate in just one day.

Extrapolating to a larger population with longer exposure time, this demonstrates the very real danger of public Wi-Fi networks.

Conclusion

This project achieved its goals:

  • Built a working phishing awareness system
  • Powered entirely by solar energy
  • Constructed mostly from upcycled components
  • Successfully educated users about phishing risks

The 30% “victim rate” in a small sample proves that this threat is real and that user education is critical.

Future Improvements

  • Replace the linear regulator with DC-DC converter
  • Add current sensing for better energy management
  • Integrate LoRaWAN or Sigfox for remote monitoring
  • Design a custom PCB to reduce size
  • Add more login page templates

Resources


This project was developed for educational purposes as part of my Computer Engineering degree at PUCRS (Pontifícia Universidade Católica do Rio Grande do Sul), Brazil, in 2025. Always obtain proper authorization before conducting security testing.

GitHub Project: https://github.com/radzki/SunPhish