Inyector de DLL con interfaz gráfica

Versión: 3.x.
Descargas: código de fuente, archivo ejecutable.

Código de fuente de un inyector de DLL de 32-bit para Windows. Incluye una lista de procesos con ruta completa (modificable) e íconos de los respectivos procesos. Utiliza intensivamente ctypes para acceder a la API de Windows, Pillow (PIL) para soportar transparencia en los íconos de la lista de procesos y Tcl/Tk (tkinter) para la interfaz gráfica.

Consta únicamente de dos archivos: main.py, archivo principal, e injector.py, módulo encargado de obtener la lista de procesos e inyectar archivos DLL (también disponible para Python 2 en este enlace).

Vista previa del archivo principal (descarga del código completo al principio del artículo):

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk, messagebox, PhotoImage
from tkinter.filedialog import askopenfilename
from tkinter.font import Font, nametofont
from ctypes import byref, memset, sizeof, windll, Structure
from ctypes import create_string_buffer, create_unicode_buffer
from ctypes import c_int, c_void_p
from ctypes.wintypes import WORD, DWORD, BYTE, BOOL, LONG, HBITMAP
from traceback import print_exc
from base64 import b64decode

from PIL import Image, ImageDraw, ImageTk

from injector import (OpenProcess, CloseHandle, get_processes_list,
                      inject_library)


# Funciones, constantes y estructuras de la API de Windows.
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
BI_RGB = 0
DIB_RGB_COLORS = 0

QueryFullProcessImageName = windll.kernel32.QueryFullProcessImageNameW
ExtractIconEx = windll.shell32.ExtractIconExW
DestroyIcon = windll.user32.DestroyIcon
GetIconInfo = windll.user32.GetIconInfo
GetDC = windll.user32.GetDC
ReleaseDC = windll.user32.ReleaseDC
DeleteDC = windll.gdi32.DeleteDC
SelectObject = windll.gdi32.SelectObject
CreateCompatibleDC = windll.gdi32.CreateCompatibleDC
GetPixel = windll.gdi32.GetPixel
GetDIBits = windll.gdi32.GetDIBits

# Imagen por defecto para procesos sin íconos, codificada vía base64
# para distribuir sin dependencias.
PROCESS_DEFAULT_ICON = b64decode(b'''\
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9\
iZSBJbWFnZVJlYWR5ccllPAAAAsVJREFUeNpcU91LFFEU/83Ox864u64flfaBlFGgCRERYQ\
k+WBFERdZDf0AQQmAf70Fvvkg9BIIg9BAUZEqIESVCL0KiiA89GJUgGoruurgzO1937u3cc\
XexBn7Mvff8fuece+45ihACL8Z+47+vi9BH6CQcI6wQZglDhOmHva1VoiIdPH9fddBAeGpa\
Rn/D/iz0pAFVV8FZhNAPkN/cgVvyXxJn4NHt1rWqg8HRX3KdVRQUGg/UwcpkUHI5uFCgJAD\
BiagIpCwVvl3E5kYBpGt9cuf4ckIqORcSz+obs0gYaRSKERyPw3YZLuz7A7vEUKJ9wWYQWg\
oyCPEfS23sIOLoonT7VTNN5AhnG7dQp7mIGIdlWoiIkKV9RzZHjhmgp5C0zAcDb3/e2s1Ai\
L7a+lrsUGTPZ1jKp3HxaICmFINu6PG/s8XH0lYKnhsRj0HypU7bzUB0R9ARhBEYRVvdVjAj\
TPS0KbAMA90nGD59t5BzFBlO3h9hUpe6zoqDw2FIBRMMd0/bMEhkmmYMVVWh6wy9Zzz4voc\
gCPBmIQ0/0CCAdOyAR1gPQ9bs+gKv52ri9zzSwHHzXAIZQ4MfAhOLAqt5SdfiLKJIgIXCLR\
dRTIeBT6nRm9MVmjMRLrUreDdTpOqHGPtWxJVTCTSleWznPAEWeLGu4mDYsW2oWiKueMfBC\
B/mAqzlQiqqh+WNAJMLPjoOsdiuahpKjiN1I5VX+Oo47ghnJSQ0C5OLCta3ZTQqVhDG/5VN\
jo90Lu3gHopFZ3zgXtt4pQ8khvK5bTK60JI1UFQ9rvbQZ486kcd7eS7F+Vwu5lcbqdyJ85T\
eeTK+cu0ctS49rFkDjZpLt9JQqHCekyfx1ihjvGfwfvuX6ixMTU39M4oTP5qvlafxMsGQSR\
KkYPj6yfXxvVxtz7q9AiK1l8fYKNtUwlVCC+EGYb483rN/BRgAM8iDXy98gZ8AAAAASUVOR\
K5CYII=''')

