简单分析Python ctypes模块的WinDLL源码(我爱Python,吼吼~)


 

 又是一个寂寞的周末啊同学们,这几天天气变冷自己却没有赖床,好吧,表扬一次^^

    扯点八卦,今天是pycon2011在上海那边开了,我早上和刚才看了网上的直播,做的很不错,形式很像irongeek.com的视频,我觉得这块在国内还是有一点点商业前景的,哈.听了网易林伟和豆瓣洪强宁的演讲,开拓了点视野,豆瓣基本主要都是用python做,网易说一些服务端也是用python写,结合c去做,但是python的代码已经提高到70%了.所以,python都是很有前景的吧.(不过2.x和3.x的版本并行发展让哥蛋疼..)

    今天有同学在群里发exploit-db上的MS11-080的连接,打开一看,尼玛的居然是用python写的,做系统提权.我感觉比较新鲜的是对windows系统api的调用居然这么方便(好吧,我孤陋寡闻- - ),顿时来了兴致,ctypes模块的WinDLL.既然python是一个开源的东东(口号:Human knowledge belongs to the world!),让我们可以深入了解底层的具体实现,我们要学习的总是这个渔而不是这个鱼对吧.下面算是我自己做做笔记吧,欢迎大家拍砖:

    首先我们来看看WinDLL对windows api调用的方便程度:

\

\

    上面图演示的是对Kernel32.dll里面的GetModuleHandleA的调用以及结果验证.同学们是不是觉得很方便哩.

    看看WinDLL的实现,WinDLL实际上是一个类,下面是代码,我们一层一层往上回溯:

 

 

 

class WinDLL(CDLL): 

    """This class represents a dll exporting functions using the

    Windows stdcall calling convention.

    """ 

    _func_flags_ = _FUNCFLAG_STDCALL 

 

    WinDLL的代码很简单,主要是声明一个函数标志,通过名字可以知道是声明函数的调用约定,这里是STDCALL,一看到这里哥就冒出了一个api需要测试,后面再说,这里卖个关子.WinDLL这个类继承自CDLL,那么我们接下来看的就是CDLL的代码:

 

 

 

 

class CDLL(object): 

    """An instance of this class represents a loaded dll/shared

    library, exporting functions using the standard C calling

    convention (named 'cdecl' on Windows).

 

    The exported functions can be accessed as attributes, or by

    indexing with the function name.  Examples:

 

    <obj>.qsort -> callable object

    <obj>['qsort'] -> callable object

 

    Calling the functions releases the Python GIL during the call and

    reacquires it afterwards.

    """ 

    _func_flags_ = _FUNCFLAG_CDECL 

    _func_restype_ = c_int 

 

    def __init__(self, name, mode=DEFAULT_MODE, handle=None, 

                 use_errno=False, 

                 use_last_error=False): 

        self._name = name 

        flags = self._func_flags_ 

        if use_errno: 

            flags |= _FUNCFLAG_USE_ERRNO 

        if use_last_error: 

            flags |= _FUNCFLAG_USE_LASTERROR 

 

        class _FuncPtr(_CFuncPtr): 

            _flags_ = flags 

            _restype_ = self._func_restype_ 

        self._FuncPtr = _FuncPtr 

 

        if handle is None: 

            self._handle = _dlopen(self._name, mode) 

        else: 

            self._handle = handle 

 

    def __repr__(self): 

        return "<%s '%s', handle %x at %x>" % \ 

               (self.__class__.__name__, self._name, 

                (self._handle & (_sys.maxint*2 + 1)), 

                id(self) & (_sys.maxint*2 + 1)) 

 

    def __getattr__(self, name): 

        if name.startswith('__') and name.endswith('__'): 

            raise AttributeError(name) 

        func = self.__getitem__(name) 

        setattr(self, name, func) 

        return func 

 

    def __getitem__(self, name_or_ordinal): 

        func = self._FuncPtr((name_or_ordinal, self)) 

        if not isinstance(name_or_ordinal, (int, long)): 

            func.__name__ = name_or_ordinal 

        return func 

 

 

    构造函数就是设置一些相关的成员,同时声明了一个内部的类对象_FuncPtr用于保存具体的api函数指针,这个类继承自_CFuncPtr,这个稍后说明.__repr__函数用于打印相关信息(以一种友好的方式-- 书上这么说的~),__getattr__是'.'运算符,__getitem__是'[ ]'索引.

    我们在上面的演示代码中, from ctypes import windll,当中的这个windll实际上是ctypes模块的一个成员对象,类型是LibraryLoader,具体代码如下:

 

 

