Ataque por diccionario usando UTesla y Erica

Iniciado por DtxdF, Julio 10, 2020, 07:13:14 AM

Tema anterior - Siguiente tema

0 Miembros y 3 Visitantes están viendo este tema.

Un ataque por diccionario es considerado un tipo de ataque contra una contraseña (o una palabra que cause validez) usando una lista de posibles contraseñas, mayormente dado en un fichero de texto o una base de datos gigante. El ataque por diccionario (muchas veces confundido con fuerza bruta) usa una gran lista de contraseñas que pueden ser generadas por un generador automatizado, preferencias de la víctima, o incluso combinarse con la mismísima fuerza bruta para lograr el cometido.


Para saber más sobre Ultra Tesla pueden leer el No tienes permitido ver los links. Registrarse o Entrar a mi cuenta que hace algunos momentos lo he actualizado para que coincida con el No tienes permitido ver los links. Registrarse o Entrar a mi cuenta del repositorio. Sin embargo es recomendable que se haga una lectura del repositorio oficial ya que puede haber un cambio reciente que no esté en algún artículo de Internet.

Erica es un script para realizar un ataque por diccionario contra numerosas funciones Hash creado por nuestro compañero @No tienes permitido ver los links. Registrarse o Entrar a mi cuenta. Para esta ocasión nosotros adaptaremos Erica para usarlo como un simple servicio en UTesla. Si quieres leer un tutorial sobre el script puedes leer un No tienes permitido ver los links. Registrarse o Entrar a mi cuenta

Cabe aclarar que UTesla no está diseñado para soportar grandes cantidades de datos, pese a que se esté trabajando para mejorarlo cada día, uno de los muchos objetivos que se tienen en mente, es realizar de una forma diferente, APIs Webs siguiendo un formato no convencional. Para esta demostración usaremos un diccionario pequeño y unas pequeñas configuraciones para que funcione eficientemente


Esquema de la red:

Para este escenario se usarán tres máquinas, pero para que se tenga más rendimiento el plugin 'Erica' contiene un pequeño algoritmo que disminuye la carga "partiendo" el diccionario en mitades de longitud similar para cada parte (las partes son los servidores), incluso el límite de las líneas es un factor a tener en cuenta. Claro que todo esto estará automatizado por un plugin de UTeslaCLI

Comencemos:

Antes que nada, es importante que tengan actualizado (si es que lo tienen instalado, si no, pueden descargarlo) UTesla porque se han hecho varias actualizaciones arreglando errores y mejorandolo. Por lo que si han usado git, simplemente ejecuten:

Código: bash
git pull origin master
git submodule update --remote


O pueden descargarlo nuevamente, no tendrán que reinstalar las dependencias, así que será menos pesado  ;)

También es importante que usen la versión No tienes permitido ver los links. Registrarse o Entrar a mi cuenta de python, porque el plugin estará usando algunas nuevas sintaxis de ésta.

Si los repositorios de su distribución no tiene incluido la versión 3.8 de python, pueden No tienes permitido ver los links. Registrarse o Entrar a mi cuenta.

Es muy recomendable configurar las claves 'connect_timeout' y 'request_timeout' en la sección 'Client' del fichero 'config/UTesla.ini' según lo que se puede tardar, lo cual es relativo a la velocidad de conexión, la velocidad de procesamiento, la memoria, las líneas del archivos, entre otros.


Instalación del plugin 'Erica':

Debemos comenzar con un pequeño fichero para empezar a crear la estructura, el cual estará ubicado en 'complements/erica.py' con el siguiente contenido:

complements/erica.py:

Código: python
class Handler:
    pass


Ahora la estructura puede verse como '<Esquema>://<Dirección IP/Host>:<Puerto>/erica'

Por consiguiente necesitamos crear sub-servicios para organizar mejor el esquema, éso se hace creando la siguiente carpeta:


Código: bash
mkdir complements/erica-folder


Ahí podemos crear nuevos servicios que se verán reflejados como lo siguiente '<Esquema>://<Dirección IP/Host>:<Puerto>/erica/<Sub-servicio>'. Por ejemplo:

complements/erica-folder/get_job.py:

Código: python
import os
import tempfile
import secrets