class ICONINFO(Structure):
    _fields_ = [
        ("fIcon", BOOL),
        ("xHotspot", DWORD),
        ("yHotspot", DWORD),
        ("hbmMask", HBITMAP),
        ("hbmColor", HBITMAP)
    ]


class RGBQUAD(Structure):
    _fields_ = [
        ("rgbBlue", BYTE),
        ("rgbGreen", BYTE),
        ("rgbRed", BYTE),
        ("rgbReserved", BYTE),
    ]


class BITMAPINFOHEADER(Structure):
    _fields_ = [
        ("biSize", DWORD),
        ("biWidth", LONG),
        ("biHeight", LONG),
        ("biPlanes", WORD),
        ("biBitCount", WORD),
        ("biCompression", DWORD),
        ("biSizeImage", DWORD),
        ("biXPelsPerMeter", LONG),
        ("biYPelsPerMeter", LONG),
        ("biClrUsed", DWORD),
        ("biClrImportant", DWORD)
    ]


class BITMAPINFO(Structure):
    _fields_ = [
        ("bmiHeader", BITMAPINFOHEADER),
        ("bmiColors", RGBQUAD * 1),
    ]


class Checkbox(ttk.Checkbutton):
    """
    Una implementación más agradable alrededor de ttk.Checkbutton.
    """
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.variable = tk.IntVar(self)
        self.configure(variable=self.variable)
    
    def checked(self):
        return bool(self.variable.get())
    
    def check(self):
        self.variable.set(1)
    
    def uncheck(self):
        self.variable.set(0)


class Linkbutton(ttk.Button):
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Obtener el nombre de la fuente utilizada para ttk.Label.
        label_font = nametofont("TkDefaultFont").cget("family")
        self.font = Font(family=label_font, size=9)
        
        # Crear un nuevo estilo.
        style = ttk.Style()
        style.configure(
            "Link.TLabel", foreground="#357fde", font=self.font)
        
        # Aplicarlo al enlace.
        self.configure(style="Link.TLabel", cursor="hand2")
        
        # Configurar eventos.
        self.bind("<Enter>", self.on_mouse_enter)
        self.bind("<Leave>", self.on_mouse_leave)
    
    def on_mouse_enter(self, event):
        # Aplicar subrayado.
        self.font.configure(underline=True)
    
    def on_mouse_leave(self, event):
        # Quitar subrayado.
        self.font.configure(underline=False)


def bgra_to_rgba(data):
    """Convertir de formato BGRA (Windows) a RGBA (Pillow)."""
    mutable_data = bytearray(data)
    for i in range(0, len(data), 4):
        # Obtener los valores originales de rojo y azul.
        b = data[i]
        r = data[i + 2]
        # Intercambiarlos.
        mutable_data[i] = ord(r)
        mutable_data[i + 2] = ord(b)
    # Convertir nuevamente a un tipo inmutable.
    return bytes(mutable_data)


