首页
社区
课程
招聘
[原创]关于一次在pwnable.kr中input题目的经历(python3)
2021-7-3 20:31 14209

[原创]关于一次在pwnable.kr中input题目的经历(python3)

2021-7-3 20:31
14209

前言

首先感谢一下社区的i乂大佬对于pwnable.kr中婴儿难度题目的解题分享,让我能够跟着一步步学习,但是其中input这道题i乂大佬使用的是C语言,然鹅深受pwntools毒害(不是)的我并不想使用C语言去写exp,于是这道题之前被我搁置了,最近在做ascii_easy题目的时候,发现可以使用process(argv=['程序名','其他参数'])来进行传递参数,于是我就想到了之前的这道input的题目,然后就开始尝试使用pwntools中的process来做这道题

过程

首先,使用ssh input2@pwnable.kr -p2222(密码guest)连接服务器,然后使用ls和pwd查看文件列表和当前目录
连接服务器查看文件
里面的input和input.c我们可以通过scp下载到本地,如下
下载文件
然后就可以开始分析题目了

<1>分析input.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
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main(int argc, char* argv[], char* envp[]){
    printf("Welcome to pwnable.kr\n");
    printf("Let's see if you know how to give input to program\n");
    printf("Just give me correct inputs then you will get the flag :)\n");
 
    // 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");   
 
    // 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");
 
    // env
    if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
    printf("Stage 3 clear!\n");
 
    // 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");   
 
    // network
    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");
 
    // here's your flag
    system("/bin/cat flag");   
    return 0;
}

首先,这个题目分了5个阶段(Stage)而且使用//注释标注的很直白,分别为
1>argv参数的传递
2>stdio标准输入输出
3>env环境变量
4>file文件操作
5>network网络服务(这里可以参考i乂大佬帖子中的network相关知识)
然后我们开始对以上的5个阶段逐一攻克

<2>argv参数的传递

目标

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");

这代码很明显,首先argc是argv传递的参数的个数,这里如果没有参数,默认的情况为
argc=1,argv[0]='程序名'
简单的说,argv[0]是给命令行的,让命令行启动程序,之后argv[1]开始都是供给程序作为参数使用的,而argc则统计有多少个参数(包括argv[0])

我们这里由于要使用process来完成,因此在结合之前的process可以用argv=[]来传参的用法,我们这里就考虑,我们是否可以生成一个有100个成员的列表传递到argv中,并将对应的成员赋值成目标需求的内容。
说干就干,我们直接通过

1
2
args=list('a'*100)
#print (args)

来生成了一个有100个成员的列表,这里可以使用被注释掉的print来验证
然后就是该对应题目进行赋值了
首先argc=100的条件已经满足
然后strcmp(A,B)是将A和B的文本进行对比,如果相同,返回0,从而使得if语句不成立,后面的return 0便不会执行。
因此,代码如下

1
2
3
args[0]='./input'
args[ord('A')]=b'\x00'
args[ord('B')]=b'\x20\x0a\x0d'

PS:这里的ord是指的对后面的单个字符(如果是多个字符会报错)取ascii码值
使用ord的原因是,在C语言中,char类型(也就是单个字符,且必须使用单引号引用)和int类型是可以混用的,当一个字符作为int类型使用时,默认就会当做它对应的ascii码值

举个例子

A的scill码值是65

1
2
char x='A';
printf("x(int)=%d,x(char)=%c",x,x);

结果为

1
x(int)=65,x(char)=A

这里%d和%c分别是将后面对应的参数输出为整型和字符型
还有一点就是,由于本人使用的是python3版本,此版本的python对于bytes和str类型区分很严格,因此对于'\x00'这种编码字符最好使用bytes类型,即引号前方加一个b进行声明,不然有些地方会出现一些问题
然后我们对第一阶段的exp进行一次测试

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
 
args=list('a'*100)
#print(args)
args[0]='./input'
args[ord('A')]=b'\x00'
args[ord('B')]=b'\x20\x0a\x0d'
 
p=process(argv=args)
 
p.interactive()

结果如下
阶段1完成

<3>stdio标准输入输出

目标

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进行了数据读入
对于read的使用方法如下

1
read(int fd, void * buf, size_t count)

它的含义是,从fd流中(0,1,2分别对应stdin,stdout,stderr)读取count个字节到buf中。
比如本题目中就是从stdin流中读取4字节到buf中,之后又从stderr流中读取了4字节到buf中

然后是memcmp(A,B,n)
意义是内存比较,即将A与B对应的位置存储的数据,分别取出n位进行对比,如果相同则返回0,在这里便可以导致return 0不执行
于是,我们的目标就是,将程序的stdin和stderr流的内容变成题目要求的内容,但是在一开始我是也是无从下手的,于是只好去查资料,然后发现,python3里有一个名叫subprocess的模块,然后我发现好像这道题的stdio和env都能靠它来实现,同时我也有些怀疑,pwntools的processs是不是就是拿这个实现的,于是我随便将process的参数填写错误,然后查看process模块的具体位置
故意输入错误参数
接着到该位置打开源码,然后发现,我的猜测是对的

