# 0x2. 关卡记录

# 1.level1~3:密码传参

前三关是传密码,第 1 关无密码,第二关运行后输入密码,第 3 关通过参数传密码。

因为密码在提示中写了,认识英语就知道密码了。

# 2.level4,7:环境变量

4 要求新增一个环境变量;

7 要求设置空环境变量;

总而言之都用一条命令:

env -i <program>

便可以重新设定环境变量运行程序,7 的要求只需要不加环境变量参数即可,当然 4 也可以使用 export 完成吧。

这些知识在挑战上方的视频中都有讲解,不明白可以看看。

# 3.level5,6:重定向

5 重定向输入;

6 重定向输出。

使用大小于号就可以重定向输入和输出。

# 4.level8~14:shell 脚本

8 创建一个 shell 脚本运行。

9 除了创建脚本还需要输入密码。

10 除了创建脚本还需要传参密码。

11 除了创建脚本还需要新增环境变量。

12 除了创建脚本还需要输入重定向。

13 除了创建脚本还需要输出重定向。

14 除了创建脚本还需要空环境变量。

# 5.level15~21:ipython

15 要求用 ipython 来运行程序,但是直接进入 ipython 后用命令:

run checker.py

来运行检测脚本会因为权限不足无法读取 /flag。。。

然后发现可以用命令:

import subprocess;subprocess.run(["./embryoio_level15"])

来运行得到 flag。

16 要求用 ipython 运行,还有输入密码(好家伙,又来一轮)

17 传参密码;18 新增环境变量;19 输入重定向;20 输出重定向;21 空环境

# 6.level22~28:python 脚本

嗯,老套路。

# 7.level29~35:c 语言

这个给了很多提示,大概是因为这个更涉及原理吧,原文如下:

[INFO] This challenge will now perform a bunch of checks.
[INFO] If you pass these checks, you will receive the flag.
[TEST] Performing checks on the parent process of this process.
[TEST] Checking to make sure that the process is a custom binary that you created by compiling a C program
[TEST] that you wrote. Make sure your C program has a function called 'pwncollege' in it --- otherwise,
[TEST] it won't pass the checks.
[HINT] If this is a check for the *parent* process, keep in mind that the exec() family of system calls
[HINT] does NOT result in a parent-child relationship. The exec()ed process simply replaces the exec()ing
[HINT] process. Parent-child relationships are created when a process fork()s off a child-copy of itself,
[HINT] and the child-copy can then execve() a process that will be the new child. If we're checking for a
[HINT] parent process, that's how you make that relationship.
[INFO] The executable that we are checking is: /usr/bin/bash.
[HINT] One frequent cause of the executable unexpectedly being a shell or docker-init is that your
[HINT] parent process terminated before this check was run. This happens when your parent process launches
[HINT] the child but does not wait on it! Look into the waitpid() system call to wait on the child!

[HINT] Another frequent cause is the use of system() or popen() to execute the challenge. Both will actually
[HINT] execute a shell that will then execute the challenge, so the parent of the challenge will be that
[HINT] shell, rather than your program. You must use fork() and one of the exec family of functions (execve(),
[HINT] execl(), etc).
[FAIL] You did not satisfy all the execution requirements.
[FAIL] Specifically, you must fix the following issue:
[FAIL]    The process must be your own program in your own home directory.

总之,总结一下看出来的知识点:

  1. 对父进程的检测需要使用 exec () 系列的函数,因为 system () 或 popen () 函数都会执行一个 shell,然后用 shell 来执行,所以此时父进程为 shell(测试后是 dash),而不是你的程序。
  2. 而 exec () 只是替换掉正在 exec () 的进程,当用 fork () 函数时子进程调用 exec () 系列函数杀死自己的子进程副本的时候就会建立与当前主进程的父子关系。
  3. 当一个父亲创建了一个孩子但没有等它结束就自己结束的话就可能造成系统异常,查询 waitpid () 的使用来等待孩子进程。

另外查询了一下,说是 exec 系统调用,实际上在 Linux 中,并不存在一个 exec () 的函数形式,exec 指的是一组函数,一共有 6 个,分别是:

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

然后整理了一下基本使用:

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

void pwncollege(char* argv[],char *env[]){
    execve("/challenge/embryoio_level29",argv,env);//使用exec系列函数执行时不会改变新进程的父亲,相当于只是将当前进>程替换掉了
    return ;
}

int main(int argc,char* argv[],char* env[]){
    pid_t fpid;

    fpid=fork();//fork()执行之后,会复制一个基本一样的进程作为子进程,然后两个进程会分别执行后面的代码
    if(fpid<0)//如果fpid为-1,说明fork失败
            printf("error in fork!\n");
    else if (fpid==0){//成功则会出现两个进程,fpid==0的是子进程
            printf("我是子进程\n");
            pwncollege(argv,env);
    }
    else{//fpid==1的是父进程
            printf("我是父进程\n");
            wait(NULL);
    }
    return 0;
}