class Application(ttk.Frame):
    
    def __init__(self, main_window):
        super().__init__(main_window)
        main_window.title("Inyector de DLL")
        
        self.filename = None
        self.processes = {}
        self.icons = {}
        
        # Expandir automáticamente los controles.
        main_window.rowconfigure(0, weight=1, minsize=310)
        main_window.columnconfigure(0, weight=1, minsize=455)
        self.grid(row=0, column=0, sticky="nsew")
        
        self.create_widgets()
    
    def create_widgets(self):
        """Crear todos los controles."""
        self.lbl_filename = ttk.Label(
            self, text="No se ha seleccionado ningún archivo.")
        self.lbl_filename.grid(
            row=0, column=0, padx=5, pady=5, sticky="nsew")
        
        self.default_icon = tk.PhotoImage(data=PROCESS_DEFAULT_ICON)
        
        self.btn_browse = ttk.Button(
            self, text="Examinar", command=self.browse)
        self.btn_browse.grid(
            row=0, column=1, padx=5, pady=5, sticky="nsew")
        
        self.tree_processes = ttk.Treeview(
            self, columns=("pid",), selectmode=tk.BROWSE)
        self.tree_processes.heading("#0", text="Nombre")
        self.tree_processes.heading("pid", text="PID")
        self.tree_processes.column("pid", width=70, stretch=False)
        self.tree_processes.grid(
            row=1, column=0, columnspan=2, padx=5, sticky="nsew")
        
        # Crear una barra de deslizamiento para la lista de procesos.
        scrollbar = ttk.Scrollbar(
            self.tree_processes, orient=tk.VERTICAL)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # Añadirla.
        self.tree_processes.config(yscrollcommand=scrollbar.set)
        scrollbar.config(command=self.tree_processes.yview)
        
        self.chk_fullpath = Checkbox(self,
            text="Ruta completa", command=self.populate_processes_list)
        self.chk_fullpath.check()
        self.chk_fullpath.grid(
            row=2, column=0, padx=5, pady=5, sticky="ew")
        
        self.lnk_refresh = Linkbutton(self,
            text="Actualizar lista", command=self.refresh_processes)
        self.lnk_refresh.grid(
            row=2, column=1, padx=5)
        
        self.btn_inject = ttk.Button(self,
            text="Inyectar", command=self.inject)
        self.btn_inject.grid(
            row=3, column=0, columnspan=2, sticky="ew", pady=5, padx=5)
        
        # Expansión automática.
        self.rowconfigure(1, weight=1)
        self.columnconfigure(0, weight=1)
        
        # Obtener los procesos por primera vez.
        self.refresh_processes()
    
    def inject(self):
        """Determinar que el archivo y el proceso seleccionados sean
        correctos y, en caso afirmativo, proceder con la inyección.
        """
        if not self.filename:
            messagebox.showinfo("Inyector de DLL",
                "Debes seleccionar un archivo.")
            return
        
        selection = self.tree_processes.selection()
        if not selection:
            messagebox.showinfo("Inyector de DLL",
                "Debes seleccionar un proceso de la lista.")
        else:
            item = self.tree_processes.item(selection[0])
            pid = item["values"][0]
            try:
                inject_library(bytes(self.filename, "utf-8"), pid)
            except RuntimeError:
                messagebox.showerror("Operación fallida",
                    "No se ha podido inyectar el archivo.")
                # Registrar el error completo en un archivo.
                with open("dllinjector.log", "a") as f:
                    print_exc(file=f)
            else:
                messagebox.showinfo("Operación exitosa",
                    "El archivo ha sido inyectado correctamente.")
    
    def load_processes(self):
        """Obtener información de los procesos."""
        self.processes = {}
        
        dc = CreateCompatibleDC(0)
        if dc == 0:
            raise RuntimeError("Failed while creating DC.")
        
        for proc_name, pid in get_processes_list():
            # Obtener acceso al proceso.
            proc_handle = OpenProcess(
                PROCESS_QUERY_LIMITED_INFORMATION, False, pid
            )
            
            # Almacenar un objeto para guardar la ruta completa del
            # proceso.
            wbuffer = create_unicode_buffer(500)
            size = c_int(500)
            
            full_path = QueryFullProcessImageName(
                proc_handle, 0, byref(wbuffer), byref(size)
            )
            # Los archivos del sistema no tienen una ruta completa.
            if full_path:
                proc_name = wbuffer.value
            else:
                proc_name = proc_name.decode("utf-8")
            
            CloseHandle(proc_handle)
            
            # Obtener el ícono del proceso.
            icon = c_int(0)
            ExtractIconEx(proc_name, 0, None, byref(icon), 1)
            
            if icon.value > 0:
                icon_info = ICONINFO(0, 0, 0, 0, 0)
                
                if GetIconInfo(icon, byref(icon_info)):
                    # Tamaño.
                    w, h = 16, 16
                    
                    bmi = BITMAPINFO()
                    memset(byref(bmi), 0, sizeof(bmi))
                    bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER)
                    bmi.bmiHeader.biWidth = w;
                    bmi.bmiHeader.biHeight = -h;
                    bmi.bmiHeader.biPlanes = 1;
                    bmi.bmiHeader.biBitCount = 32;
                    bmi.bmiHeader.biCompression = BI_RGB;
                    bmi.bmiHeader.biSizeImage = w * h * 4;
                    
                    data = create_string_buffer(bmi.bmiHeader.biSizeImage)
                    
                    # ImageTk es utilizado para soportar la transparencia
                    # (el canal Alpha) de los íconos.
                    if GetDIBits(dc, icon_info.hbmColor, 0, h, data,
                                 byref(bmi), DIB_RGB_COLORS):
                        data = bgra_to_rgba(data)
                        image = ImageTk.PhotoImage(
                            Image.frombytes("RGBA", (w, h), data),
                            (w, h)
                        )
                    
                    self.icons[pid] = image
            
            DestroyIcon(icon)
            
            self.processes[pid] = proc_name
        
        DeleteDC(dc)
    
    def populate_processes_list(self):
        """Cargar los procesos en la lista."""
        # Eliminar los procesos anteriores.
        self.tree_processes.delete(*self.tree_processes.get_children())
        for pid, proc_name in self.processes.items():
            if not self.chk_fullpath.checked():
                backslash = proc_name.rfind("\\")
                if backslash > -1:
                    proc_name = proc_name[backslash + 1:]
            self.tree_processes.insert(
                "",
                tk.END,
                text=proc_name,
                values=(pid,),
                image=self.icons.get(pid, self.default_icon)
            )
    
    def refresh_processes(self):
        """Actualizar la lista de procesos."""
        self.load_processes()
        self.populate_processes_list()
    
    def browse(self):
        """Abrir el diálogo para buscar un archivo."""
        filename = askopenfilename(
            filetypes=(("Windows DLL", "*.dll"),))
        if filename:
            self.lbl_filename["text"] = filename
            self.filename = filename