1
2
3
...
import subprocess
...

pwntools的process确实是导入了这个模块,然后我还意外的发现,process整个模块的说明也有写在源码里,然后我意识到了,自己一直通过百度去查各模块用法仿佛像一个傻子(orz)......
在process的用法说明中,我找到stdin,stdout,stderr三个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
stdin(int):
    File object or file descriptor number to use for ``stdin``.
    By default, a pipe is used.  A pty can be used instead by setting
    this to ``PTY``.  This will cause programs to behave in an
    interactive manner (e.g.., ``python`` will show a ``>>>`` prompt).
    If the application reads from ``/dev/tty`` directly, use a pty.
 
stdout(int):
    File object or file descriptor number to use for ``stdout``.
    By default, a pty is used so that any stdout buffering by libc
    routines is disabled.
    May also be ``PIPE`` to use a normal pipe.
 
stderr(int):
    File object or file descriptor number to use for ``stderr``.
    By default, ``STDOUT`` is used.
    May also be ``PIPE`` to use a separate pipe,
    although the :class:`pwnlib.tubes.tube.tube` wrapper will not be able to read this data.

这三个的参数都是int类型,但下方的说明有些File object or file,因此我们可以考虑将要写入的数据先放到一个文件中然后直接将文件交给这三个流。
明确了要做的事情,我们就该开始写exp了
首先需要将目标"\x00\x0a\x00\xff"和"\x00\x0a\x02\xff"写入到文件中

1
2
3
4
sti=b'\x00\x0a\x00\xff'
write('/tmp/sti',sti)
ste=b'\x00\x0a\x02\xff'
write('/tmp/ste',ste)

然后我们尝试将这俩文件传递给stdin和stderr流

1
p=process(argv=args,stdin=open('/tmp/sti'),stderr=open('/tmp/ste'))

然后将执行我们的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
 
args=list('a'*100)
#print(args)
args[0]='./input'
args[ord('A')]=b'\x00'
args[ord('B')]=b'\x20\x0a\x0d'
 
sti=b'\x00\x0a\x00\xff'
write('/tmp/sti',sti)
ste=b'\x00\x0a\x02\xff'
write('/tmp/ste',ste)
 
p=process(argv=args,stdin=open('/tmp/sti'),stderr=open('/tmp/ste'))
 
p.interactive()

结果如下
阶段2
这里如果是没有修改过process源码,基本都会报一个错误,就是stdin和stderr关闭时的一个异常,这里我的做法有些粗暴,直接把那个报错异常的if一句注释掉了,然后下方代码重新对齐就好了。

<4>env环境变量

目标

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

先分析,strcmp就不需要说明了,后面的getenv这句代码指的是从环境变量中获取名为"\xde\xad\xbe\xef"这个变量的值,具体如下所述

1
2
3
4
5
//首先环境变量是以键值对存在的,就是"名字(键)":"值"
//就以PATH环境变量来说,它就是"PATH":"/bin:xx1:xxx2:xx3"存在的
//而我们想要获取它后面的那些目录,我们只需要知道它的名字叫PATH就可以了
getenv("PATH")
//结果就是"/bin:xx1:xxx2:xx3"

这里,我们需要把"\xde\xad\xbe\xef":"\xca\xfe\xba\xbe"这一键值对放入到环境变量里边,而经过之前对process源码的查看,里面确实存在env这个参数来传入环境变量

1
2
env(dict):
    Environment variables.  By default, inherits from Python's environment.

这里的参数是一个dict也就是python的字典,它的格式就是
{"key1":"value1","key2":"value2",...}
但是我们需要的只有一个,因此我们直接新建一个字典

1
env_={b"\xde\xad\xbe\xef":b"\xca\xfe\xba\xbe"}

需要注意的是,如果是python2,这里的b可加可不加,但是如果是python3的话,这里的b是必须要加的,不然会阶段3无法通过
然后在process中加入env

1
p=process(argv=args,stdin=open('/tmp/sti'),stderr=open('/tmp/ste'),env=env_)

执行exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
 
args=list('a'*100)
#print(args)
args[0]='./input'
args[ord('A')]=b'\x00'
args[ord('B')]=b'\x20\x0a\x0d'
 
sti=b'\x00\x0a\x00\xff'
write('/tmp/sti',sti)
ste=b'\x00\x0a\x02\xff'
write('/tmp/ste',ste)
 
env_={b"\xde\xad\xbe\xef":b"\xca\xfe\xba\xbe"}
 
