pwnable.kr input

老规矩,ssh/nc进去,查看文件权限,查看文件类型,查看文件保护机制

1
2
3
4
5
$ ls -l
total 24
-r--r----- 1 input2_pwn root 55 Jun 30 2014 flag
-r-sr-x--- 1 input2_pwn input2 13250 Jun 30 2014 input
-rw-r--r-- 1 root root 1754 Jun 30 2014 input.c
1
2
# file input
input: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=93e7b62ded980a67120b67ca6a07f6dd4d6577d5, not stripped
1
2
3
4
5
6
# checksec input[*] '/mnt/hgfs/ubuntu_share/pwnable.kr/Toddlers_Bottle/input/input'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

OK,去看代码,这里使用了网络编程socket的基础知识,哇,很强,代码很长…

这里面用了超级多的判断,一共五个步骤,全是je判断,我突然想到,爆破,然后就开始gdb调试,调试的时候,去修改对应的标志位,这里需要了解eflags寄存器的基础知识,以及不同标志位对应的位置

dbg.png

搞定,这道题目中,只用了je判断,而je指令对应的就是ZF,零标志位,嘿嘿,只要关注这个就好了

在调试的时候,使用set $eflag=… 就好了

首先我们发现调试到第一个cmp的时候,ZF=0,而且eflags的值为0x293=0b001010010011(之后就默认不写前面两个0)