然后又是一次轮回。

注意,设定参数数组时,第一个不是参数而是文件名,而且最后需要加 NULL(因为数组最后一位需要为 \0 吧。),这两个任意错了一步就会导致运行子进程失败:

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

void pwncollege(char* argv[],char *env[]){
    char *newargv[]={"embryoio_level31","scfxabffgf",NULL};
    execve("/challenge/embryoio_level31",newargv,env);//使用exec系列函数执行时不会改变新进程的父亲,相当于只是将当前>进程替换掉了
    return ;
}

int main(int argc,char* argv[],char* env[]){
    pid_t fpid;

    fpid=fork();//fork()执行之后,会复制一个基本一样的进程作为子进程,然后两个进程会分别执行后面的代码
    if(fpid<0)//如果fpid为-1,说明fork失败
            printf("error in fork!\n");
    else if (fpid==0){//成功则会出现两个进程,fpid==0的是子进程
            printf("我是子进程\n");
            pwncollege(argv,env);
    }
    else{//fpid==1的是父进程
            printf("我是父进程\n");
            wait(NULL);
    }
    return 0;
}

# 8.level36~65:管道

36: /challenge/embryoio_level36 | cat 37 关:grep

38 关:sed

39 关:rev

40 关:输入管道符

41 关:rev 输入

42~47:shell 脚本 + 管道,然后依次 grep,sed,rev,输入,rev 输入

48~53:ipython + 管道(找了好久教程结果发现其实和重定向操作一样,就是将输入流和输出流互相绑定一下,如下:

import subprocess
p1 = subprocess.Popen(["/challenge/embryoio_level48"], stdout=subprocess.PIPE)#只能用Popen,其他会失败
p2 = subprocess.Popen(["cat"], stdin=p1.stdout, stdout=subprocess.PIPE)
output = p2.communicate()[0]
print(output)

据提示说,pwntools 也能进行这样的管道操作,可是我实在没找到,process () 和 subprocess 中的 run () 一样,都不能进行这样的操作,之后再说吧。)

之后就是一轮新的循环了,没啥好说的。

54~59:python 脚本 + 管道。

60~65:c 语言 + 管道。(其中管道和父进程直接相连通了,所以直接操作父进程即可)

# 9.level66~67:find 命令 exec

66 是考 find 命令用 - exec 和 - ok 参数运行脚本:

find . -name "4*" -exec /challenge/embryoio_level66 \;

然后 67 让输入参数密码。

# 10.level68~73:shell 脚本杂项

68 让运行时以 shell 脚本方式,并且在第 284 个参数输入密码,一行搞定:

/challenge/embryoio_level* `printf ' nzfslgudsh%0.s' {1..284}`

69 让空参数,这咋弄,不会啊。

找了找方案,一种思路是加很多很多参数,让其参数列表 “爆表”,结果其实不行,linux 考虑到了这方面,经实验,参数到达 65529 个时就会报参数太长的错误(为啥是这个数字我也不知道)。

另一种思路就还是利用 c 语言中的 exec () 系列函数来执行进程,此时可以设置参数序列为空,问题在于题目要求是在 shell 脚本中执行,而且需要在 bash 或 sh 环境(经尝试用 system () 函数是用的 dash 环境)。

实在没办法了网上也没教程,我就仔细看了下它的 checker.py 源码是怎么写的,发现它的判断有点草率,如下:

def check_bash(process):
    print("[TEST] Checking to make sure the process is the bash shell. If this is a check for the parent process, then,")
    print("[TEST] most likely, this is what you do by default anyways, but we'll check just in case...")
    check_exe_basename(process, 'bash')
    assert len(process.cmdline()) == 1, f"The shell process must be running in its default, interactive mode (/bin/bash with no commandline arguments). Your commandline arguments are: {process.cmdline()}"

def check_shellscript(process):
    print("[TEST] Checking to make sure the process is a non-interactive shell script.")

    assert os.path.basename(process.exe()) in [ 'sh', 'bash' ], f"Process interpreter must be 'sh' or 'bash'. Yours is: {os.path.basename(process.exe())}"
    assert len(process.cmdline()) == 2 and process.cmdline()[1].endswith(".sh"), f"The shell process must be executing a shell script that you wrote like this: `bash my_script.sh`"

这两个判断其实只是检测了一下命令执行的文件名,第一个 bash 检测就是检测运行的程序名为 bash 即可,第二个检测是 bash 加一个结尾为 .sh 的参数即可,所以我就把自己编写的 c 语言程序改名为 bash,然后加个参数不使用即可,具体操作如下:

c 源码:

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