class Handler:
    async def get(self):
        id = secrets.token_hex(16)
        workspace = '%s/erica_%s' % (
            tempfile.gettempdir(), id

        )

        os.makedirs(workspace, exist_ok=True)
       
        await self.write(id)


complements/erica-folder/write.py:
Código: python
import tempfile
import os

class Handler:
    async def post(self):
        wordlist = self.get_argument('wordlist')
        hash = self.get_argument('hash')
        hash_type = self.get_argument('hash_type')
        shake_length = str(self.get_argument('shake_length'))
        id = self.get_query_argument('id')
       
        workspace = '%s/erica_%s' % (
            tempfile.gettempdir(), id
       
        )
        hash_filename = '%s/hash.txt' % (workspace)
        hash_type_filename = '%s/hash_type.txt' % (workspace)
        wordlist_filename = '%s/wordlist.lst' % (workspace)
        shake_length_filename = '%s/shake_length.txt' % (workspace)
       
        response = {
            'error'   : False,
            'message' : None
               
        }

        if (os.path.isdir(workspace)):
            if not (os.path.isfile(hash_filename)):
                with open(hash_filename, 'w') as fd:
                    fd.write(hash)
                    fd.flush()
                    os.fsync(fd)

            if not (os.path.isfile(hash_type)):
                with open(hash_type_filename, 'w') as fd:
                    fd.write(hash_type)
                    fd.flush()
                    os.fsync(fd)

            if not (os.path.isfile(shake_length_filename)):
                with open(shake_length_filename, 'w') as fd:
                    fd.write(shake_length)
                    fd.flush()
                    os.fsync(fd)

            with open(wordlist_filename, 'a', buffering=1) as fd:
                fd.writelines(wordlist)
                os.fsync(fd)

        else:
            response['error'] = True
            response['message'] = 'El identificador de trabajo "%s" no existe' % (id)

        await self.write(response)


complements/erica-folder/crack.py:
Código: python
import tempfile
import os
import hashlib

def set_status(status, workspace):
    with open('%s/status.txt' % (workspace), 'w') as fd:
        fd.write(status)

def crack(hash, hash_type, wordlist, workspace, shake_length):
    result = False

    if not (hash_type in hashlib.algorithms_guaranteed):
        set_status('No se encuentra la función "%s"' % (hash_type), workspace)
        return

    set_status('running', workspace)

    with open(wordlist, 'r') as fd:
        for word in fd:
            word = word.strip()
            hashfunc = getattr(hashlib, hash_type)(word.encode())

            if (hash_type[:5] == 'shake'):
                hash2cmp = hashfunc.hexdigest(shake_length)

            else:
                hash2cmp = hashfunc.hexdigest()

            if (hash == hash2cmp):
                result = True
                break

    if (result):
        set_status('success:%s' % (word), workspace)

    else:
        set_status('fail', workspace)

class Handler:
    async def get(self):
        id = self.get_query_argument('id')
       
        workspace = '%s/erica_%s' % (
            tempfile.gettempdir(), id

        )
        hash_filename = '%s/hash.txt' % (workspace)
        hash_type_filename = '%s/hash_type.txt' % (workspace)
        wordlist_filename = '%s/wordlist.lst' % (workspace)
        shake_length_filename = '%s/shake_length.txt' % (workspace)

        response = {
            'error'   : False,
            'message' : None
               
        }

        if (os.path.isfile(hash_filename) and os.path.isfile(hash_type_filename) and os.path.isfile(wordlist_filename) and os.path.isfile(shake_length_filename)):
            with open(hash_filename, 'r') as fd:
                hash = fd.read().strip()

            with open(hash_type_filename, 'r') as fd:
                hash_type = fd.read().strip()

            with open(shake_length_filename, 'r') as fd:
                shake_length = int(fd.read())

            self.procs.create(crack, args=(hash, hash_type, wordlist_filename, workspace, shake_length))

        else:
            response['error'] = True
            response['message'] = 'Hay una incongruencia con los archivos del espacio de trabajo'

        await self.write(response)


complements/erica-folder/status.py:
Código: python
import os
import tempfile

class Handler:
    async def get(self):
        id = self.get_query_argument('id')

        workspace = '%s/erica_%s' % (
            tempfile.gettempdir(), id
           
        )
        response = {
            'error'   : False,
            'message' : None
               
        }

        status_file = '%s/status.txt' % (workspace)

        if (os.path.isfile(status_file)):
            with open(status_file, 'r') as fd:
                response['message'] = fd.read().strip()

        else:
            response['error'] = True
            response['message'] = 'No se puede leer el estado de la operación'

        await self.write(response)