p=process(argv=args,stdin=open('/tmp/sti'),stderr=open('/tmp/ste'),env=env_)
 
p.interactive()

阶段3

<5>file文件操作

目标

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");

先分析,fopen是打开文件,后面的参数"\x0a"是文件名,"r"是只读方式打开
之后的fread(buf,4,1,fp)则是代表从fp也就刚刚打开的文件中,读取1个单元的字符,每个单元为4字节,到buf中
用简单的人话说就是从刚刚打开的文件中,读取了1*4个字节到buf中
如果是fread(buf,1,4,fp),那就是从fp中读取4*1个字节到buf中,达成的效果是一样,区别就暂且不论了。
memcmp前面有说,fclose(fd)是关闭文件(这是一个好习惯)。
所以,目的就是我们要从"\x0a"这个文件中读取4字节的内容,然后要和"\x00\x00\x00\x00"一样。
我们要做的就是将"\x00\x00\x00\x00"写入到这个文件中,然后便可以通过阶段4。
直接使用write即可

1
2
buf=b'\x00\x00\x00\x00'
write('\x0a',buf)

在这里,基本都可以想到,在执行input的时候,我们是无法在当前目录写入文件的,因为权限问题,所以可以使用process的cwd参数来改变运行的目录,从而使得程序到一个可写入文件的地方即可,如/tmp
于是,对上方的wirte的目录需要进行修改

1
2
3
buf=b'\x00\x00\x00\x00'
write('/tmp/mydir/\x0a',buf)
p=process(argv=args,stdin=open('/tmp/sti'),stderr=open('/tmp/ste'),env=env_,cwd='/tmp')

但是随后,因为我们的运行目录改变了,而argv[0]的参数还是在当前目录执行./input来运行input,所以会出现找不到文件的情况,因此我个人想到的方法有3个

1
2
3
4
5
6
7
//假设dirinput所在的目录
//1.直接创建软连接,将input链接到/tmp中去
ln -sb dir/input /tmp/input
//2.修改argv[0]为input的绝对路径
argv[0]='dir/input'
//3.使用process的一个参数executable
p=process(argv=args,stdin=open('/tmp/sti'),stderr=open('/tmp/ste'),env=env_,cwd='/tmp',executable='dir/input')

以上三种方法任选其一都可以
然后执行exp(我选择的第三种)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
 
args=list('a'*100)
#print(args)
args[0]='./input'
args[ord('A')]=b'\x00'
args[ord('B')]=b'\x20\x0a\x0d'
 
sti=b'\x00\x0a\x00\xff'
write('/tmp/sti',sti)
ste=b'\x00\x0a\x02\xff'
write('/tmp/ste',ste)
 
env_={b"\xde\xad\xbe\xef":b"\xca\xfe\xba\xbe"}
 
buf=b'\x00\x00\x00\x00'
write('/tmp/\x0a',buf)
#dir要修改为input的目录
p=process(argv=args,stdin=open('/tmp/sti'),stderr=open('/tmp/ste'),env=env_,cwd='/tmp',executable='dir/input')
 
p.interactive()

阶段4

<6>network网络服务

目标

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
// network
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");

首先,分析代码吧,我想如果不是熟悉C语言的人,差不多都和我一样了,第一反应Excuse me?
不过还好,有network的提醒,而且下边靠着socket,addr,port相关的东西差不多可以联想到大概率和地址+端口的服务有关,然后我们看到几个大写的常量
AF_INET,SOCK_STREAM,INADDR_ANY
直接上百度,找到了这篇相关的播客AF_INET
然后得知,这大概是写的就是创建了一个地址为INADDR_ANY(经查是0.0.0.0,指本机),端口为argv['C']的一个socket接口,其中bind是用于绑定socket和ip+端口用的,然后利用listen进行对该接口的监听,之后的accept是获取连接,并通过recv接受数据,如果数据为"\xde\xad\xbe\xef"则通过第五阶段。
总体来说,这一部分就是开了个本机的端口,其中端口为argv['C']的值,而我们的目的就是向这个端口传递"\xde\xad\xbe\xef"。
端口我们可以自主选择,我这里选择66666

1
args[ord('C')]='66666'

然后通过remote即可实现对端口的连接,并发送消息

1
2
3
r=remote('127.0.0.1',66666)
r.send(b'\xde\xad\xbe\xef')
r.close()

最后在/tmp文件夹内建立一个文件flag,用于我们测试是否成功读取

1
echo 'hello,this is flag!' > /tmp/flag

执行exp

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
from pwn import *
 
args=list('a'*100)
#print(args)
args[0]='./input'
args[ord('A')]=b'\x00'
args[ord('B')]=b'\x20\x0a\x0d'
 
sti=b'\x00\x0a\x00\xff'
write('/tmp/sti',sti)
ste=b'\x00\x0a\x02\xff'
write('/tmp/ste',ste)
 