class LibraryLoader(object): 

    def __init__(self, dlltype): 

        self._dlltype = dlltype 

 

    def __getattr__(self, name): 

        if name[0] == '_': 

            raise AttributeError(name) 

        dll = self._dlltype(name) 

        setattr(self, name, dll) 

        return dll 

 

    def __getitem__(self, name): 

        return getattr(self, name) 

 

    def LoadLibrary(self, name): 

        return self._dlltype(name) 

         

        #......# 

         

if _os.name in ("nt", "ce"): 

    windll = LibraryLoader(WinDLL) 

    oledll = LibraryLoader(OleDLL) 

    实际上,LibraryLoader对象以一个类(类型)作为输入,通过记录这个类型,当对LibraryLoader对象进行'.'操作时(__getattr__方法),返回的是dll = self._dlltype(name),也就是说单我们执行windll.kernel32这样的语句时,背后运行的是self._dlltype(name) => dlltype(name) => WinDLL(name) => WinDLL.__init__(name), =>表示一个递进分析的过程,为什么需要加上这样的一个LIbraryLoader的对象,通过setattr(self, name, dll)这句我们可以很直观的理解到这是为了记录已经Load的DLL用以之后的使用.

 

 

    ,在WinDLL.__init__当中,需要保存要打开的dll的句柄,这部分的功能通过self._handle = _dlopen(self._name, mode)这句实现,关于_dlopen,它是从外部导入的,包括之前提到的_CFuncPtr对象,具体代码如下:

 

from _ctypes import Union, Structure, Array 

from _ctypes import _Pointer 

from _ctypes import CFuncPtr as _CFuncPtr 

from _ctypes import __version__ as _ctypes_version 

from _ctypes import RTLD_LOCAL, RTLD_GLOBAL 

from _ctypes import ArgumentError 

 

if _os.name in ("nt", "ce"): 

    from _ctypes import LoadLibrary as _dlopen 

    from _ctypes import FUNCFLAG_STDCALL as _FUNCFLAG_STDCALL 

 

     一开始我一直不清楚_ctypes是什么东西,而在Python的文档当中也没有提到,这里吐槽一下搜索引擎,对于我的关键字_ctypes,他总是比较自做聪明的用ctypes替代,-___-|||,不过哩我们还是很容易能够搜索得到,_ctypes是一个c写的外部库,具体在python的Dlls文件夹下,_ctypes.pyd就是了,实际上pyd文件就是一个dll文件,好吧,一般人到这里就算停了,反正通过名字就能知道你具体完成的功能,但是哥作为一个喜欢追根刨底的男人,对于不能看到_ctypes的具体代码很是纠结(因为默认的安装包,win版是没有_ctypes的代码,C代码),既然python是开源,那么果断下他的源代码搜一搜,结果很明显,我们可以得到我们想要的^^

    具体的实现代码和导出代码如下,对于函数的导出和对象的导出有不同的方式:

 

PyMethodDef _ctypes_module_methods[] = { 

    /*          ......          */ 

    {"LoadLibrary", load_library, METH_VARARGS, load_library_doc}, 

    /*          ......          */ 

    {NULL,      NULL}        /* Sentinel */ 

}; 

 

static PyObject *load_library(PyObject *self, PyObject *args) 

    TCHAR *name; 

    PyObject *nameobj; 

    PyObject *ignored; 

    HMODULE hMod; 

    if (!PyArg_ParseTuple(args, "O|O:LoadLibrary", &nameobj, &ignored)) 

        return NULL; 

#ifdef _UNICODE 

    name = alloca((PyString_Size(nameobj) + 1) * sizeof(WCHAR)); 

    if (!name) { 

        PyErr_NoMemory(); 

        return NULL; 

    } 

 

    { 

        int r; 

        char *aname = PyString_AsString(nameobj); 

        if(!aname) 

            return NULL; 

        r = MultiByteToWideChar(CP_ACP, 0, aname, -1, name, PyString_Size(nameobj) + 1); 

        name[r] = 0; 

    } 

#else 

    name = PyString_AsString(nameobj); 

    if(!name) 

        return NULL; 

#endif 

 

    hMod = LoadLibrary(name); 

    if (!hMod) 

        return PyErr_SetFromWindowsErr(GetLastError()); 

#ifdef _WIN64 

    return PyLong_FromVoidPtr(hMod); 

#else 

    return Py_BuildValue("i", hMod); 

#endif 

 

Py_TYPE(&PyCFuncPtr_Type) = &PyCFuncPtrType_Type; 

PyCFuncPtr_Type.tp_base = &PyCData_Type; 

if (PyType_Ready(&PyCFuncPtr_Type) < 0) 

    return; 

Py_INCREF(&PyCFuncPtr_Type); 

PyModule_AddObject(m, "CFuncPtr", (PyObject *)&PyCFuncPtr_Type); 

 