Todos los servicios anteriormente mostrados irían en cada servidor que se desee usar para realizar el ataque por diccionario, quedando de la siguiente forma:

* - <Esquema>://<Dirección IP/Host>:<Puerto>/erica/get_job
* - <Esquema>://<Dirección IP/Host>:<Puerto>/erica/write
* - <Esquema>://<Dirección IP/Host>:<Puerto>/erica/crack
* - <Esquema>://<Dirección IP/Host>:<Puerto>/erica/status

Si parece tedioso repartir cada servicio para cada servidor, hay distintas modalidades. Se puede usar el comando No tienes permitido ver los links. Registrarse o Entrar a mi cuenta para transportar los archivos facilmente, usar un USB al modo tradicional o usar el módulo de python 'http.server' el cual crearía un pequeño servidor web local según donde lo hayamos ejecutado.

Como último archivo que tendría que estar ubicado sólo en nuestra máquina local, sería el cliente para comunicarse con los servicios mostrados anteriormente.

modules/Cmd/erica.py:

Código: python
import os
import sys
import asyncio
import logging
import hashlib
import tempfile
import sqlite3
import multiprocessing

from modules.Infrastructure import client
from utils.General import parse_config
from utils.extra import create_pool

conf = parse_config.parse()
client_conf = conf['Client']
server_conf = conf['Server']
username = server_conf['user_server']
public_key = server_conf['pub_key']
private_key = server_conf['priv_key']
client.config = client_conf
db_name = '%s/erica.db' % (tempfile.gettempdir())
tmp_wordlist = '%s/erica-wordlist.lst' % (tempfile.gettempdir())

information = {
    'description' : 'Realizar un ataque por diccionario',
    'commands'    : [
        {
            'argumentos principales' : [
                {
                    'args'     : ('-o', '--option'),
                    'help'     : 'El comando a ejecutar en el servidor',
                    'choices'  : ('get_job', 'write', 'crack', 'crack_local', 'status', 'reset'),
                    'default'  : 'get_job'

                }


            ]

        },
       
        {
            'argumentos requeridos por el comando \'write\'' : [
                {
                    'args' : ('-w', '--wordlist'),
                    'help' : 'La lista de palabras a usar'

                }

            ]

        },

        {
            'argumentos requeridos por el comando \'write\' y \'crack_local\'' : [
                {
                    'args'     : ('-H', '--hash'),
                    'help'     : 'La suma de verificación a atacar'

                },

                {
                    'args'     : ('-t', '--hash-type'),
                    'help'     : 'La función hash a usar'

                }

            ]

            },

        {
            'optionals'       : [
                {
                    'args'    : ('-l', '--lines'),
                    'help'    : 'Dividir el archivo en N líneas para cada servidor',
                    'default' : 1000000,
                    'type'    : int

                },

                {
                    'args'    : ('-i', '--interval'),
                    'help'    : 'El intervalo en segundos para comprobar si el trabajo a concluido',
                    'type'    : int,
                    'default' : 15

                },

                {
                    'args'    : ('-m', '--max-workers'),
                    'help'    : 'El máximo de procesos trabajando en la operación localmente',
                    'default' : multiprocessing.cpu_count(),
                    'type'    : int

                },

                {
                    'args'    : ('-L', '--shake-length'),
                    'help'    : 'La longitud del resultado en la función shake y derivados',
                    'default' : 32,
                    'type'    : int

                }

            ]

        }

    ],
    'version'     : '1.0.0'

}

def get_lines(filename):
    count = 0

    with open(filename, 'r') as fd:
        while (line := fd.readline()):
            count += 1

    return count

def crack(args):
    (wordlist, hash, hash_type) = args
    func = getattr(hashlib, hash_type)

    with open(wordlist, 'r') as fd:
        for i in fd:
            i = i.strip()
            hash2cmp = func(i.encode()).hexdigest()

            if (hash2cmp == hash):
                logging.info('Operación concluida localmente: %s (%s(%s))',
                             hash, hash_type.upper(), i)
                return True

        logging.warning('No se pudo satisfacer la operación localmente')

        return False

