angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Eine weitere Captcha-Implementierung für Flask und WTForms

Lasst uns ihre tief lernenden GPU's verbrennen.

4 Juli 2019
post main image
Original photo unsplash.com/@barnikakovacs.

In der Vergangenheit habe ich ein Captcha in PHP geschrieben, um die Anzahl der Anmeldungen für E-Mail-Newsletter zu begrenzen, es hat gut funktioniert, es ist sogar noch heute im Einsatz. Sie können Spam-Registrierungen nicht wirklich blockieren. Es gibt Registrierungsroboter, aber es gibt auch Leute, die ein paar Dollar bezahlt bekommen, um Ihre Website mit gefälschten oder Trollkonten zu überfluten. Das ist die Realität, und wir müssen uns ihr stellen. Und jetzt gibt es auch Deep-Learning, mit dem wir unseren Captcha-Code in nur 15 Minuten knacken können.

Wir, die Entwickler von Websites, müssen Ideen entwickeln, um gefälschte Registrierungen anzufechten. Es ist nicht möglich, aber es können nur wenige Dinge getan werden und das Hinzufügen eines Captcha ist eines davon.

Für viele wahrscheinlich langweilige Sachen kannst du genügend Bibliotheken finden, die dies für dich tun. Ich, ich, ich will keine Bibliothek benutzen, ich will mein eigenes Captcha in Python schreiben, Baby lernen. Und weil ich die Privatsphäre der Besucher meiner Websites schätze, möchte ich auch ReCaptcha oder eine andere Remote-Lösung nicht verwenden.

Die unten vorgestellte Captcha-Lösung funktioniert, ist aber noch nicht fertig, nur die grundlegende Verzerrung muss hinzugefügt werden. Es ist nicht wirklich flaskspezifisch, es ist nur Python. Ich habe BytesIO zum ersten Mal verwendet, um ein Bild nicht in einer Datei, sondern im Speicher zu speichern. Was Flask spezifisch ist, ist, wie man es in einem Anmeldeformular und auf einer Webseite anzeigt. Beachten Sie, dass es sich hierbei nicht um eine Komplettlösung handelt, sondern nur um die wichtigsten Teile.

So wird das Captcha erzeugt. captcha_fonts_path() ist eine Funktion, die Sie ausführen müssen, um die Schriftart(en) für das Captcha zu importieren.

import math
import random
import secrets

from PIL import Image, ImageFont, ImageDraw, ImageOps
from io import BytesIO


def captcha_create():

    use_chars = ['A', 'b', 'C', 'd', 'e', 'f', 'h', 'K', 'M', 'N', 'p', 'R', 'S', 'T', 'W', 'X', 'Y', 'Z']
    use_nums = ['2', '3', '4', '6', '7', '8', '9']

    code_char_count = 5
    code_chars = []
    for i in range(code_char_count):
        s = random.randrange(2)
        if s & 1:
            code_chars.append( random.choice(use_chars) )
        else:
            code_chars.append( random.choice(use_nums) )

    # captcha dimensions
    w = 300
    h = 100
    background_color = (255, 255, 255)
    im_captcha = Image.new('RGB', (w, h), background_color)
    # equal width pieces for each code char
    w_code_char = int(math.floor(w/code_char_count))
    h_code_char = h

    ttf_file = os.path.join(captcha_fonts_path(), 'Arial.ttf')

    font_size = int(w_code_char * 3/4)
    draw_x = int(w_code_char/4)
    draw_y = int(h/4)

    # draw code chars one by one at different angle (with different font)
    w_code_char_offset = 0
    for code_char in code_chars:
        im_font = ImageFont.truetype(ttf_file, font_size)
        im_code_char = Image.new('RGB', (w_code_char, h_code_char), background_color)
        draw = ImageDraw.Draw(im_code_char)
        # see font size
        draw.text( (draw_x, draw_y), code_char, fill='black',  font=im_font)
        # random angle is between -20 - +20
        angle = random.randrange(40) - 20
        im_code_char_rotated = im_code_char.rotate(angle,  fillcolor=background_color)
        # paste char image into chars image
        im_captcha.paste(im_code_char_rotated, (w_code_char_offset, 0))
        w_code_char_offset += w_code_char

    # do some line / distortion stuff (to be implemented)

    # save (in memory) as jpg
    im_captcha_io = BytesIO()
    im_captcha.save(im_captcha_io, format='JPEG')

    captcha_code = ''.join(code_chars)
    captcha_image_data = im_captcha_io.getvalue()

    captcha_token = secrets.token_urlsafe()
    captcha_token_hashed = hash_string(captcha_token)

    return captcha_token, captcha_token_hashed, captcha_code, captcha_image_data

Um das Captcha-Bild mit Flask in Ihrem Browser anzuzeigen:

@auth.route('/captcha', methods=['GET'])
def captcha():

    captcha_token, captcha_token_hashed, captcha_code, captcha_image_data = captcha_create()

    resp = make_response(captcha_image_data)
    resp.content_type = "image/jpeg"
    return resp

Nun sollten wir auch das Captcha-Bild in einer Form anzeigen. Ich benutze Flask mit Flask-WTForms und habe ein Jinja-Makro, das alle Formularfelder auf der Seite platziert. Das spart so viel Zeit! Um das Captcha-Bild in die Form zu bringen, benötigen wir ein Widget. Wie macht man das? Ich bin sicherlich kein WTForms-Profi und glaube, es sollte viel mehr Beispiele für Widgets geben. Hier ist meine Lösung. In forms.py habe ich ein Widget für CaptchaCodeField erstellt, es tut nichts anderes, als das Bild auf den Bildschirm zu bringen:

class CaptchaCodeOutput(Input):
    def __call__(self, field, **kwargs):
        return kwargs.get('value', '<img src="' + field._value() + '" style="border: 1px solid #dddddd; ">')

class CaptchaCodeField(StringField):
    widget = CaptchaCodeOutput()
    

class AuthRegisterForm(FlaskForm):

    ....
    captcha_token = HiddenField('Captcha token')

    captcha_image_url = CaptchaCodeField(_l('Captcha image url'))

    captcha_code = StringField(_l('Code'), filters=[strip_whitespace], validators=[
        DataRequired()])

    accept_conditions = BooleanField(_l('I have read and accept the privacy policy *'),validators=[
        DataRequired()])

    submit = SubmitField(_l('Register'))

    def validate_password(self, field):
        password = field.data
        if not valid_password(password):
            raise ValidationError(valid_password_message())

Dann stopfe ich in views.py die Captcha-Bild-URL in dieses Feld, bevor ich render_template() aufrufe:

    ....
    captcha_image_url = url_for('auth.captcha', captcha_token=captcha_token)
    form.captcha_token.data = captcha_token
    form.captcha_image_url.data = captcha_image_url

    if captcha_error:
        flash(_l('The code you entered did not match the code from the image. Please try again.'))

    return render_template(
        'auth/register_step2.html', 
        form=form)

Ich denke, es ist immer noch die Mühe wert, ein Captcha auf Ihre Website zu setzen, wenn Ihre Website-Besucher nicht für etwas bezahlen müssen. Natürlich haben wir jetzt Deeplearning, aber die Captcha-Entwickler-Toolbox ist heute auch mehr gefüllt. Denke daran, verschiedene Schriften zu verwenden, verschiedene Breiten- und Höhenschriften zu verwenden, Verzerrungen hinzuzufügen, Farben umzukehren, kreativ zu sein, unvorhersehbar zu sein, lass uns ihre tief lernenden GPU's brennen! Sie können auch andere Captcha-Lösungen wie Q&A in Betracht ziehen.

ImportError: Das _imagingft C-Modul ist nicht installiert.

Natürlich ist bei der Verwendung von Kissen eine weitere schöne Fehlermeldung aufgetreten, siehe oben. Dies kann durch Hinzufügen von freetype, freetype-dev gelöst werden. Sie können dies überprüfen, indem Sie Ihren Container eingeben und durch Ausführen interaktiv werden:

phython3

und dann tippen:

from PIL import features
features.check('freetype2')

Wenn freetype installiert ist, gibt dies True zurück, wenn nicht installiert, False. Da ich Docker und das Alpine Image verwende, musste ich Änderungen an meiner Dockerdatei vornehmen:

FROM python:3.6-alpine as base
MAINTAINER Peter Mooring peterpm@xs4all.nl peter@petermooring.com

RUN mkdir /svc
WORKDIR /svc
COPY requirements.txt .

# install package dependencies
# COPY requirements.txt /requirements.txt, requirements.txt already copied 
# Solve 'No working compiler found' error, 
# see: https://github.com/gliderlabs/docker-alpine/issues/458
# Solve 'The headers or library files could not be found for jpeg, a required dependency when compiling Pillow from source.', 
# see https://blog.sneawo.com/blog/2017/09/07/how-to-install-pillow-psycopg-pylibmc-packages-in-pythonalpine-image/
# Solve: ImportError: The _imagingft C module is not installed (when using '.paste' of Pillow)
# Solved after adding freetype, freetype-dev


RUN rm -rf /var/cache/apk/* && \
    rm -rf /tmp/*

RUN apk update

# Instead, I run python setup.py bdist_wheel first, then run pip wheel -r requirements.txt for pypi packages.

RUN apk add --update \
    curl \
    python3 \ 
    pkgconfig \ 
    python3-dev \
    openssl-dev \ 
    libffi-dev \ 
    musl-dev \
    make \ 
    gcc \
    freetype \
    freetype-dev \
    jpeg-dev zlib-dev \
    libmagic \
    && rm -rf /var/cache/apk/* \
    && pip wheel -r requirements.txt --wheel-dir=/svc/wheels

# the wheels are now here: /svc/wheels

FROM python:3.6-alpine

RUN apk add --no-cache \
    freetype \
    freetype-dev \
    jpeg-dev zlib-dev \
    libmagic

COPY --from=base /svc /svc

WORKDIR /svc
RUN pip install --no-index --find-links=/svc/wheels -r requirements.txt

# after installation, remove wheels, does not free up space, probably because we are in new layer, too bad is some 20MB
#RUN rm -R *

# create and set working directory
RUN mkdir -p /home/flask/app/web
WORKDIR /home/flask/app/web

# copy app code into container
COPY . ./

# create group and user used in this container
RUN addgroup flaskgroup && adduser -D flaskuser -G flaskgroup && chown -R flaskuser:flaskgroup /home/flask

USER flaskuser

Links / Impressum

How I developed a captcha cracker for my University's website
https://dev.to/presto412/how-i-cracked-the-captcha-on-my-universitys-website-237j

How to break a CAPTCHA system in 15 minutes with Machine Learning
https://medium.com/@ageitgey/how-to-break-a-captcha-system-in-15-minutes-with-machine-learning-dbebb035a710

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.