main_window = tk.Tk()
app = Application(main_window)
app.mainloop()

Curso online 👨‍💻

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría 💡

Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

4 comentarios.

  1. Hola estoy tratando de ejecutar el codigo y me da error en estas lineas
    PS H:\Archivos\DLLINYECTOR\code> python main.py
    Traceback (most recent call last):
    File «H:\Archivos\DLLINYECTOR\code\main.py», line 342, in
    app = Application(main_window)
    File «H:\Archivos\DLLINYECTOR\code\main.py», line 157, in __init__
    self.create_widgets()
    File «H:\Archivos\DLLINYECTOR\code\main.py», line 211, in create_widgets
    self.refresh_processes()
    File «H:\Archivos\DLLINYECTOR\code\main.py», line 331, in refresh_processes
    self.load_processes()
    File «H:\Archivos\DLLINYECTOR\code\main.py», line 249, in load_processes
    for proc_name, pid in get_processes_list():
    File «H:\Archivos\DLLINYECTOR\code\injector.py», line 79, in GetProcessesList
    raise RuntimeError(«Could not get processes list.»)
    RuntimeError: Could not get processes list.

  2. no se sabe cual es la finalidad de inyectar dll a los procesos de Windows no explica nada el post aunque parece interesante me gustaría saber cual es el beneficio de inyectar esas DLL a los procesos alguien puede explicar Por Favor

    • Recursos Python says:

      Hola larson. La inyección de DLL es un método para inyectar código en procesos ajenos, y así poder modificar el comportamiento de programas de terceros en tiempo real. Pero aquí solo nos ocupamos del programa responsable de realizar la inyección de un archivo DLL en un proceso, cosa que se puede hacer en Python, que es el tema de esta web :).

      Saludos

Deja una respuesta