def chunk(fd, lines, parts):
    chunks = []

    index = 0
    count = 0

    for i in range(parts):
        chunks.append([])

    while (line := fd.readline()):
        if not (line.strip()):
            continue

        if (index == parts):
            index = 0

            yield chunks

            for i in range(parts):
                chunks[i] = []

        chunks[index].append(line)
           
        if (count == lines):
            index += 1
            count = 0

            continue
           
        count += 1

    if (chunks != []):
        yield chunks

def execute_sql_command(cmd, args=(), write=False):
    with sqlite3.connect(db_name) as sqlite_db:
        cursor = sqlite_db.cursor()
        cursor.execute('CREATE TABLE IF NOT EXISTS jobs(job VARCHAR(32) NOT NULL, network TEXT NOT NULL)')
        sqlite_db.commit()

        cursor.execute(cmd, args)

        if (write):
            sqlite_db.commit()

        else:
            return cursor.fetchall()

def getJob(net):
    result = execute_sql_command(
        'SELECT job FROM jobs WHERE network = ? LIMIT 1', (net,)

    )

    if (result != []):
        return result[0][0]

def writeControl(net, response):
    error = response['error']
    message = response['message']

    if (error):
        logging.warning('Ocurrió un error interno en el servidor "%s": %s', net, message)

    else:
        logging.info('Se ha escrito una parte del diccionario en el servidor %s', net)

def jobsControl(net, response):
    execute_sql_command('INSERT INTO jobs(job, network) VALUES (?, ?)', (response, net), True)

    logging.info('El trabajo "%s" se creó en el servidor "%s"', response, net)

def crackControl(net, response):
    error = response['error']
    message = response['message']

    if (error):
        logging.warning('Ocurrió un error interno en el servidor "%s": %s', net, message)

    else:
        logging.info('Se ha iniciado el ataque en el servidor "%s" 3:-)', net)

def statusControl(net, response):
    error = response['error']
    message = response['message']

    if (error):
        logging.warning('Ocurrió un error interno en el servidor "%s": %s', net, message)

    else:
        if (message == 'running'):
            logging.warning('La operación todavía se sigue efectuando')

        elif (message[:7] == 'success'):
            logging.info('Operación realizada con éxito, se ha obtenido la palabra correcta en la lista de palabras almacenada en el servidor "%s": %s', net, message[8:])

        elif (message == 'fail'):
            logging.warning('No se pudo obtener la palabra con éxito usando la lista de palabras almacenada en el servidor "%s" :-(', net)

        else:
            logging.warning('Estado desconocido en el servidor "%s": %s', net, message)

async def execute_command(net, cmd, data, callback, *args, method='POST', **kwargs):
    url = '%s/erica/%s' % (net, cmd)
    url_hash = hashlib.sha3_224(net.encode()).hexdigest()
    pub_key_file = '%s/servkeys/%s' % (
        server_conf['init_path'], url_hash
   
    )

    UClient = client.UTeslaClient(username)

    await UClient.set_user_keys(public_key, private_key)
    await UClient.set_server_key(pub_key_file)
   
    try:
        result = await UClient.fetch(
            url, data, method=method

        )

    except Exception as err:
        logging.warning('Ocurrió en la petición hacia el servidor "%s": %s', url, err)

        response = None

    else:
        response = UClient.get_message(result.body)

    if (response):
        return callback(net, response, *args, **kwargs)

    else:
        logging.warning('No se obtuvo ningún dato por parte del servidor %s...', net)