小知识,eflags寄存器的值,对应的不同标志位是反过来的,也就是说我们要从右往左数第几位,比如上图中ZF的位置是6,则是倒过来数的第7位….

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> i r
rax 0x3a 58
rbx 0x0 0
...
rip 0x400998 0x400998 <main+68>
eflags 0x293 [ CF AF SF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0

修改ZF位,将数值改为0b1011010011=0x2d3…重复多次,把所有je,jne,jns判断都改变对应的标志位,这样就可以成功绕过所有的判断,获取到flag,但是这种是需要在服务器上面调试,才有用的…我发现在服务器上面使用gdb调试,他直接就爆了这个错误:single stepping until exit from function xxx,which has no line number information…

对不起,我知道了,你编译的时候没有加上-g选项,所以我没法调试,难受…也不能给你安装插件peda或者pwndbg,所以爆破,是不行的…

这里加上一点自己发现的小知识,如果知道一个elf文件是否编译的时候添加-g 即可调试的选项:在我的ubuntu 64位系统下面试的…

helno是没加-g选项的elf文件,hel是加了-g选项的elf文件

1
2
3
4
5
6
7
$ readelf -S helno | grep debug//这个按回车之后,什么都没有显示
$ readelf -S hel | grep debug//这个却显示了很多调试信息
[26] .debug_aranges PROGBITS 0000000000000000 00001034
[27] .debug_info PROGBITS 0000000000000000 00001064
[28] .debug_abbrev PROGBITS 0000000000000000 000013c0
[29] .debug_line PROGBITS 0000000000000000 000014c2
[30] .debug_str PROGBITS 0000000000000000 0000159a

而且如果要能够调试,那就需要更高的权限…想办法去提权是不可能的,会被ban的…(以后试试)

好吧,开始找下一种方法,就是想办法去符合他们的那么多个if判断的条件,好像这个方法,才是一个正常人应该首先想到的办法…我的思路可能一直都很偏…

这里发现5个不同阶段,代表这个五个不同的知识点,main函数参数,进程通信,环境变量,文件操作,网络编程…

好嘛,一个一个来,老实说,这个真的很考验你的编程功底,尤其是如果有过一定的linux下C++开发经验的话,应该就会很轻松…

OK,第一阶段

1
2
3
4
5
// argv
if(argc != 100) return 0;
if(strcmp(argv['A'],"\x00")) return 0;
if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
printf("Stage 1 clear!\n");

要求我们使用100个参数,加上第65位是空,第66位是一个字符串”\x20\x0a\x0d”,嘿嘿,这里我们可以不用python来写脚本,因为python写脚本对于这个程序要说,是比较麻烦的(第5阶段的网络编程,是不好写脚本的,所以我们用C)

C语言中有一个execve函数,可以执行程序,并且给程序附加main函数参数和环境变量…这个函数需要百度仔细学习一下,才能搞懂…

1
2
3
4
5
6
int execve(const char *filename, char *const argv[],
char *const envp[]);
execve()执行程序由 filename决定。
filename必须是一个二进制的可执行文件,或者是一个脚本以#!格式开头的解释器参数参数。如果是后者,这个解释器必须是一个可执行的有效的路径名,但是不是脚本本身,它将调用解释器作为文件名。
argv是要调用的程序执行的参数序列,也就是我们要调用的程序需要传入的参数。
envp 同样也是参数序列,一般来说他是一种键值对的形式 key=value. 作为我们是新程序的环境。

所以我就把第三阶段,也一起写在这里

第三阶段是要求我们

1
2
3
// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");

这个什么意思呢?就是说有一个环境变量”\xde\xad\xbe\xef”,或者说是一个key,要求他的值是 \xca\xfe\xba\xbe ,所以我们在执行函数之前,需要这么写…

1
2
3
4
5
6
7
8
char *argv[101] = {"/home/input/input", [1 ... 99] = "A", NULL};//第一个默认是程序的地址,最后一个默认要设置为NULL,env也是一样
argv['A'] = "\x00";
argv['B'] = "\x20\x0a\x0d";
char *env[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
if(execve("/home/input/input",argv,env)==-1){
printf("execve program error\n");
return 0;
}

嘿嘿,第一阶段和第三阶段完成…

那么就开始解决第二阶段,这个进程通信,怎么解决呢?首先看程序

1
2
3
4
5
6
7
// stdio
char buf[4];
read(0, buf, 4);
if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
read(2, buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
printf("Stage 2 clear!\n");

第一个read函数的第一个参数为0,实际上就是stdin输入,即标准输入,但是第二次调用read函数的第一个参数是2,这个是stderr输入…1就是对应的是stdout…这些是linux文件描述符的基本知识

所谓的文件描述符是一个低级的正整数。最前面的三个文件描述符(0,1,2)分别与标准输入(stdin),标准输出(stdout)和标准错误(stderr)对应。

进程之间通信是需要用到管道的,就好像搭桥一样.这里我们用最简单好用的管道,无名管道pipe,有名管道是fifo

1
2
3
4
include<unistd.h>
int pipe(int fd[2]);
功能: 创建一个简单的管道,若成功则为数组fd分配两个文件描述符,其中fd[0] 用于读取管道,fd[1]用于写入管道。
返回:成功返回0,失败返回-1;

平常我们使用键盘输入的时候就是使用的标准输入的参数,stdin,然而同时他们要求我们也使用stderr,那就不是很好办了…这里我们就需要使用进程之间通信的工具–pipe了.

可以百度一下进程之间的通信,就可以理解管道的重要性和管道的使用方法…

这里我们就直接上代码了,其实每个知识点结合代码看完之后,就差不多懂了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//初始化
int pipe2stdin[2] = {-1,-1};
int pipe2stderr[2] = {-1,-1};
pid_t childpid;
if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0){
perror("Cannot create the pipe");//head file is stdio
exit(1);//head file is stdlib
}
//create process
childpid = fork();
if ( childpid < 0 ){
perror("Cannot fork");
exit(1);
}
if ( childpid == 0 ){
/* Child process */
close(pipe2stdin[0]); close(pipe2stderr[0]); // Close pipes for reading
write(pipe2stdin[1],"\x00\x0a\x00\xff",4);
write(pipe2stderr[1],"\x00\x0a\x02\xff",4);
}
else {
/* Parent process */
close(pipe2stdin[1]); close(pipe2stderr[1]); // Close pipes for writing
dup2(pipe2stdin[0],0); dup2(pipe2stderr[0],2); // Map to stdin and stderr
close(pipe2stdin[0]); close(pipe2stderr[1]); // Close write end (the fd has been copied before)
}

分析一下代码,其实程序的流程很简单,就是建立子进程和父进程之间的管道,关闭掉子进程的读通道和父进程的写通道,就变成了父读子写通道了…然后把子进程对应的内容,通过管道输入到父进程,这样就成功的让stderr这个问题给解决了…

nice,那就下一个step了,第四个,分析代码

1
2
3
4
5
6
7
// file
FILE* fp = fopen("\x0a", "r");
if(!fp) return 0;
if( fread(buf, 4, 1, fp)!=1 ) return 0;
if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
fclose(fp);
printf("Stage 4 clear!\n");

这个就是在本地读取一个文件,并且里面的前4个字节是特定的字符串…

这里有一个问题需要解决的就是我们写的脚本放在那里?文件读取,是要求文件的目录的,首先在pwnable.kr服务器上面,我们对于tmp目录是有写入权限的,然后有一个程序如何在不同目录运行的问题,留到最后分析,其实就是一个ln命令…

那么第四阶段的分析结果就是这样:

1
2
3
4
5
6
7
8
9
10
File *fp = fopen("\x0a","w);
if(!fp){
printf("open files error\n");
return 0;
}
if(fwrite("\x00\x00\x00\x00",4,1,fp)!=4){
printf("write files error\n");
return 0;
}
fclose(fp);

OK,第五段,网络编程…嘿嘿嘿,正好在学习,发现这个很简单,这要写一个客户端程序,连接上它服务端的端口,发送一段数据就行…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
printf("socket error, tell admin\n");
return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons( atoi(argv['C']) );
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
printf("bind error, use another port\n");
return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
printf("accept error, tell admin\n");
return 0;
}
if( recv(cd, buf, 4, 0) != 4 ) return 0;
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
printf("Stage 5 clear!\n");

原程序代码中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
OK,网络编程最基础的套接字编程,而且还是TCP协议,代码附上:
```C
int sockfd;//监听套接字
struct sockaddr_in client;
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0){
perror("create the sockst error\n");
exit(1);
}
client.sin_family = AF_INET;
client.sin_addr.s_addr = inet_addr("127.0.0.1");
client.sin_port = htons(1234);
if(connect(sockfd,(struct sockaddr*)&client,sizeof(server))){
perror("connect error\n");
exit(0);
}
write(sockfd,"\xde\xad\xbe\xef",4);
close(sockfd);

所以这五个过程就这么愉快的解决了,那么回到当初那个不在同一个文件如何操作的问题,由于execve这个函数是开辟一个子线程去运行一个elf文件,而当前目录是/tmp目录,当我们的input程序调用到system打开flag 的函数的时候,就会找不到文件,所以我们要在/tmp目录下面建立一个link连接,所以在命令行中输入这个就好了,嘿嘿.

1
ln /home/input/flag flag

然而这个命令是有问题的,它会报错,说我们不能使用硬链接,由于权限不够…真的难受,查看一下这个命令,发现加上-s选项,就是添加一个软连接,就好了,硬链接是在该目录下生成对应一模一样的文件,两处对改文件的修改,都会导致文件的修改(由于input2目录下我们是没有对flag操作的权限的,所以生成硬链接是不可能的,不知道为什么那些大佬的博客可以)…软连接是生成一个镜像文件,这样子,就可以保证在同一目录下了….

然而在我上创.c文件到tmp目录下的时候,我发现运行之后的结果只有这个

1
2
3
4
5
6
7
8
9
10
11
input2@ubuntu:/tmp$ gcc -o pwnable pwninput.c
input2@ubuntu:/tmp$ ./pwnable
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
Stage 5 clear!
input2@ubuntu:/tmp$

哇,为什么没有打开flag啊,明明我已经创建了一个flag的镜像链接了,这是假的吧,测试了很久,明明正常来说,所有用户对于tmp目录下都存在着读写权限,但是我们好像不能在tmp目录下使用ls命令,看下面peimission denied,尴尬,好像我们创建的软连接flag这个文件是没有用的,权限不足,无法打开

真可怜,又捣鼓了很久,发现一个超级神奇的事情,当我们在tmp下创建一个目录链接的时候,我们竟然能查看该目录,而且这个目录下面还有别人写的exp,为什么目录链接已经存在了,我们还能创建目录链接?这个问题,没有想清楚,可能环境问题就是玄学吧…所以就这样能够运行我的代码了,而且也学到了如何使用python来写脚本,快乐….

1
2
3
4
5
6
7
8
9
10
11
input2@ubuntu:/tmp$ ln /home/input2 input -s
input2@ubuntu:/tmp$ ln /home/input2 input2 -s
input2@ubuntu:/tmp$ cd input
input2@ubuntu:/tmp/input$ ls
? a flag input solveA.py solve.py stderr stderr_f
input2@ubuntu:/tmp/input$ cd ../input2
input2@ubuntu:/tmp/input2$ ls
? 2.txt 4.txt flag input2 new_stdin solver.c
1.txt 3.txt exp.py input new_stderr solver stderr_f
input2@ubuntu:/tmp$ ls
ls: cannot open directory '.': Permission denied

C总代码附上:由于执行程序execve函数是开启进程的问题,要把这个函数写在后面,也就是在文件读写之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
//其实这些头文件都是套接字的头文件
#include <unistd.h>//pipe管道函数头文件
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
//#include <sys/wait.h>//sleep
int main(void){
//stage 1
char *argv[101] = {"/home/input2/input", [1 ... 99] = "A", NULL};
argv['A'] = "\x00";
argv['B'] = "\x20\x0a\x0d";
argv['C'] = "1234";
//stage 3
char *env[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
//stage 4
FILE* fp = fopen("\x0a","w");
fwrite("\x00\x00\x00\x00",4,1,fp);
fclose(fp);
//stage 2
int pipe2stdin[2] = {-1,-1};
int pipe2stderr[2] = {-1,-1};
pid_t childpid;
if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0){
perror("Cannot create the pipe");
exit(1);
}
childpid = fork();
if ( childpid < 0 ){
perror("Cannot fork");
exit(1);
}
if ( childpid == 0 ){
close(pipe2stdin[0]); close(pipe2stderr[0]);
write(pipe2stdin[1],"\x00\x0a\x00\xff",4);
write(pipe2stderr[1],"\x00\x0a\x02\xff",4);
}
else {
close(pipe2stdin[1]); close(pipe2stderr[1]);
dup2(pipe2stdin[0],0); dup2(pipe2stderr[0],2);
close(pipe2stdin[0]); close(pipe2stderr[1]);
if(execve("/home/input2/input",argv,env)==-1){//look we put the execve here
printf("execve program error\n");
return 0;
}
}
//sleep(1);
int sockfd;//监听套接字
struct sockaddr_in client;
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0){
perror("create the sockst error\n");
exit(1);
}
client.sin_family = AF_INET;
client.sin_addr.s_addr = inet_addr("127.0.0.1");
client.sin_port = htons(1234);
if(connect(sockfd,(struct sockaddr*)&client,sizeof(client))){
perror("connect error\n");
exit(0);
}
write(sockfd,"\xde\xad\xbe\xef",4);
close(sockfd);
return 0;
}

网上的代码,喜欢加上sleep函数,我觉得只要你把stage的顺序给安排好了,应该就没有问题,我觉得唯一需要加sleep暂停的地方就是网络编程之前,写在注释里了…因为服务端程序只接受一次消息,没有for循环…所以暂停一下,让子进程处理一下之前的消息,也是可以的

1
2
3
4
5
6
7
8
9
10
11
input2@ubuntu:/tmp/input2$ gcc pwninput.c -o pwninput
input2@ubuntu:/tmp/input2$ ./pwninput
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
Stage 5 clear!
Mommy! I learned how to pass various input in Linux :)

所以说,说不定,后来者,也能看到我写在里面的代码….

顺便分析一下,别人的py代码,发现,好像我的python基础好垃圾啊…

这里顺便学习了一下pwntools中的process函数(pwntools-process函数),还有这个函数remotepwntools-remote,这个真的好用,比写C语言代码好太多了,它能够自带标准输入输出作为参数…我以前还觉得python没有C好,真香,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import os
from pwn import *
argvs = [str(i) for i in range(100)]//列表解析式
argvs[ord('A')] = '\x00'
argvs[ord('B')] = '\x20\x0a\x0d'
argvs[ord('C')] = '1234'
a = open("./stderr_f", 'w+')#这里是处理stderr
a.write("\x00\x0a\x02\xff")
a.close()
a = open('./stderr_f', 'r+')
stderr_f = a.fileno() #这里把文件描述符的值赋给stderr_f
log.info('stderr fd : ' + str(stderr_f))
b = open("./\x0a", 'w+')
b.write("\x00\x00\x00\x00")
b.close()
send_v = '\x00\x0a\x00\xff'
env_v = {'\xde\xad\xbe\xef' : '\xca\xfe\xba\xbe'}
p = process(executable='/home/input2/input', argv=argvs, stderr=stderr_f, env=env_v)
p.send(send_v)#这里是使用标准输入stdin
a.close()#关闭文件...
#所以说简单的套接字编程,在pwntools下,就是这两行代码?牛逼...remote这一个函数调用直接解决C语言的10多行套接字代码...
con = remote('localhost', 1234,fam="ipv4",typ="tcp")
con.send("\xde\xad\xbe\xef")
con.close()
sleep(0.1)
print p.recv()

结语

写了4000字,真的快乐

学习过程中使用过的链接

C语言分析代码:https://werewblog.wordpress.com/2016/01/11/pwnable-kr-input/

pwntools文档:https://pwntools-docs-zh.readthedocs.io/zh_CN/dev/about.html

-------------本文结束感谢您的阅读-------------