void pwncollege(char* argv[],char *env[]){
        char *newenv[]={NULL};
        execve("/challenge/embryoio_level69",newenv,env);//使用exec系列函数执行时不会改变新进程的父亲,相当于只是将当前 进程替换掉了
        return ;
}

int main(int argc,char* argv[],char* env[]){
        pid_t fpid;

        fpid=fork();//fork()执行之后,会复制一个基本一样的进程作为子进程,然后两个进程会分别执行后面的代码
        if(fpid<0)//如果fpid为-1,说明fork失败
                printf("error in fork!\n");
        else if (fpid==0){//成功则会出现两个进程,fpid==0的是子进程
                printf("我是子进程\n");
                pwncollege(argv,env);
        }
        else{//fpid==1的是父进程
                printf("我是父进程\n");
                wait(NULL);
        }
        return 0;
}

然后改名运行得到 flag:

#改名:
gcc test.c -o bash
#运行,随便加个结尾为.sh的参数,实际并未使用:
./bash a.sh

70:env

71:多参数 + env

72:重定向 + 临时文件夹

73:要求主进程和子进程路径不同,不知道为啥一直不能成功。

失败的方案:

echo "这是主进程"
pwd
cd;bash script.sh &
sleep 3
echo "这是子进程"
pwd
/challenge/e*
echo "这是主进程"

echo "子进程开始";cd /challenge;./e* &

echo "主进程开始等待";sleep 3;echo "主进程结束"

成功方案:

用了和上面那个一样的思路,shell 脚本中不容易显式的创建一个子进程或者新进程,就用 c 语言创建一个子进程来起作用,如下:

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

void pwncollege(char* argv[],char *env[]){
        char *newenv[]={"embryoio_level65",NULL};
        chdir("/tmp/coooni");
        printf("current working directory: %s\n", getcwd(NULL, NULL));
        execve("/challenge/embryoio_level73",argv,env);//使用exec系列函数执行时不会改变新进程的父亲,相当于只是将当前进 程替换掉了
        return ;
}

int main(int argc,char* argv[],char* env[]){
        pid_t fpid;

        fpid=fork();//fork()执行之后,会复制一个基本一样的进程作为子进程,然后两个进程会分别执行后面的代码
        if(fpid<0)//如果fpid为-1,说明fork失败
                printf("error in fork!\n");
        else if (fpid==0){//成功则会出现两个进程,fpid==0的是子进程
                printf("我是子进程\n");
                pwncollege(argv,env);
        }
        else{//fpid==1的是父进程
                printf("我是父进程\n");
                wait(NULL);
        }
        return 0;
}

# 11.level74~79:python 脚本杂项

74:让第 264 个参数为密码

75:要求空参数,我尝试了 os.execve 和 os.execvp,发现它底层的 os.execv () 要求参数二不能为空,和 c 语言的要求不太一样,就很无语。

所以故技重施,假冒 python 的 c 程序,但是执行报了一个很让人无语的检测失败:

[FAIL]    Executable must be 'python'. Yours is: python

这不一样嘛!唉,还得看源码:

def check_exe_basename(process, basename):
    print(f"[INFO] The process' executable is {process.exe()}.")
    if os.path.basename(process.exe()) == "docker-init":
        print("[WARN] This process is the initialization process of your docker container (aka PID 1).")
        print("[WARN] When the parent of a process terminates, that process is 'reparented' to PID 1.")
        print("[WARN] So, the likely situation here is that your parent process terminated before")
        print("[WARN] waiting on the child. Go fix that :-). Look into waitpid() in C, process.wait() for")
        print("[WARN] pwntools, or Popen.wait() for subprocess.")
    else:
        print("[INFO] This might be different than expected because of symbolic links (for example, from /usr/bin/python to /usr/bin/python3 to /usr/bin/python3.8).")

    actual_basename = os.path.basename(os.path.realpath(shutil.which(basename)))
    print(f"[INFO] To pass the checks, the executable must be {actual_basename}.")
    assert os.path.basename(process.exe()) == actual_basename, f"Executable must be '{basename}'. Yours is: {os.path.basename(process.exe())}"

def check_python(process):
    print("[TEST] We will now check that that the process is a non-interactive python instance (i.e., an executing python script).")
    check_exe_basename(process, 'python')
    assert len(process.cmdline()) == 2 and process.cmdline()[1].endswith(".py"), f"The python process must be executing a python script that you wrote like this: `python my_script.py`"

额,看来问题出在这个 actual_basename 上了,人家查的是实际的解析器名称,查一下:

hacker@embryoio_level75:~$ ls -l /bin/python
lrwxrwxrwx 1 root root 7 Apr 15  2020 /bin/python -> python3
hacker@embryoio_level75:~$ ls -l /bin/python3
lrwxrwxrwx 1 root root 9 Mar 13  2020 /bin/python3 -> python3.8
hacker@embryoio_level75:~$ ls -l /bin/python3.8
-rwxr-xr-x 1 root root 5490488 Sep 28 16:10 /bin/python3.8