PyTypeObject PyCFuncPtr_Type = { 

    PyVarObject_HEAD_INIT(NULL, 0) 

    "_ctypes.PyCFuncPtr", 

    sizeof(PyCFuncPtrObject),                   /* tp_basicsize */ 

    0,                                          /* tp_itemsize */ 

    (destructor)PyCFuncPtr_dealloc,             /* tp_dealloc */ 

    0,                                          /* tp_print */ 

    0,                                          /* tp_getattr */ 

    0,                                          /* tp_setattr */ 

    0,                                          /* tp_compare */ 

    (reprfunc)PyCFuncPtr_repr,                  /* tp_repr */ 

    &PyCFuncPtr_as_number,                      /* tp_as_number */ 

    0,                                          /* tp_as_sequence */ 

    0,                                          /* tp_as_mapping */ 

    0,                                          /* tp_hash */ 

    (ternaryfunc)PyCFuncPtr_call,               /* tp_call */ 

    0,                                          /* tp_str */ 

    0,                                          /* tp_getattro */ 

    0,                                          /* tp_setattro */ 

    &PyCData_as_buffer,                         /* tp_as_buffer */ 

    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_NEWBUFFER | Py_TPFLAGS_BASETYPE, /* tp_flags */ 

    "Function Pointer",                         /* tp_doc */ 

    (traverseproc)PyCFuncPtr_traverse,          /* tp_traverse */ 

    (inquiry)PyCFuncPtr_clear,                  /* tp_clear */ 

    0,                                          /* tp_richcompare */ 

    0,                                          /* tp_weaklistoffset */ 

    0,                                          /* tp_iter */ 

    0,                                          /* tp_iternext */ 

    0,                                          /* tp_methods */ 

    0,                                          /* tp_members */ 

    PyCFuncPtr_getsets,                         /* tp_getset */ 

    0,                                          /* tp_base */ 

    0,                                          /* tp_dict */ 

    0,                                          /* tp_descr_get */ 

    0,                                          /* tp_descr_set */ 

    0,                                          /* tp_dictoffset */ 

    0,                                          /* tp_init */ 

    0,                                          /* tp_alloc */ 

    PyCFuncPtr_new,                             /* tp_new */ 

    0,                                          /* tp_free */ 

};   

 

 

 

    对于函数的导出,使用的是PyMethodDef的数组,而对于成员的导出,使用的是PyModule_AddObject()函数._ctypes的LoadLibrary的实际函数load_library,他的当然要么是LoadLibrary要么是LoadLibraryEx这两个API啦.而对于CFuncPtr成员,我们可以看到PyTypeObject的类型,里面记录了许多相关操作的函数指针.
    回到CDLL的代码当中,当对一个CDLL对象使用'.'操作的时候,实际上返回的之后相应函数的地址,如windll.kernel32.GetModuleHandleA,执行的流程是CDLL.__getattr__() => CDLL.__getitem__(),在getitem当中通过CFuncPtr的操作得到相关的函数地址,函数指针都记录在PyCFuncPtr_Type成员中,具体我就没再细看了,既然回归到C里面,用GetProcAdress实现应该不是很麻烦.在getattr当中也将调用过的函数地址通过setattr保存下载,供之后调用.
    基本上对windll这部分的寻找他源代码的故事(kuso,寻找他乡的故事- -好冷)到这里就到一段落了.我们之前说过有一个函数想测试调用的,同学们有没有猜到是哪个函数哩.
    答案就是wsprintf这个api,这个函数特殊的地方在哪?他是windows api当中唯一(好吧,我不确定是否唯一)不是用stdcall调用约定的api.为什么不能用stacall?提示:1,参数个数可变;2.stdcall是由被调用者进行堆栈清理.那么他只能用cdcel的调用约定了
    在WinDLL类的初始化当中我们看到,他把函数调用约定一并的设置为STDCALL,所以我猜测,对于wsprintf调用是会出问题的.验证了一下,果然是的,会提示参数个数不匹配,如图

\

    所以,可怜的wsprintf就被这样排除在外了.
    强大的python啊,通过ctypes,基本上想直接用sdk都ok了,当然python写不了驱动(笑).尼玛的当初脚本选python学还是对的~
    今天看pycon2011的直播,接触了python很多高级语言特性在实际工程中的应用,也是很有体会,回头把Learning python再翻翻,估计能有新的收获.
    大体上想记录的就是这些了,欢迎大家补充交流^_^

 

    末尾的吐槽: 明天要去北校开会啊...伤不起啊,哥不想去...开完会还说要聚餐..果断那个时候开溜啦啦啦~

    ---------------------------------------------------------------

    刚才写完,点击提交没有反应,我看了一下草稿一直保持在4点半..菊花一紧,果断保存在word里.果然..提交失败....重新排版.

 

摘自 GaA.Ra的自留地

 

相关内容

    暂无相关文章

评论关闭