async def MainParser(args):
    hash = args.hash
    hash_type = args.hash_type
    wordlist = args.wordlist
    lines = args.lines
    interval = args.interval
    max_workers = args.max_workers
    option = args.option
    shake_length = args.shake_length

    db = await create_pool.create(server_conf['mysql_db'])
    networks = await db.get_networks(show_all=True)
    jobs = []
   
    if (option == 'get_job') and (os.path.isfile(db_name)):
        logging.warning('No se puede obtener nuevos trabajas hasta que se haga un reinicio para evitar alteraciones. Use el comando \'reset\' para permitir esta operación')
        return

    if (wordlist is None) and (option == 'write'):
        logging.warning('¡No se definió la ruta del diccionario a usar!')
        return

    if (hash is None or hash_type is None) and (option == 'write' or option == 'crack_local'):
        logging.warning('Falta definir el \'hash\' y/o la función a usar')
        return

    if (option == 'get_job' or option == 'status' or option == 'crack'):
        callbacks = {
            'get_job' : jobsControl,
            'crack'   : crackControl,
            'status'  : statusControl

        }

        for _, net, token in networks:
            data = {
                'token' : token

            }

            job = getJob(net)

            params = {
                'crack'   : '?id=%s' % (job),
                'status'  : '?id=%s' % (job)

            }

            jobs.append(
                execute_command(net, option + params.get(option, ''), data, callbacks[option], method='GET')

            )

    elif (option == 'write'):
        local_machine = '<Local Machine>'
        networks = networks + ((None, local_machine, None),)
        parts = len(networks)

        with open(wordlist, 'r') as fd:
            with open(tmp_wordlist, 'w', buffering=1) as tmp_fd:
                for words in chunk(fd, lines, parts):
                    for (_, net, token), chunk_val in zip(networks, words):
                        if (net == local_machine):
                            tmp_fd.writelines(chunk_val)

                        else:
                            data = {
                                'token'     : token,
                                'body'      : {
                                    'wordlist'  : chunk_val,
                                    'hash'      : hash,
                                    'hash_type' : hash_type,
                                    'shake_length'    : shake_length

                                }

                            }

                            cmd = 'write?id=%s' % (
                                getJob(net)

                            )

                            logging.warning('Escribiendo %d líneas en %s...', len(chunk_val)-1, net)
                            await execute_command(net, cmd, data, writeControl)

    elif (option == 'crack_local'):
        processes = multiprocessing.Pool(max_workers)
        processes.map(crack, [(tmp_wordlist, hash, hash_type)])
        processes.close()
        processes.join()

    elif (option == 'reset'):
        files2del = [
            tmp_wordlist,
            db_name

        ]

        for file in files2del:
            if (os.path.isfile(file)):
                os.remove(file)
                logging.warning('¡%s se ha eliminado!', file)

    await asyncio.gather(*jobs)


Preparando el escenario:

Para comenzar, necesitamos registrarnos en los diversos servidores que deseemos usar, pero la peculiaridad de todo esto es que usaremos el nombre de usuario de nuestro servidor local mas las claves de éste. Como se viene viendo en el No tienes permitido ver los links. Registrarse o Entrar a mi cuenta de UTesla, para generar el par de claves del servidor se usaría el siguiente comando:

Código: bash
./UTesla
# Si se desea usar la versión 3.8 de python explícitamente
# python3.8 ./UTesla



Mi par de claves RSA O:)

Aquí lo importante es la clave pública que tendrá que ser transportada en los múltiples servidores que nos registraremos. En las anteriores secciones se explicó cómo transportar dichos archivos.

Una vez estamos en los otros servidores necesitamos ejecutar el siguiente comando para registrarnos:


Código: bash
./UTeslaCLI add -u utesla -p uteslapasswd123@ -i /tmp/key.pub



Creando el usuario 'utesla' en Debian Buster


Creando el usuario 'utesla' en AntiX

Una vez se ha registrado el usuario correspondiente sin problemas en los distintos servidores de nuestra red, pasaremos a generar el token de acceso para agregar los servidores a posteriori.


Agregando 'No tienes permitido ver los links. Registrarse o Entrar a mi cuenta' (Debian Buster) a la red


Agregando 'No tienes permitido ver los links. Registrarse o Entrar a mi cuenta' (AntiX) a la red

Iniciando el ataque:

Una vez agregada las redes empecemos con el ataque, para ello simplemente ejecutemos:

Código: bash
./UTeslaCLI erica
./UTeslaCLI erica -w /tmp/wordlist.lst -t md5 -H 9d2b9e517b4c13d92a91472d1d1d4acb -o write -l 30000



Escribiendo un diccionario de 14MB en los servidores


El resultado de haber obtenido la palabra 'dtxdf' a partir de '9d2b9e517b4c13d92a91472d1d1d4acb'

Como ya hizo saber, el diccionario será dividido según las partes y el número de líneas proporcionado, en este se dividirá en tres partes; dos son de los servidores y la tercera es local, almacenada como '/tmp/erica-wordlist.lst'. Si los servidores no pudieron satisfacer la operación, se puede probar el ataque de manera local:


Intento por realizar el ataque de manera local

Como se puede observar se intentó realizar el ataque de manera local con el diccionario dividido, pero fue un intento fallido, aunque lo bueno es que el primer servidor (Debian Buster) si logró satisfacer la operación.

Notas:

* El contenido está hecho con fines demostrativos, en entornos reales y mejor preparados se usaría un cluster con buena maquinaria (en un futuro quizá haga un tutorial 3:-D)
* Tengo que aclarar nuevamente que UTesla aún no está diseñado para transportar grandes cantidades de datos eficientemente, se realizó un pequeño protocolo entre plugin/servicios para enviar por partes el archivo. Ya se está preparando la mejora para enviar cargas grandes con un buen rendimiento
* Espero hayan disfrutado y aprendido tanto como yo lo hice :D


~ DtxdF
PGP :: <D82F366940155CB043147178C4E075FC4403BDDC>

~ DtxdF

Buenisimo compañero.
Ya decia yo, cuando se viene lo que todos pensamos: cracking distribuido  ;D.

Felicidades por tu proyecto @No tienes permitido ver los links. Registrarse o Entrar a mi cuenta esta muy genial. UTesla sera la base de mas proyectos geniales como este.

Mis respetos, saludos.
Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn


  @No tienes permitido ver los links. Registrarse o Entrar a mi cuenta

Como te han comentado antes: felicitaciones!!

Cuando se lee tu proyecto, la claridad de exposición, los comentarios/documentación y la transparencia de un código limpio, únicamente queda expresar, no solo el agradecimiento porque lo compartes sino mi profunda admiración.

Saludos

Gabriela

Tú te enamoraste de mi valentía, yo me enamoré de tu oscuridad; tú aprendiste a vencer tus miedos, yo aprendí a no perderme en tu abismo.

Muchísimas gracias a los dos (@No tienes permitido ver los links. Registrarse o Entrar a mi cuenta y @No tienes permitido ver los links. Registrarse o Entrar a mi cuenta) y a todos los lectores, será un placer seguir continuando con esta temática ^-^

~ DtxdF
PGP :: <D82F366940155CB043147178C4E075FC4403BDDC>

~ DtxdF

Me ha gustado mucho como has implementado mi viejo script en tu gran proyecto, sin duda la idea del cracking distribuido resulta grandiosa. Nuevamente felicitaciones amigo por todos estos grandes aportes  ;D ;D ;D

Ya tendremos esa GUI esperada, un poco de paciencia  :D

Saludos!
-Kirari

Gracias compañero @No tienes permitido ver los links. Registrarse o Entrar a mi cuenta, es un placer :'D

~ DtxdF
PGP :: <D82F366940155CB043147178C4E075FC4403BDDC>

~ DtxdF

Muy bien hecho el post, y toda una joyita.
Muy bueno @No tienes permitido ver los links. Registrarse o Entrar a mi cuenta.
No tienes permitido ver los links. Registrarse o Entrar a mi cuenta

Muchísimas gracias @No tienes permitido ver los links. Registrarse o Entrar a mi cuenta, será un placer seguir aportando estos temas ^-^

~ DtxdF
PGP :: <D82F366940155CB043147178C4E075FC4403BDDC>

~ DtxdF

hola amigo que tal, estoy probando tus ataques con mis hosting pero estoy en la duda, para hacer que conecten los ataques a la pagina del login del host, como lo hago? porque como sale ahi es con ip.

No tienes permitido ver los links. Registrarse o Entrar a mi cuenta
hola amigo que tal, estoy probando tus ataques con mis hosting pero estoy en la duda, para hacer que conecten los ataques a la pagina del login del host, como lo hago? porque como sale ahi es con ip.

Hola Scooby. No sé exactamente lo que tratas de hacer, si pudieras describir mejor tu duda, con gusto la resolveríamos. Recuerda que el ataque por diccionario era contra un hash y no contra un panel web o de la misma índole.

También te recomiendo veas la nueva versión: No tienes permitido ver los links. Registrarse o Entrar a mi cuenta

Estaré al corriente de tu duda.

~ DtxdF
PGP :: <D82F366940155CB043147178C4E075FC4403BDDC>

~ DtxdF