果然,把程序名改为 python3.8 就通过了。

76:修改环境变量,这个 subprocess.run 就有参数可以指定。

77:参数密码 + 环境变量

78:输入重定向 + 临时文件夹

79:临时文件夹 + 父子进程工作目录不同

# 12.level80~85:子进程杂项

80:c 程序子进程 + 参数设置

81:空参数

82:环境变量

83:参数设置 + 环境变量

84:工作路径 + 输入重定向

85:工作路径 + 父子进程工作路径不同

# 13.level86~87:数据交互

86:shell 脚本 + 数据交互

87:shell 脚本 + 5 个数据交互(简单运算)

# 14.level88~89:改变 argv [0]

88:shell 脚本 + 要求改变 argv [0] 的值,没办法,再次采用魔道打法

89:shell 脚本 + 要求改变 argv [0] 的值

我正纳闷为啥要重复考这个,忽然发现人家给提示了!!对嘛,不然我只能一招吃遍天下了:

argv[0] is passed into the execve() system call *separately* from the program path to execute.This means that it does not have to be the same as the program path, and that you can actually control it. This is done differently for different methods of execution. For example, in C, you simply need to pass in a different argv[0]. Bash has several ways to do it, but one way is to use a combination of a symbolic link (e.g., the `ln -s` command) and the PATH environment variable.

翻译版本:

argv[0]从要执行的程序路径分别传递到execve()系统调用*中。这意味着它不必与程序路径相同,您可以实际控制它。这对于不同的执行方法是不同的。例如,在C中,您只需要传入不同的argv[0]。Bash有几种方法可以做到这一点,但其中一种方法是使用符号链接(例如,`ln -s`命令)和PATH环境变量的组合。

what!好像很简单,我之前一直想复杂了吗?

操作如下:

#在本目录建立软链接:
ln -s /challenge/embryoio_level89 ydntbk

#将当前目录作为首个PATH变量
export PATH=.:$PATH
echo "ydntbk" > script.sh
bash script.sh

耶!学到了,不过它说还有几种办法不知道是什么,另外在 shell 脚本中将参数数目变为空真的有可能吗。。

# 15.level90~93:命名管道 fifo

90:让使用 FIFO,我一直以为 mkfifo 和管道符一样呢,原来有区别啊。

总而言之说一下我的理解,也懒得找官方解释。

简单来说就是一个有实体的管道,创建时会以文件的形式出现在当前目录中,好像是单工通信,只能由一方传给另一方,而且如果没有消费者,生产者会阻塞等待,所以我们在运行时需要并发运行两个进程,不然会阻塞。

下面放一下我的通关过程:

#创建FIFO通道:
mkfifo test
#写入shell脚本,是读方
echo '/challenge/embryoio_level* < test' > script.sh
#同时运行读写进程,所以需要用算数运算符|或&进行连接,注意不能用分号;,因为那样是串行执行:
echo vctmlgmx > test | bash script.sh

91:fifo 写重定向

92:fifo 读写重定向(需要 2 个通道)

93:要求写个可交互式的双通道,写了如下 shell 脚本:

/challenge/embryoio_level* < t1 > t2 &
cat < t2 &
cat > t1

# 16.level94:绑定文件描述符 exec

94:要求绑定 235 号文件描述符,想到 exec 命令可以绑定,通过代码如下:

echo 'exec 235<t1;/challenge/embryoio_level*' > script.sh
bash script.sh & cat > t1

# 17.level95~96:简单

95,96:shell 脚本 + 输入密码(这么简单不知道是什么鬼,也没有提示)

# 18.level97~98:信号 signal

97:要求给它发送指定信号,不知道怎么就过了(哦,它估计让发送一个 SIGINT 的信号,ctrl+c 正好是)

98:要求按顺序给它发送 5 个信号,这次了解了下 linux 下信号的知识,知道了利用 kill 命令发送信号,如下通关代码:

bash script.sh &
#回显:[TEST] You must send me (PID 90) the following signals, in exactly this order: ['SIGINT', 'SIGHUP', 'SIGHUP', 'SIGHUP', 'SIGHUP']
kill -SIGINT 90
kill -SIGHUP 90
kill -SIGHUP 90
kill -SIGHUP 90
kill -SIGHUP 90

# 19.level99~111:python 脚本杂项 2

99,100:python 脚本 + 简单运算

101,102:修改 argv [0]

103,104,105:fifo

106:fifo + 简单运算

107:设定 125 文件描述符

想要学上面在 shell 脚本中使用的方法,然后遇到了很多坑才写出来,如下:

p.py 内容:

import subprocess
import os
f=open("./t1")#打开fifo通道,因为没有写端,所以打开时会阻塞
f2=os.dup2(f.fileno(),125)#用os.dup2复制通道的文件描述符到125号文件描述符,python3.4以后文件描述符是默认子进程可共享的
print("新的文件描述符:%s"%f2)
print("可继承位:%s"%os.get_inheritable(f2))#检验是否真的子进程可继承,真的为True
#print("文件内容:%s"%f2.read())
#print(f.read())
argv=["/challenge/embryoio_level107"]
p=subprocess.Popen(argv,pass_fds=[0,1,2,125])#重点来了!子进程要想获取到这些主进程的文件描述符,必须要在调用Popen时设定pass_fds数组的值,不然无法获取,这里卡了好久。。。。
p.wait()#最后一个坑,主进程需要阻塞等待子进程,不然它的父亲会变成它爷爷,哈哈

顺带一提,pwntools 子进程想要使用先前绑定的文件描述符需要在 process 时设定参数 ** close_fds=False **,默认是关闭了除 0~2 的所有文件描述符的。

调用:

#因为打开命名通道时会阻塞,所以把它放到后台运行
python p.py & cat > t1

不过突然想到好像也不用什么通道,直接把标准输入 0 给复制过去不就行了(嗯,试了一下确实可以)。

108,109:输入密码

110,111:发送信号

# 20.level112~124:c 语言杂项

112,113:c 程序 + 简单运算

114,115:设置 argv [0]

和上面一样的循环。

# 21.level125~139:交互脚本编写

125:shell 脚本,它让运算 50 次,很明显是让写脚本了。

但是利用 linux 原生的工具进行操作我不太熟悉,更何况这个一般会 python 即可,就用 python 写吧:

#!/usr/bin/python3
from pwn import *

p=process("/challenge/embryoio_level125")
for i in range(50):
    #print(p.recvline())
    print(p.recvuntil(b":\n"))
    s=p.recvline()
    print(s)
    tmp=eval(s)
    print(tmp)
    p.sendline(b"%d"%tmp)

print(p.recvall())

结果发现不行,因为人家要求解析器为 bash,所以不能直接执行 python。

我又换了一种思路,bash 脚本虽然不会写,但是写个 python 运算器脚本,和原进程进行交互不就行了,所以写了如下一个运算器:

t.py 源码:

f1=open("./t1","rb")#利用t1通道进行输入数据
f2=open("./t2","wb")#t2通道输出数据
for i in range(200):
    s=f1.readline()
    print(s)
    index=s.find(b": ")#如果有这个标志说明这一行是需要运算的
    if index != -1:
        t1=s[index+2:-1]
        print(t1)
        t2=eval(t1)#运算得到结果
        print(b"%d\n"%t2)
        f2.write(b"%d\n"%t2)#写入t2传回结果
        f2.flush()#缓冲区清空是个大坑啊,我又栽这了好久。。。不加这一行原程序得不到我们的运行结果

f1.close()
f2.close()

script.sh 内容:

#调试用命令:
python t.py & cat < t2 & cat > t1
/challenge/embryoio_level* > t1 < t2 & cat < t1 & cat > t2
/challenge/embryoio_level* > t1 < t2 & python t.py & cat > t2

#通关命令:
/challenge/embryoio_level* > t1 < t2 & python t.py

126:要输入 500 个答案,哈哈,这次运算也变得特别复杂,把输入给过滤掉可能能运算快点。

稍微升级了下上面的脚本:

f1=open("./t1","rb")#利用t1通道进行输入数据
f2=open("./t2","wb")#t2通道输出数据
sum=0
for i in range(3000):
    s=f1.readline()
    if sum >=500:
        print(s)
        print(f1.read())
        break
    index=s.find(b": ")#如果有这个标志说明这一行是需要运算的
    if index != -1:
        sum+=1
        t1=s[index+2:-1]
        #print(t1)
        t2=eval(t1)#运算得到结果
        print("%d : %d"%(sum,t2))
        f2.write(b"%d\n"%t2)#写入t2传回结果
        f2.flush()#缓冲区清空是个大坑啊,我又栽这了好久。。。不加这一行原程序得不到我们的运行结果

f1.close()
f2.close()

127:让发送给进程 50 个指定信号,很明显又是脚本编写。

但是问题在于那个程序直接输入进 fifo 命名管道里好像用了全缓冲区,导致我无论如何也无法在运行的同时获取到提示,这咋弄啊。(后来发现好像不是缓冲区的问题,看了下 checker.py 的源码,在执行这个挑战的时候会截取所有传到这个进程的信号进行处理,而使用管道的话管道好像也需要使用信号,就导致获取不到数据?好像也不对啊,在监听信号之前也有一部分输出的,但是那部分的输出也看不到,这有点说不通。)

冥思苦想了好长时间,首先想到另外一个魔道打法(我怎么总是搞这些奇奇怪怪的打法,误)。