env_={b"\xde\xad\xbe\xef":b"\xca\xfe\xba\xbe"}
 
buf=b'\x00\x00\x00\x00'
write('/tmp/\x0a',buf)
 
args[ord('C')]='66666'
#dir要修改为input的目录
p=process(argv=args,stdin=open('/tmp/sti'),stderr=open('/tmp/ste'),env=env_,cwd='/tmp',executable='dir/input')
 
r=remote('127.0.0.1',66666)
r.send(b'\xde\xad\xbe\xef')
r.close()
 
p.interactive()

阶段5
获取成功

<7>适应到服务器执行拿flag

通过前面的步骤我们已经可以拿到本地的flag了,接下来就是去服务器拿flag了,首先我们看一下我们的exp,我们将process的运行目录改好

1
p=process(argv=args,stdin=open('/tmp/sti'),stderr=open('/tmp/ste'),env=env_,cwd='/tmp',executable='/home/input2/input')

其次,我们服务器的flag并不在/tmp中,因此我们需要想办法让我们在/tmp中访问到flag,因此,我们可以使用软连接的方法,如下

1
2
import os
os.system('ln -sb /home/input2/flag /tmp/flag')

通过这种方法,将flag软连接到/tmp中,然后就可以读取了
于是,我们将exp修改如下

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
from pwn import *
import os
 
os.system('ln -sb /home/input2/flag /tmp/flag')
args=list('a'*100)
#print(args)
args[0]='./input'
args[ord('A')]=b'\x00'
args[ord('B')]=b'\x20\x0a\x0d'
 
sti=b'\x00\x0a\x00\xff'
write('/tmp/sti',sti)
ste=b'\x00\x0a\x02\xff'
write('/tmp/ste',ste)
 
env_={b"\xde\xad\xbe\xef":b"\xca\xfe\xba\xbe"}
 
buf=b'\x00\x00\x00\x00'
write('/tmp/\x0a',buf)
 
args[ord('C')]='66666'
 
p=process(argv=args,stdin=open('/tmp/sti'),stderr=open('/tmp/ste'),env=env_,cwd='/tmp',executable='/home/input2/input')
 
r=remote('127.0.0.1',66666)
r.send(b'\xde\xad\xbe\xef')
r.close()
p.interactive()

然后通过如下代码将exp上传到服务器的/tmp中执行

1
2
3
scp -P2222 exp.py input2@pwnable.kr:/tmp/exp.py
ssh input2@pwnable.kr -p2222
python '/tmp/exp.py'

运行后发现,我们的exp确实通过5个阶段,但是flag依然读取失败
失败
然后寻找原因,查询权限后发现,/tmp文件对于除了root和root组之外的用户没有读的权限
权限
但是我们拥有写的权限,于是我们考虑,在/tmp中自己建立一个文件夹,然后后在自己建立的文件夹内进行操作,代码如下:

1
2
3
import os
os.system('mkdir /tmp/mydir')
os.system('ln -sb /home/input2/flag /tmp/mydir/flag')

然后整个exp的路径也进行修改,如下

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
from pwn import *
import os
 
os.system('mkdir /tmp/mydir')
os.system('ln -sb /home/input2/flag /tmp/mydir/flag')
args=list('a'*100)
#print(args)
args[0]='./input'
args[ord('A')]=b'\x00'
args[ord('B')]=b'\x20\x0a\x0d'
 
sti=b'\x00\x0a\x00\xff'
write('/tmp/mydir/sti',sti)
ste=b'\x00\x0a\x02\xff'
write('/tmp/mydir/ste',ste)
 
env_={b"\xde\xad\xbe\xef":b"\xca\xfe\xba\xbe"}
 
buf=b'\x00\x00\x00\x00'
write('/tmp/mydir/\x0a',buf)
 
args[ord('C')]='66666'
 
p=process(argv=args,stdin=open('/tmp/mydir/sti'),stderr=open('/tmp/mydir/ste'),env=env_,cwd='/tmp/mydir',executable='/home/input2/input')
 
r=remote('127.0.0.1',66666)
r.send(b'\xde\xad\xbe\xef')
r.close()
p.interactive()

再次进行上传,运行
成功
完成,拿到了flag

小结

python3和python2在bytes和str这方面差别还是很大的,py2可以随意混用,并且对结果没啥影响,但是在python3中这些规范了很多,同时,多查资料是个挺好的习惯,但是也不能完全依靠查百度,有些时候也可以从源码中获取到很多建议。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

上传的附件:
收藏
点赞4
打赏
分享
最新回复 (1)
雪    币: 4165
活跃值: (15932)
能力值: (RANK:710 )
在线值:
发帖
回帖
粉丝
Roland_ 12 2021-7-4 14:49
2
0
支持
游客
登录 | 注册 方可回帖
返回