一、把 python3.8 复制过来,改名为 bash,这样就可以骗过 checker.py 说解析器是 bash 了

当然这个方法上面的几道题也能用。

这样做的好处是,虽然我不会处理怎么让程序运行时输出到命名管道里怎么无缓冲区输出,但是利用 pwntools 或者 subprocess 就没有这个烦恼了啊,然后只要将 python 脚本的后缀改为 sh 就能骗过检测了。

如下操作:

cp /usr/bin/python3.8 bash
./bash 127.sh

127.sh 源码:

from pwn import *
import os
import signal

sh=process("/challenge/embryoio_level127")
#print(sh.recvline())
#print(sh.recvline())
sh.recvuntil(b"(PID ")
pid=int(sh.recvuntil(b")")[:-1])
print("该程序的pid为%d"%pid)
sh.recvuntil(b": ")

s=sh.recvline()[:-1]
print("信号序列为")
print(s)
a=eval(s)
for i in range(len(a)):
	print("正在发送信号:%d"%i)
	print(pid,eval("signal."+a[i]))
	os.kill(pid,eval("signal."+a[i]))
	sh.recvline()
	sh.recvline()
print(sh.recvall())

二、第二个想法是昨天查了查资料,说命名管道 fifo 其实可以控制阻塞输出和缓冲区大小,但是需要在 c 语言中调用 api,所以如果操控好的话说不定也能获取到信息。

128:猜都猜得出来,发送 500 个信号,直接用上面的脚本。

129:有点变态,要求我用 shell 脚本编写,并且控制它的输入流指向 cat,输出流也指向 cat,还要我算 50 个复杂运算。

所以我们只需要控制第一个 cat 的输入流和第二个 cat 的输出流即可。

测试脚本(ps:这个脚本对 127 的题不起作用,就很神奇):

f1=open("./t1","r")
f2=open("./t2","w")
while True:
    s=f1.readline()
    print("获取到了数据:")
    print(s)
f1.close()
f2.close()

通关脚本:

f1=open("./t1","r")
f2=open("./t2","w")
count=0
for i in range(3000):
    if count >= 50:
        print(f1.read(1024))
        break
    s=f1.readline()
    #print("获取到了数据:")
    print(s,end='')
    if ": " in s:
        count+=1
        index=s.find(": ")
        a=s[index+2:-1]
        b=eval(a)
        print(b)
        f2.write("%d\n"%b)
        f2.flush()

f1.close()
f2.close()

script.sh 内容:

cat < t2 | /challenge/embryoio_level* | cat > t1 &
python 129.py

130:用 python 脚本运算 50 次,直接用上面脚本

131:500 次

重复上面的。

然后 134 在管道上遇到了问题,然后研究了好长时间发现 python 可以直接利用 os.pipe () 创建一个 linux 的管道,真的是,白忙活了,不管怎么样学到了:

import subprocess
import fcntl
import os

r1,w1=os.pipe()

p0 = subprocess.Popen(["cat"], stdin=subprocess.PIPE, stdout=w1)
#flags = fcntl.fcntl(p0.stdout, fcntl.F_GETFL)
#fcntl.fcntl(p0.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK)#解为非阻塞

r2,w2=os.pipe()

p1 = subprocess.Popen(["/challenge/embryoio_level134"],stdin=r1, stdout=w2)
#flags = fcntl.fcntl(p1.stdout, fcntl.F_GETFL)
#fcntl.fcntl(p1.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK)


p2 = subprocess.Popen(["cat"], stdin=r2, stdout=subprocess.PIPE)

#flags = fcntl.fcntl(p2.stdout, fcntl.F_GETFL)
#fcntl.fcntl(p2.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK)

for i in range(50):
	s=''
	while True:
		s=p2.stdout.readline().decode()
		print(s,end="")
		if ": " in s or s=="":
			break

	a=s[s.find(": ")+2:-1]
	print("式子为%s"%a)
	b=eval(a)
	print("答案为%d"%b)
	p0.stdin.write(b"%d\n"%b)
	p0.stdin.flush()

for i in range(50):
	print(p2.stdout.readline().decode(),end="")

135 开始又是 c 语言的循环,因为 c 程序的子进程可以直接继承主进程的通道关系,所以类似于上面的 bash 时的脚本,直接交互即可。

但是在 137 时又遇到了上面那个问题,信号的脚本获取不到数据啊,只要一用通道就得不到它的数据了,不知道什么毛病。

这次想要浑水摸鱼好像有点不行,因为它除了要求用 c 程序运行以外,还要求 argv [0] 为空,这个我只能通过 c 程序来实现,python 的 os 库虽然也有 exec 系列函数,但是参数长度不能为空,没办法实现,就很神奇。

为啥就这个测试一旦使用通道就获取不到输出啊,只有结束运行之后才会出现显示结果。

(后来我觉得有点不对劲,为啥 c 程序运行就能通过 argc==1 的检测,而 python 代码不能通过,而且前面的 argc==1 应该不是检测 argv [0] 为空的意思,所以就自己实验了一下,发现之前果然是我理解错了,一直有个地方没理解对。

c 程序是编译程序,而 python 和 shell 脚本是脚本解释形语言,而脚本解释语言的执行则需要一个解析器来执行,这就是脚本开头需要设置一个如:

#!/usr/bin/python
#!/bin/bash

这样的语句来指定解释器的地址。

而 checker.py 在检查的时候是这样检查的:

len(psutil.Process(os.getpid()).cmdline())==1

而直接 ./test.py 这样执行时, psutil.Process(os.getpid()).cmdline() 产生的序列是这样的:

['/usr/bin/python', './test.py']

也就是说它在运行时会把解析器当做第一个参数来执行,这样序列长度就为 2 了,这就是我用 python 和 bash 脚本通过不了的原因。

解决办法也简单,既然直接执行必须调用解释器,那我打包之后再执行不就行了?

嗯,尝试了下,用 pyinstaller 包可以很轻易的打包好 python 脚本,但是问题在于,题目中的检测还有一层:它调用了 /usr/bin/nm 进行检测,要求有一个名为 pwncollege 的函数,而问题在于,用 pyinstaller 进行编译链接的时候,应该是把所有全局标志都去除了,导致用 nm 检测时没有标志,这就很无奈了。

(研究了下又有新发现,发现虽然用 ./a.out | cat 这样获取不到输出,但是用 python 来:

p=subprocess.Popen("./a.out",stdin=subprocess.PIPE,stdout=subprocess.PIPE)

却可以获取到输出,这就好说了,直接可以用上面的脚本改一下,把运行程序改成 c 程序即可:

from pwn import *
import os
import signal

sh=process("./137.out")
#print(sh.recvline())
#print(sh.recvline())
sh.recvuntil(b"(PID ")
pid=int(sh.recvuntil(b")")[:-1])
print("该程序的pid为%d"%pid)
sh.recvuntil(b": ")

s=sh.recvline()[:-1]
print("信号序列为")
print(s)
a=eval(s)
for i in range(len(a)):
	print("正在发送信号:%d"%i)
	print(pid,eval("signal."+a[i]))
	os.kill(pid,eval("signal."+a[i]))
	sh.recvline()
	sh.recvline()
print(sh.recvall())

呼,总算把这两个通关了,没想到成为难到我的最后两题。

139 也是运算 50 次,我基本上直接把 129.py 拿来使用了,但是有个检查比较奇怪,它要求主进程被 cat 运行,我新建了个名为 cat 的 bash,然后这样执行:

cat < t2 | env -i "SHELL=./cat" ./a.out | cat > t1 & python 139.py

就通过了,但是不太清楚是不是这样过的,也不知道它想考啥。

# 22.level140~142:网络

140 这道题感觉很奇怪,正常来做感觉不可能达成啊。

在运行题目的程序时,它在本地的 1166 端口开了一个服务,可以接收其他程序来的 tcp 连接。

问题在于,它会检测你是不是通过 shell 脚本来连接的客户端,除此之外它还要求解析器必须为 bash 或 sh。

问题在于,我在使用 netcat 进行访问时它会说解析器是 netcat,甚至我用这样:

cat < /dev/tcp/127.0.0.1/1166

来访问,它也会说解析器是 cat,无论在交互式环境执行还是 shell 脚本中。

这就导致我如果想让它解析器识别为 bash,必须在 shell 脚本中不依赖其他工具,直接访问到指定网络端口,但是不知道是不是我孤陋寡闻了,我不知道这样的方法也查不到。如果想要在 shell 脚本中访问网络,其实就是调用其他的工具进行执行啊,怎么可能只是利用 bash?

想不到方法的我只好又用了之前的方法,复制一个名为 bash 的 python3.8,在 140.sh 文件中写入 python 代码,用 python 写一个小网络连接器,这样就能满足它的所有条件了。

不知道它期待中的解法是怎么样的。

140.sh 源码:

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

import socket
import threading
import time
from sys import argv,stdout,stderr,version_info

PY2 = True if version_info[0] == 2 else False
if PY2:
	input=raw_input
	cout=stdout
else:
	cout=stdout.buffer

def recvdata(conn):
	while not event.is_set():
		try:
			data=conn.recv(4096)
			if not data:
				stderr.write("客户端已断开连接\n")
				event.set()
				conn.close()
				break
			cout.write(data)
			stdout.flush()
		except Exception as e:
			stderr.write(str(e))
			conn.close()
			event.set()
			stderr.write("已断开连接\n")

def senddata(conn):
	try:
		if PY2:
			while not event.is_set():
				data=input()
				if not event.is_set():
					conn.sendall(data+b'\n')
		else:
			while not event.is_set():
				data=input()
				if not event.is_set():
					conn.sendall(data.encode()+b'\n')
	except Exception as e:
		stderr.write(e)
		conn.close()
		event.set()
		stderr.write("发送数据错误!\n")

def listener(host,port):
	if type(port)==type(''):
		port=int(port)
	s= socket.socket(socket.AF_INET,socket.SOCK_STREAM)
	s.bind((host,port))
	s.listen(1)
	stderr.write("listening on %s %s\n"%(host,port))
	conn,addr=s.accept()
	stderr.write("Connection received on %s %s\n"%addr)
	try:
		read=threading.Thread(target=recvdata,args=(conn,))
		read.setDaemon(True)#使线程在主进程结束时终止
		read.start()
		write=threading.Thread(target=senddata,args=(conn,))
		write.setDaemon(True)
		write.start()
		while not event.is_set(): time.sleep(1)
	except KeyboardInterrupt as e:
		conn.close()
		event.set()
		stderr.write("\n断开连接\n")

def requester(host,port):
	if type(port)==type(''):
		port=int(port)
	s= socket.socket(socket.AF_INET,socket.SOCK_STREAM)
	s.connect((host,port))
	stderr.write("connected to %s %s\n"%(host,port))
	conn=s
	try:
		read=threading.Thread(target=recvdata,args=(conn,))
		read.setDaemon(True)#使线程在主进程结束时终止
		read.start()
		write=threading.Thread(target=senddata,args=(conn,))
		write.setDaemon(True)
		write.start()
		while not event.is_set(): time.sleep(1)
	except KeyboardInterrupt as e:
		conn.close()
		event.set()
		stderr.write("\n断开连接\n")
		
def main():
	stderr.write('''Usage:  ./myrequester.py <ip> <port>''')
	if len(argv)==3:
		host,port=argv[1:]
	elif len(argv)==2:
		port=argv[1]
	else:
		host=input("请输入要请求的ip地址:")
		if host=="":
			host="0.0.0.0"
		port=input("请输入要请求的端口:")
	requester(host,port)
	
if __name__=='__main__':
	event=threading.Event()
	main()

连上之后它会让你运算 5 次简单运算,输入即给 flag。

141:让用 python 脚本网络连接,直接用上面的脚本。

142:让用 c 程序来网络连接,因为 python 打包的程序不能包含 pwnchallenge () 函数,所以老老实实的学了 c 语言的网络连接的方法写了个通过程序:

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>


#define PORT 1848
#define SIZE 1024

void pwncollege(){
	return;
}

int ainb(char* a,char* b){
	long unsigned int len1=strlen(a);
	long unsigned int len2=strlen(b);
	//printf("len(a):%ld\n",len1);
	//printf("len(b):%ld\n",len2);
	
	if(len1>len2) return 0;
	
	int xflag=0;
	for(int i=0;i<len2-len1;i++){
		if(strncmp(a,b+i,len1)==0){
			xflag=1;
			break;
		}
	}
	return xflag;
}

int main(int argc,char* argv[],char* env[])
{
	int client_socket = socket(AF_INET, SOCK_STREAM, 0);   //创建和服务器连接套接字
	if(client_socket == -1)
	{
		perror("socket");
		return -1;
	}
	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(addr));
	
	addr.sin_family = AF_INET;  /* Internet地址族 */
    addr.sin_port = htons(PORT);  /* 端口号 */
    addr.sin_addr.s_addr = htonl(INADDR_ANY);   /* IP地址 */
	inet_aton("127.0.0.1", &(addr.sin_addr));

	int addrlen = sizeof(addr);
	int listen_socket =  connect(client_socket,  (struct sockaddr *)&addr, addrlen);  //连接服务器
	if(listen_socket == -1)
	{
		perror("connect");
		return -1;
	}
	
	printf("成功连接到一个服务器\n");
	
	char buf[SIZE] = {0};
	int ret;
	
	while(1)        //向服务器发送数据,并接收服务器转换后的大写字母
	{
		//读取服务器内容
		//int ret = read(client_socket, buf, strlen(buf));
		while(1){
			memset(buf, 0, SIZE);//每次读取前重置
			ret = read(client_socket, buf, SIZE);
			printf("ret = %d\n", ret);
			if(0>=ret) break;
			printf("buf = %s", buf);
			printf("\n");
			if(1==ainb("Please send the solution for: ",buf))
			{
				break;
			}
		}
		
		printf("请输入你相输入的:");
		scanf("%s", buf);
		write(client_socket, buf, strlen(buf));
		write(client_socket, "\n", 1);
	}
	close(listen_socket);
	
	return 0;
}

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

lxy WeChat Pay

WeChat Pay

lxy Alipay

Alipay

lxy PayPal

PayPal